/* 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/create_ai_tone_box.h" #include "core/shortcuts.h" #include "menu/menu_check_item.h" #include "settings/sections/settings_shortcuts.h" #include "ui/widgets/checkbox.h" #include "window/window_session_controller.h" #include "boxes/premium_preview_box.h" #include "boxes/share_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_ai_compose_tones.h" #include "data/data_document.h" #include "data/data_file_origin.h" #include "data/data_msg_id.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "iv/markdown/iv_markdown_article.h" #include "iv/markdown/iv_markdown_common.h" #include "iv/markdown/iv_markdown_prepare.h" #include "iv/iv_cached_media.h" #include "iv/iv_rich_message_serializer.h" #include "iv/iv_rich_page.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" #include "main/main_session.h" #include "settings/sections/settings_premium.h" #include "spellcheck/platform/platform_language.h" #include "ui/boxes/about_cocoon_box.h" #include "ui/boxes/choose_language_box.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.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/menu/menu_action.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/tooltip.h" #include "window/themes/window_theme.h" #include "styles/style_basic.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_iv.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.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 Data::AiComposeTone &tone) { return { .id = tone.isDefault ? tone.defaultType : QString::number(tone.id), .label = tone.title, .customEmojiData = tone.emojiId ? Data::SerializeCustomEmojiId(tone.emojiId) : QString(), }; } [[nodiscard]] std::vector ResolveStyleDescriptors( const std::vector &tones) { auto result = std::vector(); result.reserve(tones.size()); for (const auto &tone : tones) { result.push_back(ResolveStyleDescriptor(tone)); } 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]] auto WithAddStyleTab(std::vector tabs) -> std::vector { tabs.push_back({ .id = u"_add_style"_q, .label = tr::lng_ai_compose_tone_create(tr::now), .icon = &st::aiComposeAddStyleIcon, .iconActive = &st::aiComposeAddStyleIconOver, }); return tabs; } [[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 ComposeAiRichBody final : public Ui::RpWidget { public: ComposeAiRichBody(QWidget *parent, not_null session); ~ComposeAiRichBody(); void setPage(std::shared_ptr page); protected: int resizeGetHeight(int newWidth) override; void paintEvent(QPaintEvent *e) override; private: void requestRepaint(QRect rect); const not_null _session; std::shared_ptr _mediaRuntime; Iv::Markdown::MarkdownArticle _article; std::unique_ptr _theme; std::unique_ptr _style; int _paletteVersion = -1; bool _hasArticle = false; }; class ComposeAiPreviewCard final : public Ui::RpWidget { public: ComposeAiPreviewCard( QWidget *parent, not_null session, TextWithEntities original, std::shared_ptr chatStyle, std::shared_ptr richSource); 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 setResultPage(std::shared_ptr page); 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 std::shared_ptr _richSource; 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; ComposeAiRichBody *_richBody = nullptr; 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]] std::shared_ptr richResult() const; [[nodiscard]] const std::vector &stylesData() const; [[nodiscard]] const std::vector &tones() 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 refreshTones(); void selectToneById(uint64 id); 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 requestRich(Api::ComposeWithAi::Request &&request); void resetState(CardState state); void applyResult(Api::ComposeWithAi::Result &&result); void applyRichResult(std::shared_ptr page); void showError(const QString &error = {}); void setAuthorId(UserId authorId); void notifyLoadingChanged(); void notifyReadyChanged(); [[nodiscard]] QString currentTranslateStyle() const; [[nodiscard]] QString currentTranslateStyleLabel() const; const not_null _box; const not_null _session; const std::shared_ptr _richSource; const TextWithEntities _original; const LanguageId _detectedFrom; LanguageId _to; std::vector _tones; std::vector _stylesData; std::vector _translateStylesData; QPointer _tabs; QPointer _styles; QPointer> _stylesWrap; const not_null _preview; const not_null _authorLabel; Fn _readyChanged; Fn _loadingChanged; Fn _premiumFlood; Fn _modeChanged; Fn _styleSelected; ComposeAiMode _mode = ComposeAiMode::Style; int _styleIndex = -1; int _translateStyleIndex = 0; UserId _authorId = UserId(0); bool _emojify = false; CardState _state = CardState::Waiting; mtpRequestId _requestId = 0; int _requestToken = 0; TextWithEntities _result; std::shared_ptr _richResult; }; // ComposeAiModeButton ComposeAiModeButton::ComposeAiModeButton( QWidget *parent, ComposeAiMode mode, QString label) : RippleButton(parent, st::aiComposeButtonRippleInactive) , _mode(mode) , _label(std::move(label)) { setCursor(style::cur_pointer); setAccessibleName(_label); } 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, std::shared_ptr richSource) : RpWidget(parent) , _context(Core::TextContext({ .session = session })) , _original(std::move(original)) , _richSource(std::move(richSource)) , _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(); } }); _copy->setAccessibleName(tr::lng_sr_ai_compose_copy_result(tr::now)); _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 (_richSource) { _richBody = Ui::CreateChild(this, session); _richBody->setPage(_richSource); _resultBody->hide(); } 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: if (_richBody) { _richBody->setPage(_richSource); } else { _resultBody->setMarkedText(_original, _context); if (wasLoading) { _skeleton.stop(); } } _copy->setVisible(false); break; case CardState::Loading: if (_richBody) { _richBody->setPage(_richSource); } else { _resultBody->setMarkedText(_original, _context); _skeleton.start(); } _copy->setVisible(false); break; case CardState::Ready: _copy->setVisible(!_richBody); if (!_richBody && wasLoading) { _skeleton.stop(); } break; } refreshGeometry(); } void ComposeAiPreviewCard::setResultText(TextWithEntities text) { _resultBody->setMarkedText(std::move(text), _context); refreshGeometry(); } void ComposeAiPreviewCard::setResultPage( std::shared_ptr page) { if (_richBody) { _richBody->setPage(std::move(page)); } 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)); if (_richBody) { _richBody->resizeToWidth(contentWidth); _richBody->setGeometryToLeft( padding.left(), y, contentWidth, _richBody->height(), newWidth); y += _richBody->height(); return y + padding.bottom(); } 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); auto color = st::windowSubTextFg->c; color.setAlphaF(st::aiComposeShadowOpacity); p.setPen(color); 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); _originalToggle->setAccessibleName(_originalExpanded ? tr::lng_sr_ai_compose_collapse_original(tr::now) : tr::lng_sr_ai_compose_expand_original(tr::now)); } // ComposeAiRichBody ComposeAiRichBody::ComposeAiRichBody( QWidget *parent, not_null session) : RpWidget(parent) , _session(session) , _article(st::messageMarkdown) , _theme(Window::Theme::DefaultChatThemeOn(lifetime())) , _style(std::make_unique(session->colorIndicesValue())) { _style->apply(_theme.get()); _paletteVersion = _style->paletteVersion(); const auto weak = base::make_weak(this); _article.setTextRepaintCallbacks( [weak] { if (const auto owner = weak.get()) { owner->requestRepaint(QRect()); } }, [weak](QRect rect) { if (const auto owner = weak.get()) { owner->requestRepaint(rect); } }); _mediaRuntime = Iv::CreateMessageMediaRuntime( session, FullMsgId(), [](QString) {}, [](QString) {}, ::Data::FileOrigin()); } ComposeAiRichBody::~ComposeAiRichBody() { _article.setTextRepaintCallbacks(nullptr, nullptr); } void ComposeAiRichBody::setPage(std::shared_ptr page) { const auto richLimits = Iv::ResolveRichMessageLimits(_session); auto prepared = Iv::Markdown::TryPrepareNativeInstantView({ .richPage = page, .mediaRuntime = _mediaRuntime, .dimensionsOverride = Iv::Markdown::CaptureMarkdownPrepareDimensions( st::messageMarkdown), .tableRenderLimits = Iv::Markdown::PrepareTableRenderLimitsForRichMessage(richLimits), }); _hasArticle = prepared.supported(); if (_hasArticle) { _article.setContent(std::move(prepared.content)); } if (width() > 0) { resizeToWidth(width()); } update(); } void ComposeAiRichBody::requestRepaint(QRect rect) { crl::on_main(this, [=] { if (rect.isEmpty()) { update(); } else { update(rect); } }); } int ComposeAiRichBody::resizeGetHeight(int newWidth) { return (_hasArticle && newWidth > 0) ? _article.resizeGetHeight(newWidth) : 0; } void ComposeAiRichBody::paintEvent(QPaintEvent *e) { if (!_hasArticle || rect().isEmpty()) { return; } if (_paletteVersion != _style->paletteVersion()) { _paletteVersion = _style->paletteVersion(); _article.invalidatePaletteCache(); } auto p = Painter(this); auto context = Iv::Markdown::MarkdownArticlePaintContext( _theme->preparePaintContext( _style.get(), rect(), rect(), e->rect(), false)); const auto messageStyle = context.messageStyle(); context.caches = { .pre = messageStyle->preCache.get(), .blockquote = context.quoteCache({}, 0), .colors = _style->highlightColors(), .st = &messageStyle->richPageStyle, .repaint = [weak = base::make_weak(this)] { if (const auto owner = weak.get()) { owner->requestRepaint(QRect()); } }, .repaintRect = [weak = base::make_weak(this)](QRect rect) { if (const auto owner = weak.get()) { owner->requestRepaint(rect); } }, }; _article.setVisibleTopBottom(0, height()); p.save(); p.setClipRect(e->rect()); _article.paint(p, context); p.restore(); } // ComposeAiContent ComposeAiContent::ComposeAiContent( QWidget *parent, not_null box, ComposeAiBoxArgs args) : RpWidget(parent) , _box(box) , _session(args.session) , _richSource(args.richSource) , _original(_richSource ? Iv::FlattenRichPageSummary(*_richSource) : std::move(args.text)) , _detectedFrom(Platform::Language::Recognize(_original.text)) , _to(DefaultAiTranslateTo(_detectedFrom)) , _tones(_session->data().aiComposeTones().list()) , _stylesData(ResolveStyleDescriptors(_tones)) , _translateStylesData(ResolveTranslateStyleDescriptors(_session, _stylesData)) , _preview( Ui::CreateChild( this, _session, _original, args.chatStyle, _richSource)) , _authorLabel(Ui::CreateChild( this, st::aiComposeAuthorLabel)) { _preview->setResizeCallback([=] { refreshLayout(); }); _preview->setChooseCallback([=] { chooseLanguage(); }); _preview->setCopyCallback([=] { copyResult(); }); _preview->setEmojifyChangedCallback([=](bool checked) { _emojify = checked; if (_mode != ComposeAiMode::Fix) { request(); } }); _preview->setShow(_box->uiShow()); _authorLabel->setVisible(false); _authorLabel->heightValue( ) | rpl::skip(1) | rpl::on_next([=] { refreshLayout(); }, lifetime()); const auto show = _box->uiShow(); _authorLabel->setClickHandlerFilter([=]( const ClickHandlerPtr &handler, Qt::MouseButton button) { if (dynamic_cast(handler.get())) { ActivateClickHandler(_authorLabel, handler, ClickContext{ .button = button, .other = QVariant::fromValue(ClickHandlerContext{ .show = show, }) }); return false; } return true; }); } ComposeAiContent::~ComposeAiContent() { cancelRequest(); } bool ComposeAiContent::hasResult() const { return _state == CardState::Ready; } const TextWithEntities &ComposeAiContent::result() const { return _result; } std::shared_ptr ComposeAiContent::richResult() const { return _richResult; } const std::vector &ComposeAiContent::stylesData() const { return _stylesData; } const std::vector &ComposeAiContent::tones() const { return _tones; } 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(_tones.size())) { const auto wasNoSelection = (_styleIndex < 0); _styleIndex = index; updateTitles(); if (_mode == ComposeAiMode::Style) { request(); if (wasNoSelection && _styleSelected) { _styleSelected(); } } } else if (index == int(_tones.size())) { _styles->setActive(_styleIndex); _box->uiShow()->show(Box( CreateAiToneBox, _session, crl::guard(this, [=](Data::AiComposeTone tone) { selectToneById(tone.id); }))); } }); _styles->setActive(_styleIndex); _stylesWrap->toggle(_mode == ComposeAiMode::Style, anim::type::instant); } void ComposeAiContent::refreshTones() { auto previousKey = QString(); auto hadSelection = false; if (_styleIndex >= 0 && _styleIndex < int(_tones.size())) { const auto &prev = _tones[_styleIndex]; previousKey = prev.isDefault ? prev.defaultType : QString::number(prev.id); hadSelection = true; } _tones = _session->data().aiComposeTones().list(); _stylesData = ResolveStyleDescriptors(_tones); _translateStylesData = ResolveTranslateStyleDescriptors( _session, _stylesData); auto remapped = -1; if (hadSelection) { for (auto i = 0; i != int(_tones.size()); ++i) { const auto &tone = _tones[i]; const auto key = tone.isDefault ? tone.defaultType : QString::number(tone.id); if (key == previousKey) { remapped = i; break; } } } _styleIndex = remapped; if (_mode == ComposeAiMode::Style && hadSelection && _styleIndex < 0) { request(); } } void ComposeAiContent::selectToneById(uint64 id) { for (auto i = 0; i != int(_tones.size()); ++i) { const auto &tone = _tones[i]; if (!tone.isDefault && tone.id == id) { const auto wasNoSelection = (_styleIndex < 0); _styleIndex = i; updateTitles(); if (_styles) { _styles->setActive(_styleIndex); _styles->scrollToActive(); } if (_mode == ComposeAiMode::Style) { request(); if (wasNoSelection && _styleSelected) { _styleSelected(); } } return; } } } void ComposeAiContent::start() { updatePinnedTabs(anim::type::instant); updateTitles(); request(); } int ComposeAiContent::resizeGetHeight(int newWidth) { _preview->resizeToWidth(newWidth); _preview->moveToLeft(0, 0, newWidth); auto y = _preview->height(); if (!_authorLabel->isHidden()) { _authorLabel->resizeToWidth(newWidth); _authorLabel->moveToLeft( 0, y + st::aiComposeAuthorLabelTop, newWidth); y += st::aiComposeAuthorLabelTop + _authorLabel->height(); } return y; } 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); setAuthorId(UserId(0)); 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)); const auto emojifyOnlyMode = !hasResult && (_mode == ComposeAiMode::Style) && (_styleIndex < 0); _preview->setEmojifyVisible( (hasResult && (_mode != ComposeAiMode::Fix)) || emojifyOnlyMode); _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) { if (_richSource) { _session->api().request(_requestId).cancel(); } else { _session->api().composeWithAi().cancel(_requestId); } _requestId = 0; } } void ComposeAiContent::request() { cancelRequest(); if (_mode == ComposeAiMode::Style && _styleIndex < 0 && !_emojify) { if (_state != CardState::Waiting) { resetState(CardState::Waiting); } return; } resetState(CardState::Loading); auto request = Api::ComposeWithAi::Request{ .text = _original, .emojify = (_mode != ComposeAiMode::Fix) && _emojify, }; switch (_mode) { case ComposeAiMode::Translate: { request.translateToLang = _to.twoLetterCode(); const auto style = currentTranslateStyle(); if (!style.isEmpty()) { request.setDefaultTone(style); } } break; case ComposeAiMode::Style: if (_styleIndex >= 0 && _styleIndex < int(_tones.size())) { const auto &tone = _tones[_styleIndex]; if (tone.isDefault) { request.setDefaultTone(tone.defaultType); } else { request.setCustomTone(tone.id, tone.accessHash); } } break; case ComposeAiMode::Fix: request.proofread = true; break; } if (_richSource) { requestRich(std::move(request)); return; } 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; if (MTP::IgnoreError(error)) { weak->resetState(CardState::Waiting); return; } weak->showError(error.type()); }); } void ComposeAiContent::requestRich(Api::ComposeWithAi::Request &&request) { const auto serialized = Iv::SerializeInputRichMessage( _session, *_richSource, Iv::SerializeInputRichMessageMode::Draft); if (serialized.status != Iv::SerializeInputRichMessageStatus::Success || !serialized.value) { showError({}); return; } using Flag = MTPmessages_composeRichMessageWithAI::Flag; auto flags = MTPmessages_composeRichMessageWithAI::Flags(0) | Flag::f_text; if (request.proofread) { flags |= Flag::f_proofread; } if (!request.translateToLang.isEmpty()) { flags |= Flag::f_translate_to_lang; } if (request.tone) { flags |= Flag::f_tone; } if (request.emojify) { flags |= Flag::f_emojify; } const auto token = ++_requestToken; const auto weak = QPointer(this); _requestId = _session->api().request( MTPmessages_ComposeRichMessageWithAI( MTP_flags(flags), *serialized.value, (request.translateToLang.isEmpty() ? MTPstring() : MTP_string(request.translateToLang)), (request.tone ? (request.tone->id ? MTP_inputAiComposeToneID( MTP_long(request.tone->id), MTP_long(request.tone->accessHash)) : MTP_inputAiComposeToneDefault( MTP_string(request.tone->defaultTone))) : MTPInputAiComposeTone())) ).done([=](const MTPmessages_ComposedRichMessageWithAI &result) { if (!weak || weak->_requestToken != token) { return; } weak->_requestId = 0; weak->applyRichResult(Iv::ParseRichPage( weak->_session, result.data().vresult())); }).fail([=](const MTP::Error &error) { if (!weak || weak->_requestToken != token) { return; } weak->_requestId = 0; if (MTP::IgnoreError(error)) { weak->resetState(CardState::Waiting); return; } weak->showError(error.type()); }).handleFloodErrors().send(); } void ComposeAiContent::setAuthorId(UserId authorId) { if (_authorId == authorId) { return; } _authorId = authorId; if (const auto user = _session->data().userLoaded(authorId)) { const auto name = user->shortName(); auto mention = tr::marked(name); mention.entities.push_back(EntityInText( EntityType::MentionName, 0, name.size(), TextUtilities::MentionNameDataFromFields({ .selfId = _session->userId().bare, .userId = authorId.bare, .accessHash = user->accessHash(), }))); _authorLabel->setMarkedText( tr::lng_ai_compose_author( tr::now, lt_user, std::move(mention), tr::marked), Core::TextContext({ .session = _session })); _authorLabel->setVisible(true); } else { _authorLabel->setMarkedText({}); _authorLabel->setVisible(false); _authorId = UserId(0); } refreshLayout(); } void ComposeAiContent::resetState(CardState state) { _state = state; _result = {}; _richResult = nullptr; setAuthorId(UserId(0)); _preview->setState(state); notifyLoadingChanged(); updateTitles(); notifyReadyChanged(); } 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)); if (_mode == ComposeAiMode::Style && _styleIndex >= 0 && _styleIndex < int(_tones.size())) { setAuthorId(_tones[_styleIndex].authorId); } else { setAuthorId(UserId(0)); } } updateTitles(); notifyReadyChanged(); refreshLayout(); } void ComposeAiContent::applyRichResult( std::shared_ptr page) { if (!page || page->blocks.empty()) { showError({}); return; } _richResult = std::move(page); _state = CardState::Ready; _preview->setState(_state); notifyLoadingChanged(); _preview->setResultPage(_richResult); if (_mode == ComposeAiMode::Style && _styleIndex >= 0 && _styleIndex < int(_tones.size())) { setAuthorId(_tones[_styleIndex].authorId); } else { setAuthorId(UserId(0)); } updateTitles(); notifyReadyChanged(); refreshLayout(); } void ComposeAiContent::showError(const QString &error) { _state = CardState::Failed; setAuthorId(UserId(0)); _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; } else if (error == u"INPUT_TEXT_TOO_LONG"_q) { _box->showToast(tr::lng_ai_compose_error_too_long(tr::now)); 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; } struct StyleTooltipHandle { QPointer tooltip; Fn updateVisibility; }; [[nodiscard]] StyleTooltipHandle 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 { tooltip, 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::aiComposeBoxClose, [=] { box->closeBox(); })->setAccessibleName(tr::lng_close(tr::now)); box->addTopButton(st::aiComposeBoxInfoButton, [=] { box->uiShow()->show(Box(Ui::AboutCocoonBox)); })->setAccessibleName(tr::lng_sr_ai_compose_info(tr::now)); 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); const auto contextMenu = box->lifetime().make_state< base::unique_qptr>(); const auto stylesWrapHolder = box->lifetime().make_state< QPointer>>(); const auto styleTooltipHolder = box->lifetime().make_state< QPointer>(); const auto styleTooltipUpdater = box->lifetime().make_state< Fn>(); content->setModeTabs(tabs); const auto rebuildStylesWrap = [=] { auto savedScroll = -1; if (const auto old = stylesWrapHolder->data()) { savedScroll = old->entity()->scrollLeft(); delete old; } if (const auto old = styleTooltipHolder->data()) { delete old; } auto emojiFactory = session->data().customEmojiManager().factory( Data::CustomEmojiSizeTag::Large); auto wrap = object_ptr>( pinnedToTop, object_ptr( pinnedToTop, WithAddStyleTab(content->stylesData()), std::move(emojiFactory)), tabsSkip); const auto ptr = wrap.data(); pinnedToTop->add(std::move(wrap), st::aiComposeContentMargin); *stylesWrapHolder = ptr; ptr->entity()->setContextMenuCallback([=](int index, QPoint globalPos) { const auto &tones = content->tones(); if (index < 0 || index >= int(tones.size())) { return; } const auto &tone = tones[index]; if (tone.isDefault) { return; } *contextMenu = base::make_unique_q( ptr->entity(), st::popupMenuWithIcons); const auto toneCopy = tone; if (!toneCopy.slug.isEmpty()) { const auto shortcutText = Api::AiApplyShortcutText(); if (shortcutText.isEmpty()) { const auto resolve = ChatHelpers::ResolveWindowDefault(); (*contextMenu)->addAction( tr::lng_ai_compose_bind_set_hotkey_short(tr::now), [=] { if (const auto window = resolve(session)) { window->setHighlightControlId( Settings::ShortcutsHighlightId( Shortcuts::Command ::ComposeAiApplyInPlace)); window->showSettings( Settings::ShortcutsId()); } }, &st::menuIconShortcut); } else { const auto label = tr::lng_ai_compose_bind_use_hotkey( tr::now, lt_keys, shortcutText); const auto checked = (Api::AiApplyBoundSlug() == toneCopy.slug); auto item = base::make_unique_q( (*contextMenu)->menu(), st::popupMenuWithIcons.menu, Ui::CreateChild( (*contextMenu)->menu().get()), nullptr, nullptr); item->action()->setText(label); item->init(checked); item->checkView()->checkedChanges( ) | rpl::on_next([=](bool toggled) { if (toggled) { Api::SetAiApplyBoundSlug(toneCopy.slug); } else if (Api::AiApplyBoundSlug() == toneCopy.slug) { Api::ClearAiApplyBoundSlug(); } }, item->lifetime()); (*contextMenu)->addAction(std::move(item)); } } if (toneCopy.creator) { (*contextMenu)->addAction( tr::lng_ai_compose_tone_edit(tr::now), [=] { box->uiShow()->show(Box( EditAiToneBox, session, toneCopy, crl::guard(content, [=](Data::AiComposeTone tone) { content->selectToneById(tone.id); }))); }, &st::menuIconEdit); } (*contextMenu)->addAction( tr::lng_ai_compose_tone_share(tr::now), [=] { const auto url = session->createInternalLinkFull( "addstyle/" + toneCopy.slug); FastShareLink( Main::MakeSessionShow(box->uiShow(), session), url); }, &st::menuIconShare); (*contextMenu)->addAction(base::make_unique_q( (*contextMenu)->menu(), st::menuWithIconsAttention, Ui::Menu::CreateAction( (*contextMenu)->menu().get(), toneCopy.creator ? tr::lng_ai_compose_tone_delete(tr::now) : tr::lng_ai_compose_tone_remove(tr::now), [=] { ConfirmDeleteAiTone( box->uiShow(), session, toneCopy); }), &st::menuIconDeleteAttention, &st::menuIconDeleteAttention)); (*contextMenu)->popup(globalPos); }); content->setStyleTabs(ptr); if (savedScroll >= 0) { ptr->entity()->setScrollLeft(savedScroll); } auto handle = SetupStyleTooltip( box, pinnedToTop, ptr, [=] { return content->mode(); }); *styleTooltipHolder = handle.tooltip; *styleTooltipUpdater = std::move(handle.updateVisibility); }; rebuildStylesWrap(); session->data().aiComposeTones().updated( ) | rpl::on_next([=] { content->refreshTones(); rebuildStylesWrap(); }, box->lifetime()); 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; } if (args.applyRich) { args.applyRich(content->richResult()); } else { 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::aiComposeBoxClose, [=] { box->closeBox(); })->setAccessibleName(tr::lng_close(tr::now)); box->addTopButton(st::aiComposeBoxInfoButton, [=] { box->uiShow()->show(Box(Ui::AboutCocoonBox)); })->setAccessibleName(tr::lng_sr_ai_compose_info(tr::now)); 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() && !content->hasResult()) { 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->setAccessibleName(tr::lng_send_button(tr::now)); 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(); (*styleTooltipUpdater)(mode == ComposeAiMode::Style); }); content->setStyleSelectedCallback([=] { rebuildButtons(); if (!Core::App().settings().readPref(kAiComposeStyleTooltipHiddenPref)) { Core::App().settings().writePref(kAiComposeStyleTooltipHiddenPref, true); } (*styleTooltipUpdater)(false); }); rebuildButtons(); content->start(); } void ShowComposeAiBox( std::shared_ptr show, ComposeAiBoxArgs &&args) { show->show(Box(ComposeAiBox, std::move(args))); } } // namespace HistoryView::Controls