/* 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/create_ai_box.h" #include "apiwrap.h" #include "base/object_ptr.h" #include "chat_helpers/compose/compose_show.h" #include "data/data_file_origin.h" #include "data/data_msg_id.h" #include "iv/iv_cached_media.h" #include "iv/iv_rich_page.h" #include "iv/markdown/iv_markdown_article.h" #include "iv/markdown/iv_markdown_common.h" #include "iv/markdown/iv_markdown_prepare.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "mtproto/mtproto_response.h" #include "spellcheck/spellcheck_types.h" #include "ui/boxes/choose_language_box.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/rp_widget.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" #include "window/themes/window_theme.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" #include "styles/style_iv.h" #include "styles/style_layers.h" namespace Iv::Editor { namespace { [[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]] TextWithEntities SelectorTitle(LanguageId id) { return tr::lng_ai_compose_to_language( tr::now, lt_language, tr::link(Ui::LanguageName(id)), tr::marked); } class PromptIsland final : public Ui::RpWidget { public: PromptIsland(QWidget *parent); [[nodiscard]] not_null field() const; private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; object_ptr _field; }; PromptIsland::PromptIsland(QWidget *parent) : RpWidget(parent) , _field( this, st::createAiPromptField, Ui::InputField::Mode::MultiLine, tr::lng_ai_compose_create_placeholder()) { _field->setSubmitSettings(Ui::InputField::SubmitSettings::None); _field->heightChanges() | rpl::on_next([=] { if (width() > 0) { resizeToWidth(width()); } }, lifetime()); resizeToWidth(st::boxWideWidth); } not_null PromptIsland::field() const { return _field.data(); } void PromptIsland::paintEvent(QPaintEvent *e) { auto p = Painter(this); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::aiComposeCardBg); p.drawRoundedRect( rect(), st::aiComposeCardRadius, st::aiComposeCardRadius); } void PromptIsland::resizeEvent(QResizeEvent *e) { const auto &padding = st::aiComposeCardPadding; const auto inner = width() - padding.left() - padding.right(); _field->resizeToWidth(inner); _field->moveToLeft(padding.left(), padding.top()); const auto height = padding.top() + _field->height() + padding.bottom(); if (this->height() != height) { resize(width(), height); } } class ResponseIsland final : public Ui::RpWidget { public: ResponseIsland( QWidget *parent, not_null session, std::shared_ptr page, LanguageId language, bool emojify, Fn chooseLanguage, Fn emojifyChanged); private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; void paintArticle(Painter &p, QRect clip); void requestArticleRepaint(QRect articleRect); [[nodiscard]] int controlRowHeight() const; [[nodiscard]] QRect articleRect() const; const not_null _session; const object_ptr _selector; const object_ptr _arrows; const object_ptr _emojify; std::unique_ptr _theme; std::unique_ptr _style; Iv::Markdown::MarkdownArticle _article; std::shared_ptr _mediaRuntime; int _articleHeight = 0; int _paletteVersion = -1; bool _hasArticle = false; }; ResponseIsland::ResponseIsland( QWidget *parent, not_null session, std::shared_ptr page, LanguageId language, bool emojify, Fn chooseLanguage, Fn emojifyChanged) : RpWidget(parent) , _session(session) , _selector(this, st::aiComposeCardTitle) , _arrows(this) , _emojify( this, tr::lng_ai_compose_emojify(tr::now), st::aiComposeEmojifyCheckbox, std::make_unique(st::defaultCheck, emojify)) , _theme(Window::Theme::DefaultChatThemeOn(lifetime())) , _style(std::make_unique(session->colorIndicesValue())) , _article(st::messageMarkdown) { _style->apply(_theme.get()); _paletteVersion = _style->paletteVersion(); _selector->setMarkedText(SelectorTitle(language)); _selector->setClickHandlerFilter([=](const auto &...) { if (chooseLanguage) { chooseLanguage(); } return false; }); const auto &icon = st::createAiSelectorArrowsIcon; _arrows->setAttribute(Qt::WA_TransparentForMouseEvents); _arrows->resize(icon.size()); _arrows->paintRequest() | rpl::on_next([=] { auto p = QPainter(_arrows.data()); st::createAiSelectorArrowsIcon.paint(p, 0, 0, _arrows->width()); }, _arrows->lifetime()); _emojify->checkedChanges() | rpl::on_next([=](bool checked) { if (emojifyChanged) { emojifyChanged(checked); } }, _emojify->lifetime()); _article.setTextRepaintCallbacks( [=] { requestArticleRepaint(QRect()); }, [=](QRect rect) { requestArticleRepaint(rect); }); const auto richLimits = Iv::ResolveRichMessageLimits(session); _mediaRuntime = Iv::CreateMessageMediaRuntime( session, FullMsgId(), [](QString) {}, [](QString) {}, ::Data::FileOrigin()); auto prepared = Iv::Markdown::TryPrepareNativeInstantView({ .richPage = page, .mediaRuntime = _mediaRuntime, .dimensionsOverride = Iv::Markdown::CaptureMarkdownPrepareDimensions( st::messageMarkdown), .tableRenderLimits = Iv::Markdown::PrepareTableRenderLimitsForRichMessage(richLimits), }); if (prepared.supported()) { _article.setContent(std::move(prepared.content)); _hasArticle = true; } } void ResponseIsland::requestArticleRepaint(QRect rect) { crl::on_main(this, [=] { if (rect.isEmpty()) { update(); } else { update(rect.translated(articleRect().topLeft())); } }); } QRect ResponseIsland::articleRect() const { const auto &padding = st::aiComposeCardPadding; const auto inner = std::max(width() - padding.left() - padding.right(), 0); const auto top = padding.top() + controlRowHeight() + st::aiComposeCardSectionSkip; return QRect(padding.left(), top, inner, _articleHeight); } void ResponseIsland::paintArticle(Painter &p, QRect clip) { if (!_hasArticle) { return; } if (_paletteVersion != _style->paletteVersion()) { _paletteVersion = _style->paletteVersion(); _article.invalidatePaletteCache(); } const auto content = articleRect(); if (content.isEmpty()) { return; } const auto articleClip = content.intersected(clip).translated( -content.topLeft()); if (articleClip.isEmpty()) { return; } auto context = Iv::Markdown::MarkdownArticlePaintContext( _theme->preparePaintContext( _style.get(), QRect(QPoint(), content.size()), QRect(QPoint(), content.size()), articleClip, false)); const auto messageStyle = context.messageStyle(); context.caches = { .pre = messageStyle->preCache.get(), .blockquote = context.quoteCache({}, 0), .colors = _style->highlightColors(), .st = &messageStyle->richPageStyle, .repaint = [=] { requestArticleRepaint(QRect()); }, .repaintRect = [=](QRect rect) { requestArticleRepaint(rect); }, }; _article.setVisibleTopBottom(0, content.height()); p.save(); p.setClipRect(content.intersected(clip)); p.translate(content.topLeft()); _article.paint(p, context); p.restore(); } int ResponseIsland::controlRowHeight() const { return std::max(_selector->height(), _emojify->height()); } void ResponseIsland::paintEvent(QPaintEvent *e) { auto p = Painter(this); { auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::aiComposeCardBg); p.drawRoundedRect( rect(), st::aiComposeCardRadius, st::aiComposeCardRadius); } paintArticle(p, e->rect()); } void ResponseIsland::resizeEvent(QResizeEvent *e) { const auto &padding = st::aiComposeCardPadding; const auto inner = width() - padding.left() - padding.right(); const auto rowHeight = controlRowHeight(); _emojify->resizeToNaturalWidth(inner); _emojify->moveToRight( padding.right(), padding.top() + (rowHeight - _emojify->height()) / 2, width()); const auto arrowsSkip = st::aiComposeCardControlSkip; const auto selectorWidth = std::max( inner - _emojify->width() - st::aiComposeCardControlSkip - _arrows->width() - arrowsSkip, 0); _selector->resizeToWidth(selectorWidth); _selector->moveToLeft( padding.left(), padding.top() + (rowHeight - _selector->height()) / 2); _arrows->moveToLeft( padding.left() + _selector->textMaxWidth() + arrowsSkip, padding.top() + (rowHeight - _arrows->height()) / 2); auto y = padding.top() + rowHeight; if (_hasArticle && inner > 0) { y += st::aiComposeCardSectionSkip; _articleHeight = _article.resizeGetHeight(inner); y += _articleHeight; } y += padding.bottom(); if (height() != y) { resize(width(), y); } } struct State { not_null session; Fn)> applyToPage; mtpRequestId requestId = 0; Ui::InputField *prompt = nullptr; LanguageId language; bool emojify = false; enum class Phase { Initial, Loading, HasResult, }; Phase phase = Phase::Initial; std::shared_ptr page; Ui::RpWidget *responseIsland = nullptr; Ui::RoundButton *reloadButton = nullptr; Fn generate; Fn rebuildButtons; Fn rebuildResponseIsland; }; } // namespace void CreateAiBox(not_null box, CreateAiBoxArgs &&args) { const auto state = box->lifetime().make_state(State{ .session = args.session, .applyToPage = std::move(args.applyToPage), .language = DefaultAiTranslateTo(LanguageId()), }); box->setWidth(st::boxWideWidth); box->setTitle(tr::lng_ai_compose_create_title()); box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); }); box->setStyle(st::aiComposeBox); const auto island = box->addRow( object_ptr(box), st::boxRowPadding); state->prompt = island->field(); const auto chooseLanguage = [=] { box->uiShow()->showBox(Box([=](not_null chooser) { Ui::ChooseLanguageBox( chooser, tr::lng_languages(), [=](std::vector ids) { if (ids.empty()) { return; } state->language = ids.front(); state->generate(); }, { state->language }, false, nullptr); })); }; state->rebuildResponseIsland = [=] { if (state->responseIsland) { delete state->responseIsland; state->responseIsland = nullptr; } if (!state->page) { return; } const auto content = box->verticalLayout(); state->responseIsland = content->insert( 1, object_ptr( content, state->session, state->page, state->language, state->emojify, chooseLanguage, [=](bool checked) { state->emojify = checked; state->generate(); }), style::margins( st::boxRowPadding.left(), st::aiComposeCardSectionSkip, st::boxRowPadding.right(), 0)); }; state->rebuildButtons = [=] { if (state->reloadButton) { delete state->reloadButton; state->reloadButton = nullptr; } box->clearButtons(); if (state->phase == State::Phase::HasResult) { box->setStyle(st::aiComposeBoxWithSend); const auto pill = box->addButton( tr::lng_ai_compose_add_to_page(), [=] { if (state->applyToPage) { state->applyToPage(state->page); } }); pill->setFullRadius(true); const auto reload = Ui::CreateChild( pill->parentWidget(), rpl::single(QString()), st::createAiReloadButton); reload->setFullRadius(true); reload->show(); const auto icon = Ui::CreateChild(reload); icon->setAttribute(Qt::WA_TransparentForMouseEvents); icon->resize(st::createAiReloadIcon.size()); icon->paintRequest() | rpl::on_next([=] { auto p = QPainter(icon); st::createAiReloadIcon.paint(p, 0, 0, icon->width()); }, icon->lifetime()); icon->move( (reload->width() - icon->width()) / 2, (reload->height() - icon->height()) / 2); pill->geometryValue() | rpl::on_next([=](QRect geometry) { const auto size = st::createAiReloadButton.height; reload->moveToLeft( geometry.x() + geometry.width() + st::aiComposeSendButtonSkip, geometry.y() + (geometry.height() - size) / 2); }, reload->lifetime()); reload->setClickedCallback([=] { state->generate(); }); state->reloadButton = reload; } else { box->setStyle(st::aiComposeBox); box->addButton( tr::lng_ai_compose_generate(), [=] { state->generate(); } )->setFullRadius(true); } }; state->generate = [=] { const auto prompt = state->prompt->getLastText(); if (prompt.trimmed().isEmpty()) { return; } if (state->requestId) { state->session->api().request(state->requestId).cancel(); state->requestId = 0; } state->phase = State::Phase::Loading; state->rebuildButtons(); using Flag = MTPmessages_composeRichMessageWithAI::Flag; auto flags = MTPmessages_composeRichMessageWithAI::Flags(0) | Flag::f_tone; if (state->emojify) { flags |= Flag::f_emojify; } const auto lang = state->language ? state->language.twoLetterCode() : QString(); if (!lang.isEmpty()) { flags |= Flag::f_translate_to_lang; } state->requestId = state->session->api().request( MTPmessages_ComposeRichMessageWithAI( MTP_flags(flags), MTPInputRichMessage(), lang.isEmpty() ? MTPstring() : MTP_string(lang), MTP_inputAiComposeToneSingleUse(MTP_string(prompt))) ).done([=](const MTPmessages_ComposedRichMessageWithAI &result) { state->requestId = 0; state->page = Iv::ParseRichPage( state->session, result.data().vresult()); state->phase = State::Phase::HasResult; state->rebuildResponseIsland(); state->rebuildButtons(); }).fail([=](const MTP::Error &error) { state->requestId = 0; state->phase = state->page ? State::Phase::HasResult : State::Phase::Initial; state->rebuildButtons(); if (MTP::IgnoreError(error)) { return; } box->showToast(error.type()); }).handleFloodErrors().send(); }; state->rebuildButtons(); } void ShowCreateAiBox( std::shared_ptr show, CreateAiBoxArgs &&args) { show->show(Box(CreateAiBox, std::move(args))); } } // namespace Iv::Editor