/* 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 "boxes/compose_ai_box.h" #include "api/api_compose_with_ai.h" #include "apiwrap.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/stickers_lottie.h" #include "core/application.h" #include "core/click_handler_types.h" #include "core/core_settings.h" #include "core/ui_integration.h" #include "data/data_document.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" #include "main/main_app_config.h" #include "main/main_session.h" #include "settings/sections/settings_premium.h" #include "spellcheck/platform/platform_language.h" #include "ui/boxes/choose_language_box.h" #include "ui/chat/chat_style.h" #include "ui/controls/labeled_emoji_tabs.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/ui_utility.h" #include "ui/text/custom_emoji_helper.h" #include "ui/text/custom_emoji_text_badge.h" #include "ui/text/text_extended_data.h" #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" #include "ui/widgets/tooltip.h" #include "styles/style_basic.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" #include "styles/style_widgets.h" #include #include namespace HistoryView::Controls { namespace { constexpr auto kAiComposeStyleTooltipHiddenPref = "ai_compose_style_tooltip_hidden"_cs; enum class ComposeAiMode { Translate, Style, 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]] TextWithEntities HighlightDiff(TextWithEntities text) { return Ui::Text::Colorized( Ui::Text::Wrapped(std::move(text), EntityType::Underline), 1); } [[nodiscard]] TextWithEntities StrikeOutDiff(TextWithEntities text) { return Ui::Text::Colorized( Ui::Text::Wrapped(std::move(text), EntityType::StrikeOut), 2); } [[nodiscard]] TextWithEntities BuildDiffDisplay( const Api::ComposeWithAi::Diff &diff) { auto result = TextWithEntities(); auto entities = diff.entities; std::stable_sort( entities.begin(), entities.end(), [](const auto &a, const auto &b) { return a.offset < b.offset; }); const auto size = int(diff.text.text.size()); auto taken = 0; for (const auto &entity : entities) { const auto offset = std::clamp(entity.offset, 0, size); const auto length = std::clamp(entity.length, 0, size - offset); if (offset > taken) { result.append(Ui::Text::Mid(diff.text, taken, offset - taken)); } auto part = Ui::Text::Mid(diff.text, offset, length); switch (entity.type) { case Api::ComposeWithAi::DiffEntity::Type::Insert: result.append(HighlightDiff(std::move(part))); break; case Api::ComposeWithAi::DiffEntity::Type::Replace: if (!entity.oldText.isEmpty()) { result.append( StrikeOutDiff( TextWithEntities::Simple(entity.oldText))); } result.append(HighlightDiff(std::move(part))); break; case Api::ComposeWithAi::DiffEntity::Type::Delete: result.append(StrikeOutDiff(std::move(part))); break; } taken = std::max(taken, offset + length); } if (taken < size) { result.append(Ui::Text::Mid(diff.text, taken)); } return result; } [[nodiscard]] QString FromTitle(LanguageId id) { return tr::lng_ai_compose_original(tr::now); } [[nodiscard]] TextWithEntities ToTitle( LanguageId id, const QString &style) { const auto name = style.isEmpty() ? tr::link(Ui::LanguageName(id)) : tr::link(tr::lng_ai_compose_name_style( tr::now, lt_name, tr::marked(Ui::LanguageName(id)), lt_style, tr::marked(style), tr::marked)); return tr::lng_ai_compose_to_language( tr::now, lt_language, name, tr::marked); } [[nodiscard]] LanguageId DefaultAiTranslateTo(LanguageId offeredFrom) { const auto current = LanguageId{ QLocale(Lang::LanguageIdOrDefault(Lang::Id())).language() }; if (current && (current != offeredFrom)) { return current; } const auto english = LanguageId{ QLocale::English }; if (english != offeredFrom) { return english; } return LanguageId{ QLocale::Spanish }; } [[nodiscard]] const style::icon &ModeIcon( ComposeAiMode mode, bool active) { switch (mode) { case ComposeAiMode::Translate: return active ? st::aiComposeTabTranslateIconActive : st::aiComposeTabTranslateIcon; case ComposeAiMode::Style: return active ? st::aiComposeTabStyleIconActive : st::aiComposeTabStyleIcon; case ComposeAiMode::Fix: return active ? st::aiComposeTabFixIconActive : st::aiComposeTabFixIcon; } return active ? st::aiComposeTabTranslateIconActive : st::aiComposeTabTranslateIcon; } [[nodiscard]] qreal ComposeAiPillRadius(int height) { return height / 2.; } [[nodiscard]] QColor ComposeAiActiveBackgroundColor( const style::color &color) { return ComposeAiColorWithAlpha( color, st::aiComposeButtonBgActiveOpacity); } [[nodiscard]] QColor ComposeAiRippleColor( const style::RippleAnimation &ripple, float64 opacity) { return ComposeAiColorWithAlpha( ripple.color, opacity); } [[nodiscard]] Ui::LabeledEmojiTab ResolveStyleDescriptor( const Main::AppConfig::AiComposeStyle &style) { return { .id = style.type, .label = style.title, .customEmojiData = Data::SerializeCustomEmojiId(style.emojiId), }; } [[nodiscard]] std::vector ResolveStyleDescriptors( const std::vector &styles) { auto result = std::vector(); result.reserve(styles.size()); for (const auto &style : styles) { result.push_back(ResolveStyleDescriptor(style)); } return result; } [[nodiscard]] std::vector ResolveTranslateStyleDescriptors( not_null session, const std::vector &styles) { const auto neutral = ChatHelpers::GenerateLocalTgsSticker( session, u"chat/white_flag_emoji"_q); auto result = std::vector(); result.reserve(styles.size() + 1); result.push_back({ .id = QString(), .label = tr::lng_ai_compose_style_neutral(tr::now), .customEmojiData = Data::SerializeCustomEmojiId(neutral->id), }); result.insert(end(result), begin(styles), end(styles)); return result; } [[nodiscard]] TextWithEntities LoadingTitleSparkle( not_null session) { const auto sparkles = ChatHelpers::GenerateLocalTgsSticker( session, u"chat/sparkles_emoji"_q); return tr::marked(u" "_q) .append(Data::SingleCustomEmoji(sparkles->id)); } class ComposeAiModeButton final : public Ui::RippleButton { public: ComposeAiModeButton( QWidget *parent, ComposeAiMode mode, QString label); void setSelected(bool selected); [[nodiscard]] ComposeAiMode mode() const; protected: void paintEvent(QPaintEvent *e) override; [[nodiscard]] QImage prepareRippleMask() const override; private: const ComposeAiMode _mode; const QString _label; bool _selected = false; }; class ComposeAiModeTabs final : public Ui::RpWidget { public: ComposeAiModeTabs(QWidget *parent); void setActive(ComposeAiMode mode); void setChangedCallback(Fn callback); protected: int resizeGetHeight(int newWidth) override; void paintEvent(QPaintEvent *e) override; private: const not_null _translate; const not_null _style; const not_null _fix; Fn _changed; ComposeAiMode _active = ComposeAiMode::Style; }; class ComposeAiPreviewCard final : public Ui::RpWidget { public: ComposeAiPreviewCard( QWidget *parent, not_null session, TextWithEntities original, std::shared_ptr chatStyle); void setResizeCallback(Fn callback); void setChooseCallback(Fn callback); void setCopyCallback(Fn callback); void setEmojifyChangedCallback(Fn callback); void setOriginalTitle(const QString &title); void setOriginalVisible(bool visible); void setResultTitle(const TextWithEntities &title); void setEmojifyVisible(bool visible); void setEmojifyChecked(bool checked); void setState(CardState state); void setResultText(TextWithEntities text); void setShow(std::shared_ptr show); protected: int resizeGetHeight(int newWidth) override; void paintEvent(QPaintEvent *e) override; private: void refreshGeometry(); void updateOriginalToggleIcon(); const Ui::Text::MarkedContext _context; const TextWithEntities _original; const not_null _originalTitle; const not_null _originalBody; const not_null _originalToggle; const not_null _resultTitle; const not_null _resultBody; const not_null _copy; const not_null _emojify; Fn _resized; Fn _chooseCallback; Fn _copyCallback; Fn _emojifyChanged; bool _ignoreResizedCallback = false; bool _originalExpanded = false; bool _originalVisible = true; bool _emojifyVisible = false; bool _dividerVisible = false; int _dividerTop = 0; CardState _state = CardState::Waiting; Ui::SkeletonAnimation _skeleton; std::array _diffColors; }; class ComposeAiContent final : public Ui::RpWidget { public: ComposeAiContent( QWidget *parent, not_null box, ComposeAiBoxArgs args); ~ComposeAiContent(); [[nodiscard]] bool hasResult() const; [[nodiscard]] const TextWithEntities &result() const; [[nodiscard]] const std::vector &stylesData() const; void setReadyChangedCallback(Fn callback); void setLoadingChangedCallback(Fn callback); void setPremiumFloodCallback(Fn callback); void setModeChangedCallback(Fn callback); void setStyleSelectedCallback(Fn callback); [[nodiscard]] ComposeAiMode mode() const; [[nodiscard]] bool hasStyleSelection() const; void setModeTabs(not_null tabs); void setStyleTabs(not_null*> stylesWrap); void start(); protected: int resizeGetHeight(int newWidth) override; private: void refreshLayout(); void chooseLanguage(); void copyResult(); void setMode(ComposeAiMode mode); void updateTitles(); void updatePinnedTabs(anim::type animated); void cancelRequest(); void request(); void applyResult(Api::ComposeWithAi::Result &&result); void showError(const QString &error = {}); void notifyLoadingChanged(); void notifyReadyChanged(); [[nodiscard]] QString currentTranslateStyle() const; [[nodiscard]] QString currentTranslateStyleLabel() const; const not_null _box; const not_null _session; const TextWithEntities _original; const LanguageId _detectedFrom; LanguageId _to; const std::vector _stylesData; const std::vector _translateStylesData; QPointer _tabs; QPointer _styles; QPointer> _stylesWrap; const not_null _preview; Fn _readyChanged; Fn _loadingChanged; Fn _premiumFlood; Fn _modeChanged; Fn _styleSelected; ComposeAiMode _mode = ComposeAiMode::Style; int _styleIndex = -1; int _translateStyleIndex = 0; bool _emojify = false; CardState _state = CardState::Waiting; mtpRequestId _requestId = 0; int _requestToken = 0; TextWithEntities _result; }; // ComposeAiModeButton ComposeAiModeButton::ComposeAiModeButton( QWidget *parent, ComposeAiMode mode, QString label) : RippleButton(parent, st::aiComposeButtonRippleInactive) , _mode(mode) , _label(std::move(label)) { setCursor(style::cur_pointer); } void ComposeAiModeButton::setSelected(bool selected) { if (_selected == selected) { return; } _selected = selected; update(); } ComposeAiMode ComposeAiModeButton::mode() const { return _mode; } void ComposeAiModeButton::paintEvent(QPaintEvent *e) { Painter p(this); PainterHighQualityEnabler hq(p); const auto radius = ComposeAiPillRadius(height()); if (_selected) { p.setPen(Qt::NoPen); p.setBrush(ComposeAiActiveBackgroundColor( st::aiComposeTabButtonBgActive)); p.drawRoundedRect( rect(), radius, radius); } const auto ripple = ComposeAiRippleColor( _selected ? st::aiComposeButtonRippleActive : st::aiComposeButtonRippleInactive, _selected ? st::aiComposeButtonRippleActiveOpacity : st::aiComposeButtonRippleInactiveOpacity); paintRipple(p, 0, 0, &ripple); const auto &icon = ModeIcon(_mode, _selected); const auto iconLeft = (width() - icon.width()) / 2; icon.paint(p, iconLeft, st::aiComposeTabIconTop, width()); p.setPen(_selected ? st::aiComposeTabLabelFgActive : st::aiComposeTabLabelFg); p.setFont(st::aiComposeTabLabelFont); p.drawText( QRect( 0, st::aiComposeTabLabelTop, width(), height() - st::aiComposeTabLabelTop), Qt::AlignHCenter | Qt::AlignTop, _label); } QImage ComposeAiModeButton::prepareRippleMask() const { return Ui::RippleAnimation::MaskByDrawer(size(), false, [&](QPainter &p) { p.setPen(Qt::NoPen); p.setBrush(Qt::white); const auto radius = ComposeAiPillRadius(height()); p.drawRoundedRect( rect(), radius, radius); }); } // ComposeAiModeTabs ComposeAiModeTabs::ComposeAiModeTabs(QWidget *parent) : RpWidget(parent) , _translate(Ui::CreateChild( this, ComposeAiMode::Translate, tr::lng_ai_compose_tab_translate(tr::now))) , _style(Ui::CreateChild( this, ComposeAiMode::Style, tr::lng_ai_compose_tab_style(tr::now))) , _fix(Ui::CreateChild( this, ComposeAiMode::Fix, tr::lng_ai_compose_tab_fix(tr::now))) { const auto bind = [=](not_null button) { button->setClickedCallback([=] { setActive(button->mode()); if (_changed) { _changed(button->mode()); } }); }; bind(_translate); bind(_style); bind(_fix); setActive(ComposeAiMode::Style); } void ComposeAiModeTabs::setActive(ComposeAiMode mode) { _active = mode; _translate->setSelected(mode == ComposeAiMode::Translate); _style->setSelected(mode == ComposeAiMode::Style); _fix->setSelected(mode == ComposeAiMode::Fix); } void ComposeAiModeTabs::setChangedCallback(Fn callback) { _changed = std::move(callback); } int ComposeAiModeTabs::resizeGetHeight(int newWidth) { const auto padding = st::aiComposeTabsPadding; const auto skip = st::aiComposeTabsSkip; const auto innerWidth = newWidth - padding.left() - padding.right(); const auto buttonWidth = (innerWidth - (2 * skip)) / 3; const auto buttonHeight = st::aiComposeTabsHeight - padding.top() - padding.bottom(); const auto top = padding.top(); auto left = padding.left(); for (const auto &button : { _translate, _style, _fix }) { button->setGeometry(left, top, buttonWidth, buttonHeight); left += buttonWidth + skip; } return st::aiComposeTabsHeight; } void ComposeAiModeTabs::paintEvent(QPaintEvent *e) { Painter p(this); PainterHighQualityEnabler hq(p); p.setPen(Qt::NoPen); p.setBrush(st::aiComposeTabsBg); const auto radius = st::aiComposeTabsRadius; p.drawRoundedRect( rect(), radius, radius); } // ComposeAiPreviewCard ComposeAiPreviewCard::ComposeAiPreviewCard( QWidget *parent, not_null session, TextWithEntities original, std::shared_ptr chatStyle) : RpWidget(parent) , _context(Core::TextContext({ .session = session })) , _original(std::move(original)) , _originalTitle(Ui::CreateChild( this, st::aiComposeCardTitle)) , _originalBody(Ui::CreateChild( this, st::aiComposeBodyLabel)) , _originalToggle(Ui::CreateChild( this, st::aiComposeExpandButton)) , _resultTitle(Ui::CreateChild( this, st::aiComposeCardTitle)) , _resultBody(Ui::CreateChild( this, st::aiComposeBodyLabel)) , _copy(Ui::CreateChild( this, st::aiComposeCopyButton)) , _emojify( Ui::CreateChild( this, tr::lng_ai_compose_emojify(tr::now), st::aiComposeEmojifyCheckbox, std::make_unique(st::defaultCheck,false))) , _skeleton(_resultBody) { _originalBody->setSelectable(true); _originalBody->setMarkedText(_original, _context); _resultTitle->setClickHandlerFilter([=](const auto &...) { if (_chooseCallback) { _chooseCallback(); } return false; }); _resultBody->setSelectable(true); const auto watchHeight = [=](not_null label) { label->heightValue( ) | rpl::skip(1) | rpl::on_next([=] { if (_resized && !_ignoreResizedCallback) { _resized(); } }, lifetime()); }; watchHeight(_originalBody); watchHeight(_resultBody); _diffColors[0] = { &st::boxTextFgGood->p, &st::boxTextFgGood->p }; _diffColors[1] = { &st::attentionButtonFg->p, &st::attentionButtonFg->p }; _resultBody->setColors(_diffColors); _originalToggle->setClickedCallback([=] { _originalExpanded = !_originalExpanded; updateOriginalToggleIcon(); if (_resized) { _resized(); } }); _copy->setClickedCallback([=] { if (_copyCallback) { _copyCallback(); } }); _emojify->checkedChanges( ) | rpl::on_next([=](bool checked) { if (_emojifyChanged) { _emojifyChanged(checked); } }, _emojify->lifetime()); setOriginalTitle(tr::lng_ai_compose_original(tr::now)); setResultTitle(tr::lng_ai_compose_result(tr::now, tr::marked)); _resultBody->setMarkedText(_original, _context); _copy->setVisible(false); updateOriginalToggleIcon(); if (chatStyle) { const auto style = chatStyle; const auto s = session.get(); const auto setupCaches = [=](not_null label) { label->setPreCache([=] { return style->messageStyle(false, false).preCache.get(); }); label->setBlockquoteCache([=] { return style->coloredQuoteCache( false, s->user()->colorIndex()); }); }; setupCaches(_originalBody); setupCaches(_resultBody); } } void ComposeAiPreviewCard::setResizeCallback(Fn callback) { _resized = std::move(callback); } void ComposeAiPreviewCard::setChooseCallback(Fn callback) { _chooseCallback = std::move(callback); } void ComposeAiPreviewCard::setCopyCallback(Fn callback) { _copyCallback = std::move(callback); } void ComposeAiPreviewCard::setEmojifyChangedCallback(Fn callback) { _emojifyChanged = std::move(callback); } void ComposeAiPreviewCard::setOriginalTitle(const QString &title) { _originalTitle->setText(title); refreshGeometry(); } void ComposeAiPreviewCard::setOriginalVisible(bool visible) { if (_originalVisible == visible) { return; } _originalVisible = visible; _originalTitle->setVisible(visible); _originalBody->setVisible(visible); _originalToggle->setVisible(false); refreshGeometry(); } void ComposeAiPreviewCard::setResultTitle(const TextWithEntities &title) { _resultTitle->setMarkedText(title); refreshGeometry(); } void ComposeAiPreviewCard::setEmojifyVisible(bool visible) { _emojifyVisible = visible; _emojify->setVisible(visible); refreshGeometry(); } void ComposeAiPreviewCard::setEmojifyChecked(bool checked) { _emojify->setChecked(checked, Ui::Checkbox::NotifyAboutChange::DontNotify); refreshGeometry(); } void ComposeAiPreviewCard::setState(CardState state) { if (_state == state) { return; } 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::setResultText(TextWithEntities text) { _resultBody->setMarkedText(std::move(text), _context); refreshGeometry(); } void ComposeAiPreviewCard::setShow(std::shared_ptr show) { const auto setupFilter = [&](not_null label) { label->setClickHandlerFilter([=]( const ClickHandlerPtr &handler, Qt::MouseButton button) { if (dynamic_cast(handler.get())) { ActivateClickHandler(label, handler, ClickContext{ .button = button, .other = QVariant::fromValue(ClickHandlerContext{ .show = show, }) }); return false; } return true; }); }; setupFilter(_originalBody); setupFilter(_resultBody); } int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { const auto padding = st::aiComposeCardPadding; const auto contentWidth = newWidth - padding.left() - padding.right(); auto y = padding.top(); _dividerVisible = false; if (_originalVisible) { _originalTitle->show(); _originalBody->show(); _originalTitle->resizeToWidth(contentWidth); _originalToggle->setVisible(false); const auto toggleTop = y + (_originalTitle->height() - _originalToggle->height()) / 2; _originalToggle->moveToRight(padding.right(), toggleTop, newWidth); const auto originalTitleWidth = contentWidth - _originalToggle->width() - st::aiComposeCardControlSkip; _originalTitle->setGeometryToLeft( padding.left(), y, std::max(originalTitleWidth, 0), _originalTitle->height(), newWidth); y = std::max( y + _originalTitle->height(), toggleTop + _originalToggle->height()); _ignoreResizedCallback = true; const auto wasOriginalSize = _originalBody->size(); _originalBody->resizeToWidth(contentWidth); const auto fullOriginalHeight = _originalBody->height(); _originalBody->resize(wasOriginalSize); _ignoreResizedCallback = false; const auto lineHeight = _originalBody->st().style.lineHeight; const auto originalHeight = _originalExpanded ? fullOriginalHeight : std::min(fullOriginalHeight, lineHeight); _originalBody->setGeometryToLeft( padding.left(), y, contentWidth, originalHeight, newWidth); const auto expandable = fullOriginalHeight > lineHeight; _originalToggle->setVisible(expandable); y += originalHeight + st::aiComposeCardSectionSkip; _dividerTop = y; _dividerVisible = true; y += st::lineWidth + st::aiComposeCardSectionSkip; } else { _originalTitle->hide(); _originalBody->hide(); _originalToggle->hide(); } _resultTitle->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( y + _resultTitle->height(), (_emojifyVisible ? (y - _emojify->getMargins().top() + _emojify->height()) : 0)); const auto lineHeight = _resultBody->st().style.lineHeight ? _resultBody->st().style.lineHeight : _resultBody->st().style.font->height; if (!_copy->isHidden()) { _resultBody->setSkipBlock( _copy->width(), lineHeight); } else { _resultBody->setSkipBlock(0, 0); } _resultBody->resizeToWidth(contentWidth); _resultBody->setGeometryToLeft( padding.left(), y, contentWidth, _resultBody->height(), newWidth); if (!_copy->isHidden()) { _copy->moveToRight( padding.right(), y + _resultBody->height() - lineHeight, newWidth); } y += _resultBody->height(); return y + padding.bottom(); } void ComposeAiPreviewCard::paintEvent(QPaintEvent *e) { Painter p(this); PainterHighQualityEnabler hq(p); p.setPen(Qt::NoPen); p.setBrush(st::aiComposeCardBg); p.drawRoundedRect( rect(), st::aiComposeCardRadius, st::aiComposeCardRadius); if (_dividerVisible) { p.setBrush(Qt::NoBrush); p.setPen(st::aiComposeCardDivider); p.drawLine( st::aiComposeCardPadding.left(), _dividerTop, width() - st::aiComposeCardPadding.right(), _dividerTop); } } void ComposeAiPreviewCard::refreshGeometry() { if (width() > 0) { resizeToWidth(width()); } if (_resized) { _resized(); } } void ComposeAiPreviewCard::updateOriginalToggleIcon() { _originalToggle->setIconOverride( _originalExpanded ? &st::aiComposeCollapseIcon : nullptr, _originalExpanded ? &st::aiComposeCollapseIcon : nullptr); } // ComposeAiContent ComposeAiContent::ComposeAiContent( QWidget *parent, not_null box, ComposeAiBoxArgs args) : RpWidget(parent) , _box(box) , _session(args.session) , _original(std::move(args.text)) , _detectedFrom(Platform::Language::Recognize(_original.text)) , _to(DefaultAiTranslateTo(_detectedFrom)) , _stylesData(ResolveStyleDescriptors( _session->appConfig().aiComposeStyles())) , _translateStylesData(ResolveTranslateStyleDescriptors(_session, _stylesData)) , _preview( Ui::CreateChild( this, _session, _original, args.chatStyle)) { _preview->setResizeCallback([=] { refreshLayout(); }); _preview->setChooseCallback([=] { chooseLanguage(); }); _preview->setCopyCallback([=] { copyResult(); }); _preview->setEmojifyChangedCallback([=](bool checked) { _emojify = checked; if (_mode != ComposeAiMode::Fix) { request(); } }); _preview->setShow(_box->uiShow()); } ComposeAiContent::~ComposeAiContent() { cancelRequest(); } bool ComposeAiContent::hasResult() const { return _state == CardState::Ready; } const TextWithEntities &ComposeAiContent::result() const { return _result; } const std::vector &ComposeAiContent::stylesData() const { return _stylesData; } void ComposeAiContent::setReadyChangedCallback(Fn callback) { _readyChanged = std::move(callback); } void ComposeAiContent::setLoadingChangedCallback(Fn callback) { _loadingChanged = std::move(callback); notifyLoadingChanged(); } void ComposeAiContent::setModeTabs(not_null tabs) { _tabs = tabs; _tabs->setChangedCallback([=](ComposeAiMode mode) { setMode(mode); }); _tabs->setActive(_mode); } void ComposeAiContent::setStyleTabs( not_null*> stylesWrap) { _stylesWrap = stylesWrap; _stylesWrap->setDuration(0); _styles = stylesWrap->entity(); _styles->setChangedCallback([=](int index) { if (index >= 0 && index < int(_stylesData.size())) { const auto wasNoSelection = (_styleIndex < 0); _styleIndex = index; updateTitles(); if (_mode == ComposeAiMode::Style) { request(); if (wasNoSelection && _styleSelected) { _styleSelected(); } } } }); _styles->setActive(_styleIndex); _stylesWrap->toggle(_mode == ComposeAiMode::Style, anim::type::instant); } void ComposeAiContent::start() { updatePinnedTabs(anim::type::instant); updateTitles(); request(); } int ComposeAiContent::resizeGetHeight(int newWidth) { _preview->resizeToWidth(newWidth); _preview->moveToLeft(0, 0, newWidth); return _preview->height(); } void ComposeAiContent::refreshLayout() { if (width() > 0) { resizeToWidth(width()); } } void ComposeAiContent::chooseLanguage() { if (_mode != ComposeAiMode::Translate) { return; } const auto weak = QPointer(this); const auto session = _session; const auto styles = _translateStylesData; const auto selectedStyle = std::make_shared(_translateStyleIndex); _box->uiShow()->showBox(Box([=](not_null box) { const auto apply = [=](LanguageId id, int styleIndex) { if (!weak) { return; } weak->_to = id; if (const auto count = int(weak->_translateStylesData.size())) { weak->_translateStyleIndex = std::clamp(styleIndex, 0, count - 1); } weak->updateTitles(); weak->request(); }; Ui::ChooseLanguageBox( box, tr::lng_languages(), [=](std::vector ids) { if (ids.empty()) { return; } apply(ids.front(), *selectedStyle); }, { _to }, false, nullptr); const auto bottom = box->setPinnedToBottomContent( object_ptr(box)); const auto skip = st::defaultSubsectionTitlePadding.left(); const auto tabs = bottom->add( object_ptr( bottom, styles, session->data().customEmojiManager().factory( Data::CustomEmojiSizeTag::Large)), QMargins( (skip - st::aiComposeStyleTabsPadding.left()), 0, (skip - st::aiComposeStyleTabsPadding.right()), 0)); tabs->setPaintOuterCorners(false); tabs->setChangedCallback([=](int index) { if (index >= 0 && index < int(styles.size())) { *selectedStyle = index; apply(_to, index); box->closeBox(); } }); tabs->setActive(std::clamp(*selectedStyle, 0, int(styles.size()) - 1)); tabs->scrollToActive(); })); } void ComposeAiContent::copyResult() { if (_state != CardState::Ready) { return; } TextUtilities::SetClipboardText( TextForMimeData::WithExpandedLinks(_result)); } void ComposeAiContent::setMode(ComposeAiMode mode) { if (_mode == mode) { return; } if (mode != ComposeAiMode::Style) { _styleIndex = -1; } _mode = mode; _state = CardState::Waiting; _preview->setState(CardState::Waiting); notifyLoadingChanged(); if (_modeChanged) { _modeChanged(_mode); } updatePinnedTabs(anim::type::normal); updateTitles(); refreshLayout(); request(); } void ComposeAiContent::updateTitles() { 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( hasResult ? ((_mode == ComposeAiMode::Translate) ? ToTitle(_to, currentTranslateStyleLabel()) : 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); } void ComposeAiContent::updatePinnedTabs(anim::type animated) { if (_tabs) { _tabs->setActive(_mode); } if (_styles) { _styles->setActive(_styleIndex); } if (_stylesWrap) { _stylesWrap->toggle(_mode == ComposeAiMode::Style, animated); } } void ComposeAiContent::cancelRequest() { ++_requestToken; if (_requestId) { _session->api().composeWithAi().cancel(_requestId); _requestId = 0; } } void ComposeAiContent::request() { cancelRequest(); if (_mode == ComposeAiMode::Style && _styleIndex < 0) { return; } _state = CardState::Loading; _result = {}; _preview->setState(CardState::Loading); notifyLoadingChanged(); updateTitles(); notifyReadyChanged(); auto request = Api::ComposeWithAi::Request{ .text = _original, .emojify = (_mode != ComposeAiMode::Fix) && _emojify, }; switch (_mode) { case ComposeAiMode::Translate: request.translateToLang = _to.twoLetterCode(); request.changeTone = currentTranslateStyle(); break; case ComposeAiMode::Style: request.changeTone = _stylesData[_styleIndex].id; break; case ComposeAiMode::Fix: request.proofread = true; break; } const auto token = ++_requestToken; const auto weak = QPointer(this); _requestId = _session->api().composeWithAi().request( std::move(request), [=](Api::ComposeWithAi::Result &&result) { if (!weak || weak->_requestToken != token) { return; } weak->_requestId = 0; weak->applyResult(std::move(result)); }, [=](const MTP::Error &error) { if (!weak || weak->_requestToken != token) { return; } weak->_requestId = 0; weak->showError(error.type()); }); } void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) { _result = std::move(result.resultText); if (_result.text.isEmpty()) { showError({}); return; } auto display = (_mode == ComposeAiMode::Fix && result.diffText) ? BuildDiffDisplay(*result.diffText) : _result; _state = _result.text.isEmpty() ? CardState::Failed : CardState::Ready; _preview->setState(_state); notifyLoadingChanged(); if (_state == CardState::Ready) { _preview->setResultText(std::move(display)); } updateTitles(); notifyReadyChanged(); refreshLayout(); } void ComposeAiContent::showError(const QString &error) { _state = CardState::Failed; _preview->setState(CardState::Failed); notifyLoadingChanged(); updateTitles(); notifyReadyChanged(); refreshLayout(); if (error == u"AICOMPOSE_FLOOD_PREMIUM"_q) { const auto show = Main::MakeSessionShow( _box->uiShow(), _session); Settings::ShowPremiumPromoToast( show, ChatHelpers::ResolveWindowDefault(), tr::lng_ai_compose_flood_text( tr::now, lt_link, tr::link(tr::lng_ai_compose_flood_link(tr::now, tr::bold)), tr::rich), u"ai_compose"_q); if (_premiumFlood) { _premiumFlood(); } return; } _box->showToast(error.isEmpty() ? tr::lng_ai_compose_error(tr::now) : error); } void ComposeAiContent::notifyLoadingChanged() { if (_loadingChanged) { _loadingChanged(_state == CardState::Loading); } } void ComposeAiContent::notifyReadyChanged() { if (_readyChanged) { _readyChanged(_state == CardState::Ready); } } void ComposeAiContent::setPremiumFloodCallback(Fn callback) { _premiumFlood = std::move(callback); } void ComposeAiContent::setModeChangedCallback( Fn callback) { _modeChanged = std::move(callback); } void ComposeAiContent::setStyleSelectedCallback(Fn callback) { _styleSelected = std::move(callback); } QString ComposeAiContent::currentTranslateStyle() const { return (_translateStyleIndex >= 0 && _translateStyleIndex < int(_translateStylesData.size())) ? _translateStylesData[_translateStyleIndex].id : QString(); } QString ComposeAiContent::currentTranslateStyleLabel() const { if (const auto style = currentTranslateStyle(); !style.isEmpty()) { return (_translateStyleIndex >= 0 && _translateStyleIndex < int(_translateStylesData.size())) ? _translateStylesData[_translateStyleIndex].label : QString(); } return QString(); } ComposeAiMode ComposeAiContent::mode() const { return _mode; } bool ComposeAiContent::hasStyleSelection() const { return _styleIndex >= 0; } [[nodiscard]] Fn SetupStyleTooltip( not_null box, not_null pinnedToTop, not_null stylesWrap, Fn currentMode) { const auto tooltip = Ui::CreateChild( box, object_ptr>( box, Ui::MakeNiceTooltipLabel( box, tr::lng_ai_compose_style_tooltip(tr::rich), st::historyMessagesTTLLabel.minWidth, st::ttlMediaImportantTooltipLabel), st::historyRecordTooltip.padding), st::historyRecordTooltip); tooltip->toggleFast(false); struct State { bool shown = false; bool shownOnce = false; }; const auto state = box->lifetime().make_state(); const auto updateGeometry = [=] { const auto local = stylesWrap->geometry(); if (local.isEmpty()) { return; } const auto geometry = Ui::MapFrom(box, pinnedToTop, local); const auto countPosition = [=](QSize size) { const auto left = geometry.x() + (geometry.width() - size.width()) / 2; return QPoint( std::max(std::min(left, box->width() - size.width()), 0), (geometry.y() + geometry.height() - st::historyRecordTooltip.arrow - (st::aiComposeBoxStyleTabsSkip / 2))); }; tooltip->pointAt(geometry, RectPart::Bottom, countPosition); }; const auto updateVisibility = [=](bool visible) { const auto show = visible && !Core::App().settings().readPref( kAiComposeStyleTooltipHiddenPref); if (state->shown != show) { state->shown = show; if (show) { updateGeometry(); tooltip->raise(); } if (show && !state->shownOnce) { state->shownOnce = true; tooltip->toggleFast(true); } else { tooltip->toggleAnimated(show); } } }; stylesWrap->geometryValue( ) | rpl::on_next([=](const QRect &geometry) { if (!geometry.isEmpty()) { if (state->shown) { updateGeometry(); } else { updateVisibility(currentMode() == ComposeAiMode::Style); } } }, tooltip->lifetime()); return updateVisibility; } } // namespace void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { const auto sendButtonHeight = st::aiComposeSendButton.inner.height; const auto buttonHeight = st::aiComposeSendButton.inner.icon.height() + 2 * st::aiComposeSendButton.sendIconFillPadding; const auto boxStyle = [&](const style::Box &base) { const auto result = box->lifetime().make_state(base); result->button.height = buttonHeight; result->buttonHeight = buttonHeight; result->button.textTop = base.button.textTop - (base.button.height - buttonHeight) / 2; return result; }; const auto boxStyleNoSend = boxStyle(st::aiComposeBox); const auto boxStyleWithSend = boxStyle(st::aiComposeBoxWithSend); box->setStyle(*boxStyleNoSend); box->setNoContentMargin(true); box->setWidth(st::boxWideWidth); const auto session = args.session; box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); const auto body = box->verticalLayout(); const auto tabsSkip = QMargins(0, 0, 0, st::aiComposeBoxStyleTabsSkip); const auto pinnedToTop = box->setPinnedToTopContent( object_ptr(box)); const auto tabs = pinnedToTop->add( object_ptr(pinnedToTop), st::aiComposeContentMargin + tabsSkip); const auto content = body->add( object_ptr(box, box, args), st::aiComposeContentMargin); auto emojiFactory = session->data().customEmojiManager().factory( Data::CustomEmojiSizeTag::Large); const auto stylesWrap = pinnedToTop->add( object_ptr>( pinnedToTop, object_ptr( pinnedToTop, content->stylesData(), std::move(emojiFactory)), tabsSkip), st::aiComposeContentMargin); stylesWrap->hide(anim::type::instant); content->setModeTabs(tabs); content->setStyleTabs(stylesWrap); const auto updateStyleTooltipVisibility = SetupStyleTooltip( box, pinnedToTop, stylesWrap, [=] { return content->mode(); }); const auto sparkle = LoadingTitleSparkle(session); const auto loading = box->lifetime().make_state< rpl::variable>(); content->setLoadingChangedCallback([=](bool value) { *loading = value; }); box->setTitle(rpl::combine( loading->value(), tr::lng_ai_compose_title(tr::marked) ) | rpl::map([=](bool loading, TextWithEntities title) { return loading ? title.append(sparkle) : title; }), Core::TextContext({ .session = session })); auto premiumFlooded = std::make_shared(false); auto sendButton = std::make_shared>(); const auto applyAndClose = [=] { if (!content->hasResult()) { return; } args.apply(TextWithEntities(content->result())); box->closeBox(); }; const auto sendResult = [=](Api::SendOptions options) { if (!args.send || !content->hasResult()) { return; } args.send( TextWithEntities(content->result()), options, crl::guard(box, [=] { box->closeBox(); })); }; const auto addApplyButton = [=]( const style::Box &style, rpl::producer text, Fn callback) { box->setStyle(style); const auto result = box->addButton(std::move(text), std::move(callback)); result->setFullRadius(true); return result; }; const auto disableButton = [=](not_null button) { button->clearState(); button->setDisabled(true); button->setAttribute(Qt::WA_TransparentForMouseEvents); button->setTextFgOverride( anim::color(st::activeButtonBg, st::activeButtonFg, 0.5)); button->setClickedCallback([] { }); }; const auto rebuildButtons = [=] { if (*sendButton) { delete sendButton->data(); } *sendButton = nullptr; box->clearButtons(); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); if (*premiumFlooded) { auto helper = Ui::Text::CustomEmojiHelper(); const auto badge = helper.paletteDependent( Ui::Text::CustomEmojiTextBadge( u"x50"_q, st::aiComposeBadge, st::aiComposeBadgeMargin)); const auto btn = addApplyButton( *boxStyleNoSend, tr::lng_ai_compose_increase_limit(), nullptr); btn->setContext(helper.context()); btn->setText(rpl::single( tr::lng_ai_compose_increase_limit(tr::now, tr::marked) .append(' ') .append(badge))); const auto resolve = ChatHelpers::ResolveWindowDefault(); const auto close = crl::guard(box, [=] { box->closeBox(); }); btn->setClickedCallback([=] { if (const auto controller = resolve(session)) { ShowPremiumPreviewBox( controller, PremiumFeature::AiCompose); } close(); }); } else if (content->mode() == ComposeAiMode::Style && !content->hasStyleSelection()) { const auto btn = addApplyButton( *boxStyleNoSend, tr::lng_ai_compose_select_style(), nullptr); disableButton(btn); } else if (content->hasResult()) { const auto isStyle = (content->mode() == ComposeAiMode::Style); const auto btn = addApplyButton( args.send ? *boxStyleWithSend : *boxStyleNoSend, isStyle ? tr::lng_ai_compose_apply_style() : tr::lng_ai_compose_apply(), applyAndClose); if (args.send) { const auto send = Ui::CreateChild( btn->parentWidget(), st::aiComposeSendButton); send->setState({ .type = Ui::SendButton::Type::Send }); send->show(); btn->geometryValue( ) | rpl::on_next([=](QRect geometry) { const auto size = sendButtonHeight; send->resize(size, size); send->moveToLeft( geometry.x() + geometry.width() + st::aiComposeSendButtonSkip, geometry.y() + (geometry.height() - size) / 2); }, send->lifetime()); send->setClickedCallback([=] { sendResult({}); }); if (args.setupMenu) { args.setupMenu( send, [=](Api::SendOptions options) { sendResult(options); }); } *sendButton = send; } } else { const auto isStyle = (content->mode() == ComposeAiMode::Style); const auto btn = addApplyButton( *boxStyleNoSend, isStyle ? tr::lng_ai_compose_apply_style() : tr::lng_ai_compose_apply(), nullptr); disableButton(btn); } }; content->setReadyChangedCallback([=](bool) { rebuildButtons(); }); content->setPremiumFloodCallback([=] { *premiumFlooded = true; rebuildButtons(); }); content->setModeChangedCallback([=](ComposeAiMode mode) { rebuildButtons(); updateStyleTooltipVisibility(mode == ComposeAiMode::Style); }); content->setStyleSelectedCallback([=] { rebuildButtons(); if (!Core::App().settings().readPref(kAiComposeStyleTooltipHiddenPref)) { Core::App().settings().writePref(kAiComposeStyleTooltipHiddenPref, true); } updateStyleTooltipVisibility(false); }); rebuildButtons(); content->start(); } void ShowComposeAiBox( std::shared_ptr show, ComposeAiBoxArgs &&args) { show->show(Box(ComposeAiBox, std::move(args))); } } // namespace HistoryView::Controls