Improve loading / failed states in ai editor.

This commit is contained in:
John Preston
2026-03-25 21:28:02 +07:00
parent 3da3334131
commit 577e8f14af
5 changed files with 324 additions and 131 deletions
+146 -130
View File
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/choose_language_box.h"
#include "ui/controls/send_button.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/skeleton_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/text/custom_emoji_helper.h"
@@ -54,6 +55,21 @@ enum class ComposeAiMode {
Fix,
};
enum class CardState {
Waiting,
Loading,
Ready,
Failed,
};
[[nodiscard]] QColor ComposeAiColorWithAlpha(
const style::color &color,
float64 alpha) {
auto result = color->c;
result.setAlphaF(result.alphaF() * alpha);
return result;
}
[[nodiscard]] QString ToneTitle(const QString &key) {
const auto value = Lang::GetNonDefaultValue(
QByteArray("cloud_lng_ai_compose_style_") + key.toUtf8());
@@ -177,14 +193,6 @@ enum class ComposeAiMode {
return st::aiComposeStyleTabsRadius;
}
[[nodiscard]] QColor ComposeAiColorWithAlpha(
const style::color &color,
float64 alpha) {
auto result = color->c;
result.setAlphaF(result.alphaF() * alpha);
return result;
}
[[nodiscard]] QColor ComposeAiActiveBackgroundColor(
const style::color &color) {
return ComposeAiColorWithAlpha(
@@ -328,9 +336,8 @@ public:
void setResultTitle(const TextWithEntities &title);
void setEmojifyVisible(bool visible);
void setEmojifyChecked(bool checked);
void setLoading(bool loading);
void setResultVisible(bool visible);
void setResult(TextWithEntities text, bool copyVisible);
void setState(CardState state);
void setResultText(TextWithEntities text);
protected:
int resizeGetHeight(int newWidth) override;
@@ -355,13 +362,11 @@ private:
Fn<void(bool)> _emojifyChanged;
bool _originalExpanded = false;
bool _originalVisible = true;
bool _loading = false;
bool _hasShownResult = false;
bool _resultVisible = true;
bool _copyVisible = false;
bool _emojifyVisible = false;
bool _dividerVisible = false;
int _dividerTop = 0;
CardState _state = CardState::Waiting;
Ui::SkeletonAnimation _skeleton;
std::array<Ui::Text::SpecialColor, 2> _diffColors;
};
@@ -420,7 +425,7 @@ private:
ComposeAiMode _mode = ComposeAiMode::Translate;
int _styleIndex = -1;
bool _emojify = false;
bool _ready = false;
CardState _state = CardState::Waiting;
mtpRequestId _requestId = 0;
int _requestToken = 0;
TextWithEntities _result;
@@ -1003,7 +1008,8 @@ ComposeAiPreviewCard::ComposeAiPreviewCard(
this,
tr::lng_ai_compose_emojify(tr::now),
st::aiComposeEmojifyCheckbox,
std::make_unique<Ui::RoundCheckView>(st::defaultCheck,false))) {
std::make_unique<Ui::RoundCheckView>(st::defaultCheck,false)))
, _skeleton(_resultBody) {
_originalBody->setSelectable(true);
_originalBody->setMarkedText(_original, _context);
_resultTitle->setClickHandlerFilter([=](const auto &...) {
@@ -1036,7 +1042,8 @@ ComposeAiPreviewCard::ComposeAiPreviewCard(
}, _emojify->lifetime());
setOriginalTitle(tr::lng_ai_compose_original(tr::now));
setResultTitle(tr::lng_ai_compose_result(tr::now, tr::marked));
setLoading(true);
_resultBody->setMarkedText(_original, _context);
_copy->setVisible(false);
updateOriginalToggleIcon();
}
@@ -1088,29 +1095,38 @@ void ComposeAiPreviewCard::setEmojifyChecked(bool checked) {
refreshGeometry();
}
void ComposeAiPreviewCard::setLoading(bool loading) {
_loading = loading;
if (loading && !_hasShownResult) {
_resultBody->setMarkedText(_original, _context);
}
_copy->setVisible(!loading && _copyVisible);
refreshGeometry();
}
void ComposeAiPreviewCard::setResultVisible(bool visible) {
if (_resultVisible == visible) {
void ComposeAiPreviewCard::setState(CardState state) {
if (_state == state) {
return;
}
_resultVisible = visible;
const auto wasLoading = (_state == CardState::Loading);
_state = state;
switch (_state) {
case CardState::Waiting:
case CardState::Failed:
_resultBody->setMarkedText(_original, _context);
_copy->setVisible(false);
if (wasLoading) {
_skeleton.stop();
}
break;
case CardState::Loading:
_resultBody->setMarkedText(_original, _context);
_copy->setVisible(false);
_skeleton.start();
break;
case CardState::Ready:
_copy->setVisible(true);
if (wasLoading) {
_skeleton.stop();
}
break;
}
refreshGeometry();
}
void ComposeAiPreviewCard::setResult(TextWithEntities text, bool copyVisible) {
_loading = false;
_hasShownResult = true;
_copyVisible = copyVisible;
_resultBody->setMarkedText(text, _context);
_copy->setVisible(_copyVisible);
void ComposeAiPreviewCard::setResultText(TextWithEntities text) {
_resultBody->setMarkedText(std::move(text), _context);
refreshGeometry();
}
@@ -1165,70 +1181,61 @@ int ComposeAiPreviewCard::resizeGetHeight(int newWidth) {
_originalToggle->hide();
}
if (_resultVisible) {
_resultTitle->show();
_resultBody->show();
auto controlsWidth = 0;
if (_emojifyVisible) {
_emojify->show();
_emojify->resizeToNaturalWidth(contentWidth);
controlsWidth += _emojify->width()
+ st::aiComposeCardControlSkip;
} else {
_emojify->hide();
}
const auto resultTitleWidth = std::max(
contentWidth - controlsWidth,
0);
_resultTitle->resizeToWidth(resultTitleWidth);
auto right = padding.right();
if (_emojifyVisible) {
_emojify->moveToRight(right, y, newWidth);
right += _emojify->width() + st::aiComposeCardControlSkip;
}
_resultTitle->setGeometryToLeft(
padding.left(),
y,
resultTitleWidth,
_resultTitle->height(),
newWidth);
y += std::max({
_resultTitle->height(),
_emojifyVisible ? _emojify->height() : 0,
});
y += st::aiComposeCardTextSkip;
const auto lineHeight = _resultBody->st().style.lineHeight
? _resultBody->st().style.lineHeight
: _resultBody->st().style.font->height;
if (_copy->isVisible()) {
_resultBody->setSkipBlock(
_copy->width(),
lineHeight);
} else {
_resultBody->setSkipBlock(0, 0);
}
_resultBody->resizeToWidth(contentWidth);
_resultBody->setGeometryToLeft(
padding.left(),
y,
contentWidth,
_resultBody->height(),
newWidth);
if (_copy->isVisible()) {
_copy->moveToRight(
padding.right(),
y + _resultBody->height() - lineHeight,
newWidth);
}
y += _resultBody->height();
_resultTitle->show();
auto controlsWidth = 0;
if (_emojifyVisible) {
_emojify->show();
_emojify->resizeToNaturalWidth(contentWidth);
controlsWidth += _emojify->width()
+ st::aiComposeCardControlSkip;
} else {
_resultTitle->hide();
_resultBody->hide();
_copy->hide();
_emojify->hide();
}
const auto resultTitleWidth = std::max(
contentWidth - controlsWidth,
0);
_resultTitle->resizeToWidth(resultTitleWidth);
auto right = padding.right();
if (_emojifyVisible) {
_emojify->moveToRight(right, y, newWidth);
right += _emojify->width() + st::aiComposeCardControlSkip;
}
_resultTitle->setGeometryToLeft(
padding.left(),
y,
resultTitleWidth,
_resultTitle->height(),
newWidth);
y += std::max({
_resultTitle->height(),
_emojifyVisible ? _emojify->height() : 0,
});
y += st::aiComposeCardTextSkip;
const auto lineHeight = _resultBody->st().style.lineHeight
? _resultBody->st().style.lineHeight
: _resultBody->st().style.font->height;
if (_copy->isVisible()) {
_resultBody->setSkipBlock(
_copy->width(),
lineHeight);
} else {
_resultBody->setSkipBlock(0, 0);
}
_resultBody->resizeToWidth(contentWidth);
_resultBody->setGeometryToLeft(
padding.left(),
y,
contentWidth,
_resultBody->height(),
newWidth);
if (_copy->isVisible()) {
_copy->moveToRight(
padding.right(),
y + _resultBody->height() - lineHeight,
newWidth);
}
y += _resultBody->height();
return y + padding.bottom();
}
@@ -1298,7 +1305,7 @@ ComposeAiContent::~ComposeAiContent() {
}
bool ComposeAiContent::hasResult() const {
return _ready;
return _state == CardState::Ready;
}
const TextWithEntities &ComposeAiContent::result() const {
@@ -1385,7 +1392,7 @@ void ComposeAiContent::chooseLanguage() {
}
void ComposeAiContent::copyResult() {
if (!_ready) {
if (_state != CardState::Ready) {
return;
}
TextUtilities::SetClipboardText(
@@ -1397,6 +1404,8 @@ void ComposeAiContent::setMode(ComposeAiMode mode) {
return;
}
_mode = mode;
_state = CardState::Waiting;
_preview->setState(CardState::Waiting);
if (_modeChanged) {
_modeChanged(_mode);
}
@@ -1407,20 +1416,21 @@ void ComposeAiContent::setMode(ComposeAiMode mode) {
}
void ComposeAiContent::updateTitles() {
const auto styleNoSelection = (_mode == ComposeAiMode::Style)
&& (_styleIndex < 0);
_preview->setOriginalVisible(
_mode != ComposeAiMode::Style || styleNoSelection);
_preview->setResultVisible(!styleNoSelection);
const auto hasResult = (_state == CardState::Loading)
|| (_state == CardState::Ready);
_preview->setOriginalVisible(hasResult);
_preview->setOriginalTitle(
(_mode == ComposeAiMode::Translate)
? FromTitle(_detectedFrom)
: tr::lng_ai_compose_original(tr::now));
_preview->setResultTitle(
(_mode == ComposeAiMode::Translate)
? ToTitle(_to)
: tr::lng_ai_compose_result(tr::now, tr::marked));
_preview->setEmojifyVisible(_mode != ComposeAiMode::Fix);
hasResult
? ((_mode == ComposeAiMode::Translate)
? ToTitle(_to)
: tr::lng_ai_compose_result(tr::now, tr::marked))
: tr::lng_ai_compose_original(tr::now, tr::marked));
_preview->setEmojifyVisible(
hasResult && (_mode != ComposeAiMode::Fix));
_preview->setEmojifyChecked(_emojify);
}
@@ -1449,9 +1459,10 @@ void ComposeAiContent::request() {
return;
}
cancelRequest();
_ready = false;
_state = CardState::Loading;
_result = {};
_preview->setLoading(true);
_preview->setState(CardState::Loading);
updateTitles();
notifyReadyChanged();
auto request = Api::ComposeWithAi::Request{
@@ -1495,17 +1506,20 @@ void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) {
auto display = (_mode == ComposeAiMode::Fix && result.diffText)
? BuildDiffDisplay(*result.diffText)
: _result;
_preview->setResult(std::move(display), true);
_ready = !_result.text.isEmpty();
_state = _result.text.isEmpty() ? CardState::Failed : CardState::Ready;
_preview->setState(_state);
if (_state == CardState::Ready) {
_preview->setResultText(std::move(display));
}
updateTitles();
notifyReadyChanged();
refreshLayout();
}
void ComposeAiContent::showError(const MTP::Error &error) {
_preview->setResult(
Ui::Text::Italic(tr::lng_ai_compose_error(tr::now)),
false);
_ready = false;
_state = CardState::Failed;
_preview->setState(CardState::Failed);
updateTitles();
notifyReadyChanged();
refreshLayout();
if (error.type() == u"AICOMPOSE_FLOOD_PREMIUM"_q) {
@@ -1533,7 +1547,7 @@ void ComposeAiContent::showError(const MTP::Error &error) {
void ComposeAiContent::notifyReadyChanged() {
if (_readyChanged) {
_readyChanged(_ready);
_readyChanged(_state == CardState::Ready);
}
}
@@ -1652,7 +1666,7 @@ void ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {
btn->setDisabled(true);
btn->setTextFgOverride(
anim::color(st::activeButtonBg, st::activeButtonFg, 0.5));
} else {
} else if (content->hasResult()) {
box->setStyle(st::aiComposeBoxWithSend);
const auto isStyle =
(content->mode() == ComposeAiMode::Style);
@@ -1662,7 +1676,6 @@ void ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {
: tr::lng_ai_compose_apply(),
applyAndClose);
btn->setFullRadius(true);
btn->setDisabled(!content->hasResult());
*applyButton = btn;
const auto send = Ui::CreateChild<Ui::SendButton>(
@@ -1670,9 +1683,6 @@ void ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {
st::aiComposeSendButton);
send->setState({ .type = Ui::SendButton::Type::Send });
send->show();
send->setAttribute(
Qt::WA_TransparentForMouseEvents,
!content->hasResult());
btn->geometryValue(
) | rpl::on_next([=](QRect geometry) {
const auto size = st::aiComposeBoxWithSend.buttonHeight;
@@ -1684,17 +1694,23 @@ void ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {
}, send->lifetime());
send->setClickedCallback(applyAndClose);
*sendButton = send;
} else {
box->setStyle(st::aiComposeBox);
const auto isStyle =
(content->mode() == ComposeAiMode::Style);
const auto btn = box->addButton(
isStyle
? tr::lng_ai_compose_apply_style()
: tr::lng_ai_compose_apply(),
nullptr);
btn->setFullRadius(true);
btn->setDisabled(true);
*applyButton = btn;
}
};
content->setReadyChangedCallback([=](bool ready) {
if (*applyButton) {
(*applyButton)->setDisabled(!ready);
}
if (*sendButton) {
(*sendButton)->setAttribute(
Qt::WA_TransparentForMouseEvents, !ready);
}
content->setReadyChangedCallback([=](bool) {
rebuildButtons();
});
content->setPremiumFloodCallback([=] {
*premiumFlooded = true;
@@ -0,0 +1,138 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/effects/skeleton_animation.h"
#include "ui/painter.h"
#include "ui/widgets/labels.h"
#include "styles/style_widgets.h"
namespace Ui {
namespace {
constexpr auto kSlideDuration = crl::time(1000);
constexpr auto kWaitDuration = crl::time(1000);
constexpr auto kFullDuration = kSlideDuration + kWaitDuration;
constexpr auto kBaseAlpha = 0.5;
constexpr auto kGradientAlpha = 0.2;
[[nodiscard]] QColor ColorWithAlpha(
const style::color &color,
float64 alpha) {
auto result = color->c;
result.setAlphaF(result.alphaF() * alpha);
return result;
}
} // namespace
SkeletonAnimation::SkeletonAnimation(not_null<FlatLabel*> label)
: _label(label)
, _animation([=] { _label->update(); }) {
base::install_event_filter(_label, _label, [=](not_null<QEvent*> e) {
return (_active && e->type() == QEvent::Paint)
? paint()
: base::EventFilterResult::Continue;
});
_label->sizeValue(
) | rpl::on_next([=](QSize) {
if (_active) {
recompute();
}
}, _lifetime);
}
void SkeletonAnimation::start() {
_active = true;
recompute();
_animation.start();
_label->update();
}
void SkeletonAnimation::stop() {
_active = false;
_animation.stop();
_label->update();
}
bool SkeletonAnimation::active() const {
return _active;
}
void SkeletonAnimation::recompute() {
_lineWidths = _label->countLineWidths();
}
base::EventFilterResult SkeletonAnimation::paint() {
if (_lineWidths.empty()) {
return base::EventFilterResult::Cancel;
}
auto p = QPainter(_label);
PainterHighQualityEnabler hq(p);
const auto &st = _label->st();
const auto lineHeight = st.style.lineHeight
? st.style.lineHeight
: st.style.font->height;
const auto thickness = st.style.font->height / 2;
const auto radius = thickness / 2.;
const auto left = st.margin.left();
const auto textWidth = _label->width()
- st.margin.left()
- st.margin.right();
const auto baseColor = ColorWithAlpha(
st::windowSubTextFg,
kBaseAlpha);
const auto now = crl::now();
const auto period = now % kFullDuration;
const auto gradientActive = (period < kSlideDuration);
p.setPen(Qt::NoPen);
if (gradientActive) {
const auto progress = period
/ float64(kSlideDuration);
const auto gradientWidth = textWidth;
const auto gradientStart = anim::interpolate(
left - gradientWidth,
left + textWidth,
progress);
auto gradient = QLinearGradient(
gradientStart,
0,
gradientStart + gradientWidth,
0);
const auto centerColor = ColorWithAlpha(
st::windowSubTextFg,
kGradientAlpha);
gradient.setStops({
{ 0., baseColor },
{ 0.5, centerColor },
{ 1., baseColor },
});
p.setBrush(gradient);
} else {
p.setBrush(baseColor);
}
auto lineTop = st.margin.top();
for (const auto lineWidth : _lineWidths) {
if (lineWidth <= 0) {
lineTop += lineHeight;
continue;
}
const auto rect = QRectF(
left,
lineTop + (lineHeight - thickness) / 2.,
lineWidth,
thickness);
p.drawRoundedRect(rect, radius, radius);
lineTop += lineHeight;
}
return base::EventFilterResult::Cancel;
}
} // namespace Ui
@@ -0,0 +1,37 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/event_filter.h"
#include "ui/effects/animations.h"
namespace Ui {
class FlatLabel;
class SkeletonAnimation final {
public:
explicit SkeletonAnimation(not_null<FlatLabel*> label);
void start();
void stop();
[[nodiscard]] bool active() const;
private:
void recompute();
base::EventFilterResult paint();
const not_null<FlatLabel*> _label;
Ui::Animations::Basic _animation;
std::vector<int> _lineWidths;
bool _active = false;
rpl::lifetime _lifetime;
};
} // namespace Ui
+2
View File
@@ -481,6 +481,8 @@ PRIVATE
ui/effects/round_checkbox.h
ui/effects/scroll_content_shadow.cpp
ui/effects/scroll_content_shadow.h
ui/effects/skeleton_animation.cpp
ui/effects/skeleton_animation.h
ui/effects/shake_animation.cpp
ui/effects/shake_animation.h
ui/effects/snowflakes.cpp