diff --git a/ankihub/ankihub_client/ankihub_client.py b/ankihub/ankihub_client/ankihub_client.py
index 5bde61efe..81baeb898 100644
--- a/ankihub/ankihub_client/ankihub_client.py
+++ b/ankihub/ankihub_client/ankihub_client.py
@@ -1259,12 +1259,15 @@ def media_upload_finished(self, ah_did: uuid.UUID) -> None:
if response.status_code != 204:
raise AnkiHubHTTPError(response)
- def owned_deck_ids(self) -> List[uuid.UUID]:
+ def get_user_details(self) -> Dict[str, Any]:
response = self._send_request("GET", API.ANKIHUB, "/users/me")
if response.status_code != 200:
raise AnkiHubHTTPError(response)
- data = response.json()
+ return response.json()
+
+ def owned_deck_ids(self) -> List[uuid.UUID]:
+ data = self.get_user_details()
result = [uuid.UUID(deck["id"]) for deck in data["created_decks"]]
return result
@@ -1290,6 +1293,10 @@ def send_daily_card_review_summaries(
if response.status_code != 201:
raise AnkiHubHTTPError(response)
+ def is_premium_or_trialing(self) -> bool:
+ data = self.get_user_details()
+ return data["is_premium"] or data["is_trialing"]
+
class ThreadLocalSession:
def __init__(self):
diff --git a/ankihub/gui/js_message_handling.py b/ankihub/gui/js_message_handling.py
index b8cfa8d4a..b0206a664 100644
--- a/ankihub/gui/js_message_handling.py
+++ b/ankihub/gui/js_message_handling.py
@@ -16,9 +16,8 @@
from jinja2 import Template
from ..db import ankihub_db
-from ..settings import url_plans_page, url_view_note
+from ..settings import url_view_note
from .operations.scheduling import suspend_notes, unsuspend_notes
-from .utils import show_dialog
VIEW_NOTE_PYCMD = "ankihub_view_note"
VIEW_NOTE_BUTTON_ID = "ankihub-view-note-button"
@@ -27,7 +26,6 @@
UNSUSPEND_NOTES_PYCMD = "ankihub_unsuspend_notes"
SUSPEND_NOTES_PYCMD = "ankihub_suspend_notes"
GET_NOTE_SUSPENSION_STATES_PYCMD = "ankihub_get_note_suspension_states"
-ANKIHUB_UPSELL = "ankihub_ai_upsell"
COPY_TO_CLIPBOARD_PYCMD = "ankihub_copy_to_clipboard"
OPEN_LINK_PYCMD = "ankihub_open_link"
@@ -95,22 +93,6 @@ def _on_js_message(handled: Tuple[bool, Any], message: str, context: Any) -> Any
web=reviewer_sidebar.content_webview,
)
return (True, None)
- elif message == ANKIHUB_UPSELL:
-
- def on_button_clicked(button_index: int) -> None:
- if button_index == 1:
- openLink(url_plans_page())
-
- show_dialog(
- text="Upgrade your membership to Premium to access this feature 🌟",
- title="Your trial has ended!",
- buttons=[
- ("Cancel", aqt.QDialogButtonBox.ButtonRole.RejectRole),
- ("Upgrade", aqt.QDialogButtonBox.ButtonRole.ActionRole),
- ],
- default_button_idx=1,
- callback=on_button_clicked,
- )
elif message.startswith(COPY_TO_CLIPBOARD_PYCMD):
kwargs = parse_js_message_kwargs(message)
content = kwargs.get("content")
diff --git a/ankihub/gui/reviewer.py b/ankihub/gui/reviewer.py
index 6044297b9..62231f4b2 100644
--- a/ankihub/gui/reviewer.py
+++ b/ankihub/gui/reviewer.py
@@ -29,11 +29,7 @@
from ..gui.webview import AuthenticationRequestInterceptor, CustomWebPage # noqa: F401
from ..main.utils import mh_tag_to_resource_title_and_slug
from ..settings import config, url_login, url_mh_integrations_preview
-from .js_message_handling import (
- ANKIHUB_UPSELL,
- VIEW_NOTE_PYCMD,
- parse_js_message_kwargs,
-)
+from .js_message_handling import VIEW_NOTE_PYCMD, parse_js_message_kwargs
from .utils import get_ah_did_of_deck_or_ancestor_deck, using_qt5
from .web.templates import (
get_empty_state_html,
@@ -619,9 +615,6 @@ def _on_js_message(handled: Tuple[bool, Any], message: str, context: Any) -> Any
openLink(url)
return True, None
- elif message == ANKIHUB_UPSELL:
- reviewer_sidebar.close_sidebar()
- return True, None
return handled
diff --git a/ankihub/gui/web/media/_chatbot_icon_sleeping.svg b/ankihub/gui/web/media/_chatbot_icon_sleeping.svg
new file mode 100644
index 000000000..a25189162
--- /dev/null
+++ b/ankihub/gui/web/media/_chatbot_icon_sleeping.svg
@@ -0,0 +1,14 @@
+
diff --git a/ankihub/gui/web/media/_chatbot_icon_sleeping_dark_theme.svg b/ankihub/gui/web/media/_chatbot_icon_sleeping_dark_theme.svg
new file mode 100644
index 000000000..a2023e7e1
--- /dev/null
+++ b/ankihub/gui/web/media/_chatbot_icon_sleeping_dark_theme.svg
@@ -0,0 +1,14 @@
+
diff --git a/ankihub/gui/web/reviewer_buttons.js b/ankihub/gui/web/reviewer_buttons.js
index 3afe9f239..3f97c0055 100644
--- a/ankihub/gui/web/reviewer_buttons.js
+++ b/ankihub/gui/web/reviewer_buttons.js
@@ -3,6 +3,7 @@
class AnkiHubReviewerButtons {
constructor() {
this.theme = "{{ THEME }}";
+ this.isPremiumOrTrialing = "{{ IS_PREMIUM_OR_TRIALING }}" == "True";
this.isAnKingDeck = null;
this.bbCount = 0;
this.faCount = 0;
@@ -35,7 +36,8 @@ class AnkiHubReviewerButtons {
},
{
name: "chatbot",
- iconPath: "/_chatbot_icon.svg",
+ iconPath: this.isPremiumOrTrialing ? "/_chatbot_icon.svg" : "/_chatbot_icon_sleeping.svg",
+ iconPathDarkTheme: this.isPremiumOrTrialing ? null : "/_chatbot_icon_sleeping_dark_theme.svg",
active: false,
tooltip: "AI Chatbot"
},
diff --git a/ankihub/gui/web/templates.py b/ankihub/gui/web/templates.py
index 8a023678f..2353489f6 100644
--- a/ankihub/gui/web/templates.py
+++ b/ankihub/gui/web/templates.py
@@ -3,6 +3,8 @@
from jinja2 import Environment, FileSystemLoader, select_autoescape
+from ...addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient
+
TEMPLATES_PATH = (pathlib.Path(__file__).parent).absolute()
env = Environment(
@@ -24,10 +26,12 @@ def get_header_webview_html(
def get_reviewer_buttons_js(theme: str, enabled_buttons: List[str]) -> str:
+ client = AnkiHubClient()
return env.get_template("reviewer_buttons.js").render(
{
"THEME": theme,
"ENABLED_BUTTONS": ",".join(enabled_buttons),
+ "IS_PREMIUM_OR_TRIALING": str(client.is_premium_or_trialing()),
}
)
diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py
index 2db16ce1d..13fc07b39 100644
--- a/tests/addon/test_integration.py
+++ b/tests/addon/test_integration.py
@@ -6019,9 +6019,15 @@ def mock_using_qt5_to_return_false(mocker: MockerFixture):
mocker.patch("ankihub.gui.reviewer.using_qt5", return_value=False)
+@pytest.fixture
+def mock_user_details(mocker: MockerFixture):
+ user_details = {"is_premium": True, "is_trialing": False}
+ mocker.patch.object(AnkiHubClient, "get_user_details", return_value=user_details)
+
+
# The mock_using_qt5_to_return_false fixture is used to test the AnkiHub AI feature on Qt5,
# even though the feature is disabled on Qt5. (In CI we are only running test on Qt5.)
-@pytest.mark.usefixtures("mock_using_qt5_to_return_false")
+@pytest.mark.usefixtures("mock_using_qt5_to_return_false", "mock_user_details")
class TestAnkiHubAIInReviewer:
@pytest.mark.sequential
@pytest.mark.parametrize(