From e219e16d4285df9bcb77ae836fd97d02e6fb8c87 Mon Sep 17 00:00:00 2001 From: aslon Date: Sun, 8 Feb 2026 19:04:06 +0500 Subject: [PATCH] integrate smarty_ui components and primitives to where possible --- .gitignore | 15 +- functions_to_format/functions/__init__.py | 2 + .../functions/activity_report.py | 222 +- .../functions/activity_report_events.py | 243 +- functions_to_format/functions/balance.py | 657 ++--- .../functions/base_strategy.py | 93 + functions_to_format/functions/buttons.py | 242 +- .../functions/chatbot_answer.py | 388 +-- functions_to_format/functions/contact.py | 394 ++- functions_to_format/functions/functions.py | 22 +- functions_to_format/functions/general/text.py | 31 +- .../functions/human_approval.py | 504 ++-- functions_to_format/functions/news.py | 299 +- functions_to_format/functions/notification.py | 123 +- functions_to_format/functions/payment.py | 2485 ++++------------- functions_to_format/functions/products.py | 694 ++--- functions_to_format/functions/start_page.py | 47 +- functions_to_format/functions/transfer.py | 732 +++++ functions_to_format/functions/weather.py | 358 +-- pyproject.toml | 5 + share/approve.json | 194 ++ .../request_function_response_activity.json | 49 +- uv.lock | 29 +- 23 files changed, 3462 insertions(+), 4366 deletions(-) create mode 100644 functions_to_format/functions/base_strategy.py create mode 100644 functions_to_format/functions/transfer.py create mode 100644 share/approve.json diff --git a/.gitignore b/.gitignore index 9d0b48c..4164a3a 100644 --- a/.gitignore +++ b/.gitignore @@ -325,16 +325,11 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ temp -build*.json -*widget*.json -test_* -test_json/ -logs/json/ -logs/temp/ +logs/ +logs/ logs/usage/ *.ignore.* -a.json -a.py -share/ +components-check-list.md +jprq jprq_/ -jprq \ No newline at end of file +smarty_ui/ \ No newline at end of file diff --git a/functions_to_format/functions/__init__.py b/functions_to_format/functions/__init__.py index d35e438..c5c670b 100644 --- a/functions_to_format/functions/__init__.py +++ b/functions_to_format/functions/__init__.py @@ -1,7 +1,9 @@ from .functions import functions_mapper, sdui_functions_map +from .base_strategy import FunctionStrategy __all__ = [ "functions_mapper", "sdui_functions_map", + "FunctionStrategy", ] diff --git a/functions_to_format/functions/activity_report.py b/functions_to_format/functions/activity_report.py index 1a62403..fc39bf6 100644 --- a/functions_to_format/functions/activity_report.py +++ b/functions_to_format/functions/activity_report.py @@ -3,13 +3,30 @@ from pydantic import BaseModel from typing import Any, Dict, List from pydivkit.core import Expr +import structlog + +from functions_to_format.functions.balance import ( + build_balance_ui, + build_home_balances_ui, +) from .general import Widget, add_ui_to_widget, WidgetInput from .general.utils import save_builder_output from models.build import BuildOutput from functions_to_format.functions.general.const_values import LanguageOptions -from conf import logger from models.context import Context, LoggerContext -import structlog + +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + text_1, + text_2, + caption_1, + caption_2, + default_theme, +) + +logger = structlog.get_logger(__name__) # Backend output schemas for activity reports @@ -82,38 +99,48 @@ def _trigger_row( target_state: str, log_id: str, ) -> dv.DivContainer: - """Trigger row: message + chevron (like ReasoningTrigger). Tapping toggles state.""" + """Trigger row: message + chevron (like ReasoningTrigger). Tapping toggles state. + + Uses smarty_ui HStack for horizontal layout. + """ set_state_url = f"div-action://set_state?state_id=0/{state_id}/{target_state}" - return dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - background=[dv.DivSolidBackground(color=MESSAGE_BLOCK_BG)], - paddings=dv.DivEdgeInsets( - left=TRIGGER_PADDING_H, - right=TRIGGER_PADDING_H, - top=TRIGGER_PADDING_V, - bottom=TRIGGER_PADDING_V, - ), - border=dv.DivBorder( - corner_radius=TRIGGER_RADIUS, - stroke=dv.DivStroke(color="#4B5563", width=1), - ), - alignment_vertical=dv.DivAlignmentVertical.CENTER, - items=[ - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - width=dv.DivMatchParentSize(weight=1), - margins=dv.DivEdgeInsets(right=GAP_MESSAGE_CHEVRON), - items=[_message_glow_text(message)], - ), - dv.DivText( - text=chevron, - font_size=16, - text_color=CHEVRON_COLOR, - ), - ], - action=dv.DivAction(log_id=log_id, url=set_state_url), + + # Message container with flex weight + message_container = dv.DivContainer( + orientation=dv.DivContainerOrientation.VERTICAL, + width=dv.DivMatchParentSize(weight=1), + items=[_message_glow_text(message)], ) + # Chevron text + chevron_text = dv.DivText( + text=chevron, + font_size=16, + text_color=CHEVRON_COLOR, + ) + + # Use HStack for horizontal layout + trigger = HStack( + [message_container, chevron_text], + gap=GAP_MESSAGE_CHEVRON, + align_v="center", + padding_left=TRIGGER_PADDING_H, + padding_right=TRIGGER_PADDING_H, + padding_top=TRIGGER_PADDING_V, + padding_bottom=TRIGGER_PADDING_V, + background=MESSAGE_BLOCK_BG, + corner_radius=TRIGGER_RADIUS, + ) + + # Add border stroke and action manually (not supported by HStack helper) + trigger.border = dv.DivBorder( + corner_radius=TRIGGER_RADIUS, + stroke=dv.DivStroke(color="#4B5563", width=1), + ) + trigger.action = dv.DivAction(log_id=log_id, url=set_state_url) + + return trigger + def _build_activity_message_holder( message: str, @@ -122,7 +149,10 @@ def _build_activity_message_holder( log_id: str, action_url: str = "div-action://activity-report", ): - """Reasoning-like collapsible: trigger row (message + chevron) + expandable content (title + detail).""" + """Reasoning-like collapsible: trigger row (message + chevron) + expandable content (title + detail). + + Uses smarty_ui VStack for vertical layouts. + """ state_id = f"activity_{log_id}" # Collapsed: only trigger row with ▼ (expand) @@ -134,42 +164,30 @@ def _build_activity_message_holder( log_id=log_id, ) - # Expanded: trigger row with ▲ (collapse) + content below in a card - content_inner = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=title, - font_size=13, - text_color=CONTENT_TITLE_COLOR, - font_weight=dv.DivFontWeight.MEDIUM, - margins=dv.DivEdgeInsets(bottom=8), - ), - dv.DivText( - text=detail_json, - font_size=11, - text_color=CONTENT_DETAIL_COLOR, - line_height=18, - ), - ], + # Content inner using VStack + title_text = caption_1(title, color=CONTENT_TITLE_COLOR) + detail_text = caption_2(detail_json, color=CONTENT_DETAIL_COLOR) + + content_inner = VStack( + [title_text, detail_text], + gap=8, ) - content_block = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - margins=dv.DivEdgeInsets(top=SPACING_ABOVE_CONTENT), - paddings=dv.DivEdgeInsets( - left=CONTENT_PADDING, - right=CONTENT_PADDING, - top=CONTENT_PADDING, - bottom=CONTENT_PADDING, - ), - background=[dv.DivSolidBackground(color=CONTENT_BLOCK_BG)], - border=dv.DivBorder( - corner_radius=CONTENT_RADIUS, - stroke=dv.DivStroke(color=CONTENT_BLOCK_BORDER, width=1), - ), - transition_in=dv.DivFadeTransition(duration=200, alpha=0.5), - items=[content_inner], + + # Content block with styling + content_block = VStack( + [content_inner], + padding=CONTENT_PADDING, + background=CONTENT_BLOCK_BG, + corner_radius=CONTENT_RADIUS, + ) + content_block.margins = dv.DivEdgeInsets(top=SPACING_ABOVE_CONTENT) + content_block.border = dv.DivBorder( + corner_radius=CONTENT_RADIUS, + stroke=dv.DivStroke(color=CONTENT_BLOCK_BORDER, width=1), ) + content_block.transition_in = dv.DivFadeTransition(duration=200, alpha=0.5) + + # Expanded trigger expanded_trigger = _trigger_row( message=message, chevron="▲", @@ -177,13 +195,9 @@ def _build_activity_message_holder( target_state="collapsed", log_id=log_id, ) - expanded_div = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - expanded_trigger, - content_block, - ], - ) + + # Expanded div using VStack + expanded_div = VStack([expanded_trigger, content_block]) state = dv.DivState( id=state_id, @@ -193,11 +207,18 @@ def _build_activity_message_holder( dv.DivStateState(state_id="expanded", div=expanded_div), ], ) - wrapper = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - margins=dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8), - items=[state], + + widgets = [state] + + # Wrapper using VStack + wrapper = VStack( + widgets, + padding_left=12, + padding_right=12, + padding_top=8, + padding_bottom=8, ) + return dv.make_div(wrapper) @@ -205,8 +226,14 @@ def build_function_call_activity_widget( message: str, function_name: str, arguments: Dict[str, Any], + language: LanguageOptions = LanguageOptions.RUSSIAN, ): """Builder for function_call activity: shows message and call details (function_name, arguments).""" + logger.debug( + "building_function_call_activity_widget", + function_name=function_name, + message=message, + ) title = f"Calling: {function_name}" detail_json = json.dumps(arguments, ensure_ascii=False, default=str)[:500] if len(detail_json) >= 500: @@ -224,12 +251,19 @@ def build_function_response_activity_widget( message: str, function_name: str, response: Dict[str, Any], + language: LanguageOptions = LanguageOptions.RUSSIAN, ): """Builder for function_response activity: shows message and response (function_name, response).""" + logger.debug( + "building_function_response_activity_widget", + function_name=function_name, + message=message, + ) title = f"Response from: {function_name}" detail_json = json.dumps(response, ensure_ascii=False, default=str)[:500] if len(detail_json) >= 500: detail_json += "..." + return _build_activity_message_holder( message=message, title=title, @@ -240,20 +274,21 @@ def build_function_response_activity_widget( def build_activity_indicator_widget(message: str) -> ActivityIndicatorWidget: - container = dv.DivContainer( - id="activity_report", - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText(text=message, font_size=14, text_color="#F1F5F9"), - ], - action=dv.DivAction( - log_id="activity_report", - url="div-action://activity-report", - payload={ - "message": message, - "link": "http://10.212.134.3:8003/new_messages?idx=12341234", - }, - ), + """Build activity indicator widget using smarty_ui components.""" + logger.debug("building_activity_indicator_widget", message=message) + # Use text_1 for the message text + message_text = text_1(message, color="#F1F5F9") + + # Use VStack for vertical layout + container = VStack([message_text]) + container.id = "activity_report" + container.action = dv.DivAction( + log_id="activity_report", + url="div-action://activity-report", + payload={ + "message": message, + "link": "http://10.212.134.3:8003/new_messages?idx=12341234", + }, ) return dv.make_div(container) @@ -267,7 +302,9 @@ def activity_indicator(context: Context) -> BuildOutput: language = context.language chat_id = context.logger_context.chat_id api_key = context.api_key - logger = context.logger_context.logger + ctx_logger = context.logger_context.logger + + ctx_logger.info("activity_indicator_started", llm_output=llm_output) inp = {} @@ -291,5 +328,6 @@ def activity_indicator(context: Context) -> BuildOutput: widgets=[widget.model_dump(exclude_none=True) for widget in widgets], ) + ctx_logger.info("activity_indicator_completed", widgets_count=len(widgets)) save_builder_output(context, output) return output diff --git a/functions_to_format/functions/activity_report_events.py b/functions_to_format/functions/activity_report_events.py index 1d1e72a..0180c5b 100644 --- a/functions_to_format/functions/activity_report_events.py +++ b/functions_to_format/functions/activity_report_events.py @@ -1,8 +1,18 @@ -from .general import Widget, add_ui_to_widget, WidgetInput -from .general.utils import save_builder_output +"""Orchestration layer for function-call / function-response activity reports. + +* Pure UI builders live in ``activity_report.py``. +* This module wires context → UI builders and, for responses whose + ``function_name`` has a known handler, embeds the rich widget produced + by that handler alongside the activity report. +""" + +from typing import Callable, Dict, List, Optional + +from .general import Widget, WidgetInput from models.build import BuildOutput from models.context import Context +from .base_strategy import FunctionStrategy from .activity_report import ( FunctionCallBackendOutput, FunctionResponseBackendOutput, @@ -10,76 +20,181 @@ build_function_response_activity_widget, ) +# --------------------------------------------------------------------------- +# Lazy-loaded registry: function_name → handler(Context) → BuildOutput +# --------------------------------------------------------------------------- -def function_call_activity_record(context: Context) -> BuildOutput: - """Build UI for function_call activity: message from llm_output, function_name + arguments from backend_output.""" - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version +_response_ui_registry: Optional[Dict[str, Callable[[Context], BuildOutput]]] = None - data = FunctionCallBackendOutput( - function_name=backend_output.get("function_name", ""), - arguments=backend_output.get("arguments", {}), - ) - activity_widget = Widget( - name="function_call_activity_widget", - type="function_call_activity_widget", - order=1, - layout="vertical", - fields=["function_call_activity"], +def _get_response_ui_registry() -> Dict[str, Callable[[Context], BuildOutput]]: + """Return (and lazily initialise) the map of function names whose + response data can be turned into a rich widget by the existing handler. + + Lazy importing avoids circular dependencies with ``functions.py``.""" + global _response_ui_registry + if _response_ui_registry is not None: + return _response_ui_registry + + from .balance import get_balance, get_home_balances + from .weather import get_weather_info + from .news import get_news + from .notification import get_notifications + from .contact import get_contact + from .products import get_products, search_products + from .payment import ( + get_categories, + get_fields_of_supplier, + get_suppliers_by_category, ) - inp = { - build_function_call_activity_widget: WidgetInput( - widget=activity_widget, - args={ - "message": llm_output, - "function_name": data.function_name, - "arguments": data.arguments, - }, - ) + + _response_ui_registry = { + "get_balance": get_balance, + "get_home_balances": get_home_balances, + "get_weather_info": get_weather_info, + "get_categories": get_categories, + # "get_fields_of_supplier": get_fields_of_supplier, + "get_suppliers_by_category": get_suppliers_by_category, + # "get_news": get_news, + # "get_notifications": get_notifications, + # "get_contact": get_contact, + "get_products": get_products, + "search_products": search_products, } - widgets = add_ui_to_widget(widget_inputs=inp, version=version) - output = BuildOutput( - widgets_count=len(widgets), - widgets=[w.model_dump(exclude_none=True) for w in widgets], - ) - save_builder_output(context, output) - return output + return _response_ui_registry -def function_response_activity_record(context: Context) -> BuildOutput: - """Build UI for function_response activity: message from llm_output, function_name + response from backend_output.""" - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- - data = FunctionResponseBackendOutput( - function_name=backend_output.get("function_name", ""), - response=backend_output.get("response", {}), - ) - activity_widget = Widget( - name="function_response_activity_widget", - type="function_response_activity_widget", - order=1, - layout="vertical", - fields=["function_response_activity"], +def _try_build_embedded_ui( + function_name: str, + response: dict, + parent_context: Context, +) -> Optional[List[dict]]: + """If *function_name* has a known UI handler, call it with *response* as + ``backend_output`` and return its non-text widget dicts. + + * Text widgets are filtered out because the activity report already + shows the LLM message. + * Returns ``None`` when the function is not registered or the handler + fails (failures are logged, never raised). + """ + function_name = function_name.replace("_wrapper", "") + registry = _get_response_ui_registry() + handler = registry.get(function_name) + if handler is None: + return None + + synthetic_ctx = Context( + llm_output="", # empty → text widgets are skipped by add_ui_to_widget + backend_output=response, + version=parent_context.version, + language=parent_context.language, + api_key=parent_context.api_key, + logger_context=parent_context.logger_context, ) - inp = { - build_function_response_activity_widget: WidgetInput( - widget=activity_widget, - args={ - "message": llm_output, - "function_name": data.function_name, - "response": data.response, - }, + + try: + result: BuildOutput = handler(context=synthetic_ctx) + except Exception as exc: + parent_context.logger_context.logger.warning( + "embedded_ui_build_failed", + function_name=function_name, + error=str(exc), ) - } - widgets = add_ui_to_widget(widget_inputs=inp, version=version) - output = BuildOutput( - widgets_count=len(widgets), - widgets=[w.model_dump(exclude_none=True) for w in widgets], - ) - save_builder_output(context, output) - return output + return None + + # Keep only real UI widgets (drop text_widget duplicates) + embedded: List[dict] = [] + for w in result.widgets: + w_dict = w if isinstance(w, dict) else w.model_dump(exclude_none=True) + if w_dict.get("type") == "text_widget": + continue + embedded.append(w_dict) + + return embedded if embedded else None + + +# --------------------------------------------------------------------------- +# Public handlers (registered in functions_mapper) +# --------------------------------------------------------------------------- + + +class FunctionCallActivityRecord(FunctionStrategy): + """Strategy for function-call activity event UI.""" + + def build_widget_inputs(self, context): + data = FunctionCallBackendOutput( + function_name=context.backend_output.get("function_name", ""), + arguments=context.backend_output.get("arguments", {}), + ) + return { + build_function_call_activity_widget: WidgetInput( + widget=Widget( + name="function_call_activity_widget", + type="function_call_activity_widget", + order=1, + layout="vertical", + fields=["function_call_activity"], + ), + args={ + "message": context.llm_output, + "function_name": data.function_name, + "arguments": data.arguments, + "language": context.language, + }, + ), + } + + +function_call_activity_record = FunctionCallActivityRecord() + + +class FunctionResponseActivityRecord(FunctionStrategy): + """Strategy for function-response activity event UI. + + When function_name matches a known handler, embeds rich widgets + alongside the activity report. + """ + + def build_widget_inputs(self, context): + data = FunctionResponseBackendOutput( + function_name=context.backend_output.get("function_name", ""), + response=context.backend_output.get("response", {}), + ) + return { + build_function_response_activity_widget: WidgetInput( + widget=Widget( + name="function_response_activity_widget", + type="function_response_activity_widget", + order=1, + layout="vertical", + fields=["function_response_activity"], + ), + args={ + "message": context.llm_output, + "function_name": data.function_name, + "response": data.response, + "language": context.language, + }, + ), + } + + def execute(self, context): + data = FunctionResponseBackendOutput( + function_name=context.backend_output.get("function_name", ""), + response=context.backend_output.get("response", {}), + ) + embedded = _try_build_embedded_ui( + function_name=data.function_name, + response=data.response, + parent_context=context, + ) + widget_inputs = self.build_widget_inputs(context) + return self._build_and_save(context, widget_inputs, extra_widget_dicts=embedded) + + +function_response_activity_record = FunctionResponseActivityRecord() diff --git a/functions_to_format/functions/balance.py b/functions_to_format/functions/balance.py index f2a7266..781d323 100644 --- a/functions_to_format/functions/balance.py +++ b/functions_to_format/functions/balance.py @@ -2,26 +2,38 @@ from pydivkit.core import Expr import json from .general import ( - add_ui_to_widget, Widget, WidgetInput, - build_buttons_row, - build_text_widget, - TextWidget, - create_feedback_variables, - create_success_container, - create_error_container, ) -from .general.utils import save_builder_output from pydantic import BaseModel -from typing import List, Optional +from typing import Any, List, Optional from conf import logger -from models.build import BuildOutput -from models.context import Context, LoggerContext +from models.context import Context +from .base_strategy import FunctionStrategy from tool_call_models.cards import CardsBalanceResponse from tool_call_models.home_balance import HomeBalance from functions_to_format.functions.general.const_values import LanguageOptions -import structlog + +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + title_2, + text_1, + text_2, + caption_1, + icon, + avatar, + divider, + simple_card, + default_theme, +) + +# Import smarty_ui blocks for ready-made components +from smarty_ui.blocks import ( + cards_own_list as smarty_cards_own_list, + home_balance_widget as smarty_home_balance_widget, +) # Feedback texts for balance actions @@ -85,296 +97,170 @@ def format_balance(balance: int): return f"{balance:,}" -def build_balances_part(home_balance: HomeBalance): - # Services list - service_icons = { - "electricity": "https://smarty-test.smartbank.uz/ui_server/static/electricity.png", - "gas": "https://smarty-test.smartbank.uz/ui_server/static/gas.png", - "internet": "https://smarty-test.smartbank.uz/ui_server/static/trash.png", - "water": "https://smarty-test.smartbank.uz/ui_server/static/water.png", - "mobile": "https://smarty-test.smartbank.uz/ui_server/static/mobile.png", - "garbage": "https://smarty-test.smartbank.uz/ui_server/static/trash.png", - "coldWater": "https://smarty-test.smartbank.uz/ui_server/static/water.png", - "hotWater": "https://smarty-test.smartbank.uz/ui_server/static/water.png", - "heating": "https://smarty-test.smartbank.uz/ui_server/static/heating.png", - } - - items = [] - for service_name, icon_url in service_icons.items(): - if service_name not in home_balance.services: - continue - try: - items.append( - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - width=( - dv.DivFixedSize(value=150) - if service_name == "electricity" - else None - ), - height=dv.DivFixedSize(value=24), - items=[ - dv.DivImage( - image_url=icon_url, - width=dv.DivFixedSize(value=24), - height=dv.DivFixedSize(value=24), - ), - dv.DivText( - text=format_balance( - home_balance.services[service_name].balance * 100 - ), - font_size=16, - text_color="#666666", - margins=dv.DivEdgeInsets( - left=8 if service_name == "electricity" else 12 - ), - ), - ], - ) - ) - except KeyError: - pass - # Add separator after each service except the last one - if service_name != "internet": - items.append( - dv.DivContainer( - height=dv.DivFixedSize(value=1), - background=[dv.DivSolidBackground(color="#E5E5E5")], - margins=dv.DivEdgeInsets(bottom=4, top=4), - ) - ) - - return dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - margins=dv.DivEdgeInsets(top=8), - items=items, - ) - +def build_home_balances_ui_widget( + home_balance: HomeBalance, + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + """ + Build home balances UI using smarty_ui home_balance_widget block component. + """ + div = build_home_balances_ui(home_balance) + output = dv.make_div(div) -def build_home_balance_main_container(home_balance: HomeBalance): - return dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - width=dv.DivFixedSize(value=361), - height=dv.DivFixedSize(value=150), - background=[dv.DivSolidBackground(color="#EBF2FA")], - border=dv.DivBorder(corner_radius=20), - paddings=dv.DivEdgeInsets(left=16, right=16, top=12, bottom=12), - margins=dv.DivEdgeInsets(left=15), # Added left margin to move card right - items=[ - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text="Мой дом", - font_size=20, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#000000", - margins=dv.DivEdgeInsets(bottom=8), - ), - dv.DivImage( - image_url="https://smarty-test.smartbank.uz/ui_server/static/home.png", - width=dv.DivFixedSize(value=60), - height=dv.DivFixedSize(value=60), - ), - ], - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - margins=dv.DivEdgeInsets(left=64), - items=[ - build_balances_part(home_balance), - ], - ), - ], - ) + return output -def build_home_balances_ui(home_balance: HomeBalance): +def build_home_balances_ui(home_balance: HomeBalance | dict[str, Any]): """ - Build home balances UI using Yandex DivKit SDK. + Build home balances UI using smarty_ui home_balance_widget block component. Creates a card with house icon and utility service options. """ - container = build_home_balance_main_container(home_balance) - output = dv.make_div(container) - with open("logs/json/build_home_balances_ui.json", "w", encoding="utf-8") as f: - json.dump(output, f, indent=2, ensure_ascii=False) - return output + # Map service names to utility types for smarty_ui + service_type_map = { + "electricity": "electricity", + "gas": "gas", + "water": "water", + "coldWater": "water", + "hotWater": "water", + "garbage": "recycling", + "internet": "other", + "mobile": "other", + "heating": "other", + } + if isinstance(home_balance, dict): + home_balance = HomeBalance(**home_balance) + + # Convert home_balance services to smarty_ui UtilityBalanceData format + utilities = [] + for service_name, service_data in home_balance.services.items(): + utility_type = service_type_map.get(service_name, "other") + balance_value = service_data.balance * 100 # Convert to minor units + is_negative = balance_value < 0 + + utilities.append( + { + "utility_type": utility_type, + "balance": format_balance(abs(balance_value)), + "currency": "сум", + "is_negative": is_negative, + } + ) -def get_home_balances(context: Context) -> BuildOutput: - """ - Get home balances. - """ - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - if "services" in backend_output: - for k, v in backend_output["services"].items(): - backend_output[k] = v - del backend_output["services"] - - backend_data: HomeBalance = HomeBalance(**backend_output) - - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - home_Balance_Widget = Widget( - name="home_balance_widget", - type="home_balance_widget", - order=2, - layout="vertical", - fields=["homeName", "services"], - values=[backend_data.model_dump(exclude_none=True)], + # Use smarty_ui home_balance_widget block component + home_widget = smarty_home_balance_widget( + title=home_balance.homeName if home_balance.homeName else "Мой дом", + subtitle="Балансы", + utilities=utilities, + corner_radius=20, ) - widgets = add_ui_to_widget( - { - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, - ), - build_home_balances_ui: WidgetInput( - widget=home_Balance_Widget, + # Wrap with margins + container = VStack([home_widget]) + container.margins = dv.DivEdgeInsets(left=15) + + return container + + +class GetHomeBalances(FunctionStrategy): + """Strategy for building home balances UI.""" + + def build_widget_inputs(self, context): + backend_output = context.backend_output + if "services" in backend_output: + for k, v in backend_output["services"].items(): + backend_output[k] = v + del backend_output["services"] + + backend_data = HomeBalance(**backend_output) + text_builder, text_input = self.make_text_input(context.llm_output) + return { + text_builder: text_input, + build_home_balances_ui_widget: WidgetInput( + widget=Widget( + name="home_balance_widget", + type="home_balance_widget", + order=2, + layout="vertical", + fields=["homeName", "services"], + values=[backend_data.model_dump(exclude_none=True)], + ), args={"home_balance": backend_data}, ), - }, - version, - ) - output = BuildOutput( - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - widgets_count=len(widgets), - ) - save_builder_output(context, output) - return output + } -def get_balance(context: Context) -> BuildOutput: - """ - Process balance information and create UI widgets. +get_home_balances = GetHomeBalances() - Args: - context (Context): Context object containing llm_output, backend_output, version, etc. - Returns: - BuildOutput: Processed output with widgets - """ - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - logger.info(f"Processing balance request for chat_id: {chat_id}") - logger.debug(f"Backend output type: {type(backend_output)}") - - # Janis Rubins: use precompiled validator if available to avoid repeated validation overhead - # preprocess backend output: - output = [ - llm_output, - ] +class GetBalance(FunctionStrategy): + """Strategy for building balance UI.""" - try: - backend_data: CardsBalanceResponse = CardsBalanceResponse(**backend_output) + def build_widget_inputs(self, context): + logger = context.logger_context.logger logger.info( - f"Successfully parsed backend data with {len(backend_data.body[0].cardList)} cards" - ) - except Exception as e: - logger.error(f"Failed to parse backend output: {e}") - raise - - backend_output_processed: List[CardInfo] = [] - # logger.info(f"backend_output {backend_output} ----- type:{type(backend_output)}") - for i, card_info in enumerate(backend_data.body[0].cardList): - logger.debug(f"Processing card {i + 1}: {card_info.pan}") - - balance_value = 0 - if type(card_info.cardBalance.balance) is int: - balance_value = card_info.cardBalance.balance - logger.debug(f"Card {i + 1} balance (int): {balance_value}") - elif type(card_info.cardBalance.balance) is str: - balance_value = int(card_info.cardBalance.balance.replace(" ", "")) - logger.debug(f"Card {i + 1} balance (str->int): {balance_value}") - elif type(card_info.cardBalance.balance) is float: - balance_value = int(card_info.cardBalance.balance) - logger.debug(f"Card {i + 1} balance (float->int): {balance_value}") - - card_info_processed = CardInfo( - masked_card_pan=card_info.pan, - card_type=card_info.processingSystem, - balance=balance_value, - card_name=card_info.cardDetails.cardName, - cardColor=card_info.cardDetails.cardColor, - image_url=card_info.bankIcon.bankLogoMini, - ) - backend_output_processed.append(card_info_processed) - logger.debug( - f"Added card: {card_info_processed.card_name} with balance {balance_value}" + f"Processing balance request for chat_id: {context.logger_context.chat_id}" ) - logger.info(f"Processed {len(backend_output_processed)} cards successfully") - - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - cards_list = Widget( - name="cards_own_list_widget_balance", - type="cards_own_list_widget_balance", - order=2, - layout="vertical", - fields=["masked_card_pan", "card_type", "balance", "card_name"], - values=[ - card.model_dump(exclude_none=True) for card in backend_output_processed - ], - ) + backend_data = CardsBalanceResponse(**context.backend_output) + logger.info( + f"Successfully parsed backend data with {len(backend_data.body[0].cardList)} cards" + ) - logger.debug(f"Created widgets: text_widget (order=1), cards_list (order=2)") + backend_output_processed = [] + for i, card_info in enumerate(backend_data.body[0].cardList): + balance_value = 0 + if type(card_info.cardBalance.balance) is int: + balance_value = card_info.cardBalance.balance + elif type(card_info.cardBalance.balance) is str: + balance_value = int(card_info.cardBalance.balance.replace(" ", "")) + elif type(card_info.cardBalance.balance) is float: + balance_value = int(card_info.cardBalance.balance) + + backend_output_processed.append( + CardInfo( + masked_card_pan=card_info.pan, + card_type=card_info.processingSystem, + balance=balance_value, + card_name=card_info.cardDetails.cardName, + cardColor=card_info.cardDetails.cardColor, + image_url=card_info.bankIcon.bankLogoMini, + ) + ) - widgets = add_ui_to_widget( - { - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, - ), - build_balance_ui: WidgetInput( - widget=cards_list, + logger.info(f"Processed {len(backend_output_processed)} cards successfully") + + text_builder, text_input = self.make_text_input(context.llm_output) + return { + text_builder: text_input, + build_balance_ui_widget: WidgetInput( + widget=Widget( + name="cards_own_list_widget_balance", + type="cards_own_list_widget_balance", + order=2, + layout="vertical", + fields=["masked_card_pan", "card_type", "balance", "card_name"], + values=[ + card.model_dump(exclude_none=True) + for card in backend_output_processed + ], + ), args={ "balance_input": BalanceInput( cards=backend_output_processed, accounts=[] ), - "language": language, + "language": context.language, }, ), - }, - version, - ) + } - logger.info(f"Built {len(widgets)} UI widgets for version {version}") - output = BuildOutput( - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - widgets_count=len(widgets), - ) - - logger.info( - f"Balance processing completed successfully. Output contains {output.widgets_count} widgets" - ) - - save_builder_output(context, output) - return output +get_balance = GetBalance() def card_block(card: CardInfo, language: LanguageOptions = LanguageOptions.RUSSIAN): """ - Create a UI container for displaying card information. + Create a UI container for displaying card information using smarty_ui. Args: card (dict): Card data including image_url, balance, last_digits, and type @@ -394,43 +280,44 @@ def card_block(card: CardInfo, language: LanguageOptions = LanguageOptions.RUSSI }, } sum_text = texts_map[language]["sum"] - return dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivImage( - image_url=card.image_url, - width=dv.DivFixedSize(value=48), - height=dv.DivFixedSize(value=32), - scale=dv.DivImageScale.FIT, - # corner_radius=6, - margins=dv.DivEdgeInsets(right=12), - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=f"{card.balance // 100} {sum_text}", - font_size=16, - font_weight=dv.DivFontWeight.BOLD, - text_color="#111827", - ), - dv.DivText( - text=f"💳 •• {card.masked_card_pan} — {card.card_type}", - font_size=12, - text_color="#6B7280", - ), - ], - ), - ], - margins=dv.DivEdgeInsets(bottom=12), + + # Card image using smarty_ui icon + card_image = dv.DivImage( + image_url=card.image_url, + width=dv.DivFixedSize(value=48), + height=dv.DivFixedSize(value=32), + scale=dv.DivImageScale.FIT, + ) + + # Balance text using text_1 (bold) + balance_text = text_1(f"{card.balance // 100} {sum_text}", color="#111827") + balance_text.font_weight = dv.DivFontWeight.BOLD + + # Card details using text_2 + card_details = text_2( + f"💳 •• {card.masked_card_pan} — {card.card_type}", + color="#6B7280", + ) + + # Card info column using VStack + card_info = VStack([balance_text, card_details]) + + # Main row using HStack + card_row = HStack( + [card_image, card_info], + gap=12, + align_v="center", ) + card_row.margins = dv.DivEdgeInsets(bottom=12) + + return card_row def account_block( account: Account, language: LanguageOptions = LanguageOptions.RUSSIAN ): """ - Create a UI container for displaying account information. + Create a UI container for displaying account information using smarty_ui. Args: account (dict): Account data including image_url, balance, and type @@ -451,35 +338,35 @@ def account_block( } sum_text = texts_map[language]["sum"] - return dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivImage( - image_url=account.image_url, - width=dv.DivFixedSize(value=48), - height=dv.DivFixedSize(value=32), - scale=dv.DivImageScale.FIT, - # corner_radius=6, - margins=dv.DivEdgeInsets(right=12), - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=f"{account.balance} {sum_text}", - font_size=16, - font_weight=dv.DivFontWeight.BOLD, - text_color="#111827", - ), - dv.DivText( - text=f"💼 {account.type}", font_size=12, text_color="#6B7280" - ), - ], - ), - ], - margins=dv.DivEdgeInsets(bottom=12), + + # Account image + account_image = dv.DivImage( + image_url=account.image_url, + width=dv.DivFixedSize(value=48), + height=dv.DivFixedSize(value=32), + scale=dv.DivImageScale.FIT, ) + # Balance text using text_1 (bold) + balance_text = text_1(f"{account.balance} {sum_text}", color="#111827") + balance_text.font_weight = dv.DivFontWeight.BOLD + + # Account type using text_2 + account_type_text = text_2(f"💼 {account.type}", color="#6B7280") + + # Account info column using VStack + account_info = VStack([balance_text, account_type_text]) + + # Main row using HStack + account_row = HStack( + [account_image, account_info], + gap=12, + align_v="center", + ) + account_row.margins = dv.DivEdgeInsets(bottom=12) + + return account_row + def action_button( text: str, @@ -524,12 +411,28 @@ def action_button( ) -def build_balance_ui( +def build_balance_ui_widget( balance_input: BalanceInput, language: LanguageOptions = LanguageOptions.RUSSIAN, ): """ - Build the balance UI using backend data and LLM output. + Build the balance UI using smarty_ui cards_own_list block component. + """ + + div = build_balance_ui(balance_input, language) + output = dv.make_div(div) + with open("logs/json/build_balance_ui.json", "w", encoding="utf-8") as f: + json.dump(output, f, indent=2, ensure_ascii=False) + + return output + + +def build_balance_ui( + balance_input: BalanceInput | dict[str, Any], + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + """ + Build the balance UI using smarty_ui cards_own_list block component. Args: balance_input (BalanceInput): Pydantic model containing cards and accounts data @@ -538,6 +441,9 @@ def build_balance_ui( dict: Complete UI definition in DivKit format """ + if isinstance(balance_input, dict): + balance_input = BalanceInput(**balance_input) + logger.info("Build balance ui") cards = balance_input.cards texts_map = { @@ -546,80 +452,73 @@ def build_balance_ui( "accounts_title": "Баланс ваших счетов:", "text": "Вы можете пополнить счёт или перевести средства на другую карту.", "actions": ["Пополнить", "Перевести"], + "currency": "сум", }, LanguageOptions.ENGLISH: { "cards_title": "Your cards balance:", "accounts_title": "Your accounts balance:", "text": "You can top up your account or transfer money to another card.", "actions": ["Top up", "Transfer"], + "currency": "sum", }, LanguageOptions.UZBEK: { "cards_title": "Sizning kartalaringizning balansi:", "accounts_title": "Sizning hisoblaringizning balansi:", "text": "Siz hisobingizni to'ldirishingiz yoki boshqa kartaga pul o'tkazishingiz mumkin.", "actions": ["To'ldirish", "O'tkazish"], + "currency": "so'm", }, } - # Parse LLM output for text and actions - # text = "Вы можете пополнить счёт или перевести средства на другую карту." - # actions = ["Пополнить", "Перевести"] - text = texts_map[language]["text"] - actions = texts_map[language]["actions"] cards_title = texts_map[language]["cards_title"] - accounts_title = texts_map[language]["accounts_title"] - - main_items = [] + currency = texts_map[language]["currency"] if cards: - main_items.append( - dv.DivText( - text=cards_title, - font_size=13, - font_weight=dv.DivFontWeight.BOLD, - text_color="#374151", - margins=dv.DivEdgeInsets(bottom=8), + # Convert CardInfo to smarty_ui CardBalanceData format + smarty_cards = [] + for card in cards: + smarty_cards.append( + { + "balance": format_balance(card.balance // 100), + "card_name": card.card_name, + "card_thumbnail_url": card.image_url, + "card_last_digits": card.masked_card_pan[-4:] + if len(card.masked_card_pan) >= 4 + else card.masked_card_pan, + "currency": currency, + } ) + + # Use smarty_ui cards_own_list block component + cards_widget = smarty_cards_own_list( + cards=smarty_cards, + corner_radius=8, + padding=16, ) - main_items.extend([card_block(c, language) for c in cards]) - - # Сообщение и кнопки - # main_items.append( - # dv.DivContainer( - # orientation=dv.DivContainerOrientation.VERTICAL, - # background=[dv.DivSolidBackground(color="#F9FAFB")], - # paddings=dv.DivEdgeInsets(top=16, bottom=16, left=16, right=16), - # items=[ - # dv.DivText(text=text, font_size=13, text_color="#374151"), - # dv.DivContainer( - # orientation=dv.DivContainerOrientation.HORIZONTAL, - # items=[action_button(act) for act in actions], - # margins=dv.DivEdgeInsets(top=12), - # ), - # ], - # margins=dv.DivEdgeInsets(top=16), - # ) - # ) - - # Root контейнер - root = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=main_items, - width=dv.DivFixedSize(value=280), - # height=dv.DivFixedSize(value=248), - background=[dv.DivSolidBackground(color="#FFFFFF")], - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#E5E7EB", width=1) - ), - paddings=dv.DivEdgeInsets(top=16, bottom=16, left=16, right=16), - margins=dv.DivEdgeInsets(top=16, left=20), - ) - output = dv.make_div(root) - with open("logs/json/build_balance_ui.json", "w", encoding="utf-8") as f: - json.dump(output, f, indent=2, ensure_ascii=False) + # Add title above the cards widget + title_text = caption_1(cards_title, color="#374151") + title_text.font_weight = dv.DivFontWeight.BOLD + title_text.margins = dv.DivEdgeInsets(bottom=8) - return output + root = VStack( + [title_text, cards_widget], + padding=0, + width=dv.DivFixedSize(value=280), + ) + root.margins = dv.DivEdgeInsets(top=16, left=20) + else: + # Empty state + root = VStack( + [], + padding=16, + background="#FFFFFF", + corner_radius=8, + width=dv.DivFixedSize(value=280), + ) + root.margins = dv.DivEdgeInsets(top=16, left=20) + + return root if __name__ == "__main__": diff --git a/functions_to_format/functions/base_strategy.py b/functions_to_format/functions/base_strategy.py new file mode 100644 index 0000000..e0c81bf --- /dev/null +++ b/functions_to_format/functions/base_strategy.py @@ -0,0 +1,93 @@ +"""Strategy Design Pattern base for function handlers. + +The Strategy pattern separates the *what* (which widgets to build, how to +parse backend data) from the *how* (the common workflow of building output +and saving). + +Each function handler is a concrete strategy that only needs to implement +build_widget_inputs() to describe what widgets should be built. +The base class handles the rest: calling add_ui_to_widget, creating +BuildOutput, and calling save_builder_output. +""" + +from abc import ABC, abstractmethod +from typing import Callable, Dict, List, Optional + +from models.build import BuildOutput +from models.context import Context + +from .general import WidgetInput, TextWidget, add_ui_to_widget +from .general.text import build_text_widget +from .general.utils import save_builder_output + + +class FunctionStrategy(ABC): + """Abstract base for all function handler strategies. + + Subclasses implement build_widget_inputs to declare the mapping from + builder functions to their WidgetInput configurations. The common + execution workflow (widget building, output creation, persistence) is + handled by execute. + + Instances are callable, so they can be used directly in + functions_mapper as drop-in replacements for plain handler functions. + """ + + @abstractmethod + def build_widget_inputs( + self, context: Context + ) -> Dict[Callable, WidgetInput]: + """Return the widget-builder to WidgetInput mapping for this handler.""" + ... + + def execute(self, context: Context) -> BuildOutput: + """Run the common workflow: build widgets, assemble output, save. + + Override this method only when the handler needs non-standard + post-processing (e.g. embedding extra widgets from sub-handlers). + """ + widget_inputs = self.build_widget_inputs(context) + return self._build_and_save(context, widget_inputs) + + def __call__(self, context: Context) -> BuildOutput: + """Allow strategy instances to be called like plain functions.""" + return self.execute(context) + + # ------------------------------------------------------------------ + # Helpers available to subclasses + # ------------------------------------------------------------------ + + @staticmethod + def _build_and_save( + context: Context, + widget_inputs: Dict[Callable, WidgetInput], + extra_widget_dicts: Optional[List[dict]] = None, + ) -> BuildOutput: + """Shared helper: build widgets, assemble BuildOutput, save, return.""" + widgets = add_ui_to_widget(widget_inputs, context.version) + all_widgets: List[dict] = [ + w.model_dump(exclude_none=True) for w in widgets + ] + + if extra_widget_dicts: + base_order = len(all_widgets) + 1 + for idx, w_dict in enumerate(extra_widget_dicts): + all_widgets.append({**w_dict, "order": base_order + idx}) + + output = BuildOutput( + widgets_count=len(all_widgets), + widgets=all_widgets, + ) + save_builder_output(context, output) + return output + + @staticmethod + def make_text_input(llm_output: str, order: int = 1): + """Convenience: create a (builder, WidgetInput) pair for a text widget.""" + return ( + build_text_widget, + WidgetInput( + widget=TextWidget(order=order, values=[{"text": llm_output}]), + args={"text": llm_output}, + ), + ) diff --git a/functions_to_format/functions/buttons.py b/functions_to_format/functions/buttons.py index 1de7b8d..76584e9 100644 --- a/functions_to_format/functions/buttons.py +++ b/functions_to_format/functions/buttons.py @@ -3,6 +3,16 @@ import json from typing import List, Optional +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + text_1, + caption_1, + default_theme, +) +from smarty_ui.primitives import smarty_button + # Feedback texts for button actions BUTTON_FEEDBACK_TEXTS = { @@ -27,154 +37,124 @@ def build_buttons_row( include_feedback: bool = True, ): """ - Build a row of buttons with optional feedback handling. - + Build a row of buttons with optional feedback handling using smarty_ui smarty_button component. + Args: button_texts: List of button text labels language: Language code for feedback text (ru, en, uz) include_feedback: Whether to include success/error feedback containers - + Returns: DivContainer with buttons and optional feedback """ feedback_texts = BUTTON_FEEDBACK_TEXTS.get(language, BUTTON_FEEDBACK_TEXTS["en"]) - - button_items = [ - dv.DivText( - text=txt, - font_size=14, - text_color="#2563EB", - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#3B82F6") + + # Build button items using smarty_ui smarty_button + button_items = [] + for txt in button_texts: + # Create smarty_button with action_url for the main action + action_url = f"div-action://button/{txt.lower().replace(' ', '_')}" + btn = smarty_button(text=txt, action_url=action_url) + + # Add custom actions including feedback action + btn.actions = [ + dv.DivAction( + log_id=f"btn-{txt.lower().replace(' ', '_')}", + url=action_url, + payload={"button_text": txt, "action": txt.lower()}, ), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - height=dv.DivFixedSize(value=36), - paddings=dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8), - margins=dv.DivEdgeInsets(right=8), - actions=[ - dv.DivAction( - log_id=f"btn-{txt.lower().replace(' ', '_')}", - url=f"div-action://button/{txt.lower().replace(' ', '_')}", - payload={"button_text": txt, "action": txt.lower()}, - ), - # Success feedback action - dv.DivAction( - log_id=f"btn-{txt.lower().replace(' ', '_')}-success", - url="div-action://set_variable?name=simple_btn_success_visible&value=1", - ), - ], - ) - for txt in button_texts - ] - + # Success feedback action + dv.DivAction( + log_id=f"btn-{txt.lower().replace(' ', '_')}-success", + url="div-action://set_variable?name=simple_btn_success_visible&value=1", + ), + ] + btn.margins = dv.DivEdgeInsets(right=8) + button_items.append(btn) + if not include_feedback: - return dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=button_items, + return HStack(button_items) + + # Success feedback container using smarty_ui + success_icon = caption_1("✅") + success_icon.margins = dv.DivEdgeInsets(right=8) + + success_text = caption_1(feedback_texts["action_success"], color="#065F46") + success_text.font_weight = dv.DivFontWeight.MEDIUM + success_text.width = dv.DivMatchParentSize(weight=1) + + dismiss_success = caption_1("✕", color="#065F46") + dismiss_success.font_weight = dv.DivFontWeight.BOLD + dismiss_success.paddings = dv.DivEdgeInsets(left=8) + dismiss_success.actions = [ + dv.DivAction( + log_id="dismiss-simple-btn-success", + url="div-action://set_variable?name=simple_btn_success_visible&value=0", ) - - # Success feedback container - success_container = dv.DivContainer( - id="simple-btn-success", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{simple_btn_success_visible == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + ] + + success_container = HStack( + [success_icon, success_text, dismiss_success], + padding=8, + background="#ECFDF5", + corner_radius=8, width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=8), - paddings=dv.DivEdgeInsets(top=8, bottom=8, left=12, right=12), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=13, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["action_success"], - font_size=13, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=14, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-simple-btn-success", - url="div-action://set_variable?name=simple_btn_success_visible&value=0", - ) - ], - ), - ], ) - - # Error feedback container - error_container = dv.DivContainer( - id="simple-btn-error", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{simple_btn_error_visible == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + success_container.id = "simple-btn-success" + success_container.visibility = Expr( + "@{simple_btn_success_visible == 1 ? 'visible' : 'gone'}" + ) + success_container.margins = dv.DivEdgeInsets(top=8) + success_container.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#A7F3D0", width=1) + ) + + # Error feedback container using smarty_ui + error_icon = caption_1("⚠️") + error_icon.margins = dv.DivEdgeInsets(right=8) + + error_text = caption_1(feedback_texts["action_error"], color="#B91C1C") + error_text.font_weight = dv.DivFontWeight.MEDIUM + error_text.width = dv.DivMatchParentSize(weight=1) + + dismiss_error = caption_1("✕", color="#B91C1C") + dismiss_error.font_weight = dv.DivFontWeight.BOLD + dismiss_error.paddings = dv.DivEdgeInsets(left=8) + dismiss_error.actions = [ + dv.DivAction( + log_id="dismiss-simple-btn-error", + url="div-action://set_variable?name=simple_btn_error_visible&value=0", + ) + ] + + error_container = HStack( + [error_icon, error_text, dismiss_error], + padding=8, + background="#FEF2F2", + corner_radius=8, width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=8), - paddings=dv.DivEdgeInsets(top=8, bottom=8, left=12, right=12), - background=[dv.DivSolidBackground(color="#FEF2F2")], - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#FECACA", width=1) - ), - items=[ - dv.DivText( - text="⚠️", - font_size=13, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["action_error"], - font_size=13, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=14, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-simple-btn-error", - url="div-action://set_variable?name=simple_btn_error_visible&value=0", - ) - ], - ), - ], ) - - return dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - variables=[ - dv.IntegerVariable(name="simple_btn_success_visible", value=0), - dv.IntegerVariable(name="simple_btn_error_visible", value=0), - ], - items=[ - # Buttons row - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=button_items, - ), - # Feedback containers - success_container, - error_container, - ], + error_container.id = "simple-btn-error" + error_container.visibility = Expr( + "@{simple_btn_error_visible == 1 ? 'visible' : 'gone'}" + ) + error_container.margins = dv.DivEdgeInsets(top=8) + error_container.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#FECACA", width=1) ) + # Buttons row using HStack + buttons_row = HStack(button_items) + + # Main container using VStack + main_container = VStack([buttons_row, success_container, error_container]) + main_container.variables = [ + dv.IntegerVariable(name="simple_btn_success_visible", value=0), + dv.IntegerVariable(name="simple_btn_error_visible", value=0), + ] + + return dv.make_div(main_container) + if __name__ == "__main__": buttons = ["submit", "cancel"] diff --git a/functions_to_format/functions/chatbot_answer.py b/functions_to_format/functions/chatbot_answer.py index e7cab12..0e62d87 100644 --- a/functions_to_format/functions/chatbot_answer.py +++ b/functions_to_format/functions/chatbot_answer.py @@ -1,21 +1,33 @@ import json from typing import Any, Dict, List + +from smarty_ui.blocks import contacts_list from .general import ( - add_ui_to_widget, Widget, - TextWidget, ButtonsWidget, - build_buttons_row, - build_text_widget, WidgetInput, ) -from .general.utils import save_builder_output +from .buttons import build_buttons_row import pydivkit as dv from pydivkit.core import Expr -from models.build import BuildOutput from pydantic import BaseModel from .general.const_values import WidgetMargins, LanguageOptions from models.context import Context +from .base_strategy import FunctionStrategy + +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + title_2, + text_1, + text_2, + caption_1, + caption_2, + avatar, + default_theme, +) +from smarty_ui.composites import user_bubble, assistant_bubble # Feedback texts for contact actions @@ -35,54 +47,19 @@ } -def chatbot_answer(context: Context) -> BuildOutput: - # Janis Rubins: logic unchanged, just returns text_widget schema and llm_output as data +class ChatbotAnswer(FunctionStrategy): + """Strategy for chatbot answer - simple text widget.""" - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger + def build_widget_inputs(self, context): + text_builder, text_input = self.make_text_input(context.llm_output) + return {text_builder: text_input} - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - widgets = add_ui_to_widget( - { - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, - ), - }, - version, - ) - - output = BuildOutput( - widgets_count=1, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output +chatbot_answer = ChatbotAnswer() def unauthorized_response(context: Context): - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - return { - "data": llm_output, - } + return {"data": context.llm_output} class Contact(BaseModel): @@ -91,12 +68,54 @@ class Contact(BaseModel): phone: str +# ============================================================================ +# Contacts List UI using smarty_ui +# ============================================================================ + + +def build_contacts_list_ui( + contacts: List[Contact], + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Build contacts list UI using smarty_ui contacts_list component. + + Args: + contacts_data: List of contact dicts with 'name' and 'phone' keys + language: Language for localization + + Returns: + DivKit JSON for the contacts list UI + """ + # Use smarty_ui contacts_list component + contacts_data = [ + { + "name": f"{contact.first_name} {contact.last_name}", + "phone": contact.phone, + "action_url": f"divkit://send_contact?phone={contact.phone}", + "avatar_url": f"https://ui-avatars.com/api/?name={contact.first_name}+{contact.last_name}", + } + for contact in contacts + ] + + contacts_widget = contacts_list(contacts_data) + + # Wrap with margins + container = VStack([contacts_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) + + div = dv.make_div(container) + with open("logs/json/build_contacts_list_ui.json", "w") as f: + json.dump(div, f, ensure_ascii=False, indent=2) + return div + + def build_contacts_list_widget( contacts: list[Contact], language: LanguageOptions = LanguageOptions.RUSSIAN, ) -> Dict[str, Any]: """ - Build a contacts list widget with feedback handling for contact selection. + Build a contacts list widget using smarty_ui contacts_list component. Args: contacts: List of contact objects @@ -105,237 +124,70 @@ def build_contacts_list_widget( Returns: DivKit JSON for the contacts list widget """ - feedback_texts = CONTACT_FEEDBACK_TEXTS.get( - language, CONTACT_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] - ) - - items: List[dv.Div] = [ - dv.DivText( - text="Contacts List", - font_family="Manrope", - font_size=18, - font_weight=dv.DivFontWeight.BOLD, - text_color="#111133", - margins=dv.DivEdgeInsets(bottom=16), - ) + # Convert contacts to the format expected by smarty_ui contacts_list + contacts_data = [ + { + "name": f"{contact.first_name} {contact.last_name}", + "phone": contact.phone, + "action_url": f"divkit://send_contact?phone={contact.phone}&first_name={contact.first_name}&last_name={contact.last_name}", + "avatar_url": f"https://ui-avatars.com/api/?name={contact.first_name}+{contact.last_name}", + } + for contact in contacts ] - for idx, contact in enumerate(contacts): - contact_cell = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - paddings=dv.DivEdgeInsets(left=16, right=16, top=8, bottom=8), - border=dv.DivBorder( - stroke=dv.DivStroke(color="#E0E0E0"), - ), - items=[ - dv.DivText( - text=f"{contact.first_name} {contact.last_name}", - font_family="Manrope", - font_size=16, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#111133", - line_height=20, - letter_spacing=0, - ), - dv.DivText( - text=contact.phone, - font_family="Manrope", - font_size=14, - font_weight=dv.DivFontWeight.REGULAR, - text_color="#808080", - line_height=20, - letter_spacing=0, - ), - ], - actions=[ - # Main selection action - dv.DivAction( - log_id=f"send_contact_{contact.phone.replace('+', '').replace(' ', '')}", - url="divkit://send_contact", - payload={ - "first_name": contact.first_name, - "last_name": contact.last_name, - "phone": contact.phone, - }, - ), - # Success feedback action - dv.DivAction( - log_id=f"send_contact_{idx}_success", - url="div-action://set_variable?name=contact_selection_success&value=1", - ), - ], - ) - items.append(contact_cell) - - # Success feedback container - success_container = dv.DivContainer( - id="contact-selection-success", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{contact_selection_success == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12, bottom=4), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["contact_selected"], - font_size=13, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-contact-selection-success", - url="div-action://set_variable?name=contact_selection_success&value=0", - ) - ], - ), - ], + # Use smarty_ui contacts_list component + contacts_widget = contacts_list( + contacts=contacts_data, + show_index=True, + corner_radius=8, + padding=16, + item_spacing=12, ) - # Error feedback container - error_container = dv.DivContainer( - id="contact-selection-error", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{contact_selection_error == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12, bottom=4), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#FEF2F2")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#FECACA", width=1) - ), - items=[ - dv.DivText( - text="⚠️", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["contact_error"], - font_size=13, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-contact-selection-error", - url="div-action://set_variable?name=contact_selection_error&value=0", - ) - ], - ), - ], + # Wrap with margins + container = VStack([contacts_widget]) + container.margins = dv.DivEdgeInsets( + top=WidgetMargins.TOP.value, + left=WidgetMargins.LEFT.value, + right=WidgetMargins.RIGHT.value, + bottom=WidgetMargins.BOTTOM.value, ) - items.append(success_container) - items.append(error_container) - - container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=items, - variables=[ - dv.IntegerVariable(name="contact_selection_success", value=0), - dv.IntegerVariable(name="contact_selection_error", value=0), - ], - margins=dv.DivEdgeInsets( - top=WidgetMargins.TOP.value, - left=WidgetMargins.LEFT.value, - right=WidgetMargins.RIGHT.value, - bottom=WidgetMargins.BOTTOM.value, - ), - paddings=dv.DivEdgeInsets(left=16, right=16, top=8, bottom=8), - ) - container = dv.make_div(container) + result = dv.make_div(container) with open("logs/json/build_contacts.json", "w") as f: - json.dump(container, f, indent=4) - return container - - -def build_contacts_list(context: Context) -> BuildOutput: - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - contacts_list: list[Contact] = [] - - contact_widget = Widget( - order=1, - name="contacts_list", - type="contacts_list", - layout="horizontal", - fields=["contacts"], - values=[{"contacts": contacts_list}], - ) - # text_widget = TextWidget( - # order=1, - # values=[{"text": llm_output}], - # ) - buttons = ButtonsWidget( - order=2, - values=[{"text": "cancel"}], - ) + json.dump(result, f, indent=4) + return result + + +class BuildContactsList(FunctionStrategy): + """Strategy for building contacts list UI.""" - for contact in backend_output: - contacts_list.append( + def build_widget_inputs(self, context): + contacts_list = [ Contact( - first_name=contact["first_name"], - last_name=contact["last_name"], - phone=contact["phone"], + first_name=c["first_name"], + last_name=c["last_name"], + phone=c["phone"], ) - ) - - widgets = add_ui_to_widget( - { + for c in context.backend_output + ] + return { build_contacts_list_widget: WidgetInput( - widget=contact_widget, - args={ - "contacts": contacts_list, - }, + widget=Widget( + order=1, + name="contacts_list", + type="contacts_list", + layout="horizontal", + fields=["contacts"], + values=[{"contacts": contacts_list}], + ), + args={"contacts": contacts_list}, ), build_buttons_row: WidgetInput( - widget=buttons, - args={"button_texts": ["cancel"], "language": language}, + widget=ButtonsWidget(order=2, values=[{"text": "cancel"}]), + args={"button_texts": ["cancel"], "language": context.language}, ), - # build_text_widget: WidgetInput( - # widget=text_widget, - # args={"text": llm_output}, - # ), - }, - version, - ) + } - output = BuildOutput( - widgets_count=1, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output + +build_contacts_list = BuildContactsList() diff --git a/functions_to_format/functions/contact.py b/functions_to_format/functions/contact.py index ca16359..a0b7e71 100644 --- a/functions_to_format/functions/contact.py +++ b/functions_to_format/functions/contact.py @@ -3,15 +3,24 @@ import json from pydantic import BaseModel from typing import List, Optional -from .general import Widget, add_ui_to_widget -from .general.utils import save_builder_output -from models.build import BuildOutput +from .general import Widget, WidgetInput from .general.const_values import WidgetMargins, LanguageOptions -from .general import WidgetInput -from conf import logger -import structlog - from models.context import Context, LoggerContext +from .base_strategy import FunctionStrategy + +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + title_2, + text_1, + text_2, + caption_1, + avatar, + default_theme, +) +# Import smarty_ui blocks for ready-made components +from smarty_ui.blocks import contacts_list as smarty_contacts_list # Feedback texts for contact widget actions @@ -34,42 +43,29 @@ } -def get_contact(context: Context) -> BuildOutput: - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - widget = Widget( - name="contact_widget", - type="contact_widget", - order=1, - layout="horizontal", - fields=["name", "avatar_url", "subtitle"], - ) - widgets = add_ui_to_widget( - { +class GetContact(FunctionStrategy): + """Strategy for building contact UI.""" + + def build_widget_inputs(self, context): + return { build_contact_widget: WidgetInput( - widget=widget, + widget=Widget( + name="contact_widget", + type="contact_widget", + order=1, + layout="horizontal", + fields=["name", "avatar_url", "subtitle"], + ), args={ - "backend_output": backend_output, - "llm_output": llm_output, + "backend_output": context.backend_output, + "llm_output": context.llm_output, "context": context.logger_context, }, - ) - }, - version, - ) - output = BuildOutput( - widgets_count=1, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output + ), + } + + +get_contact = GetContact() def contact_widget( @@ -81,8 +77,8 @@ def contact_widget( language: LanguageOptions = LanguageOptions.RUSSIAN, ): """ - Create a contact widget with optional click handling and feedback. - + Create a contact widget with optional click handling and feedback using smarty_ui. + Args: name: Contact name avatar_url: Contact avatar image URL @@ -90,103 +86,69 @@ def contact_widget( clickable: Whether the contact is clickable contact_id: Optional contact identifier for action payload language: Language for localization - + Returns: DivContainer for the contact widget """ - feedback_texts = CONTACT_WIDGET_FEEDBACK_TEXTS.get(language, CONTACT_WIDGET_FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) - - base_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivImage( - image_url=avatar_url, - width=dv.DivFixedSize(value=40), - height=dv.DivFixedSize(value=40), - scale=dv.DivImageScale.FILL, - margins=dv.DivEdgeInsets(right=12), - border=dv.DivBorder(corner_radius=20), - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=name, - font_size=14, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#111827", - ), - dv.DivText(text=subtitle, font_size=12, text_color="#6B7280"), - ], - ), - ], - paddings=dv.DivEdgeInsets(top=12, bottom=12, right=12, left=12), - background=[dv.DivSolidBackground(color="#FFFFFF")], - border=dv.DivBorder(corner_radius=12, stroke=dv.DivStroke(color="#E5E7EB")), + feedback_texts = CONTACT_WIDGET_FEEDBACK_TEXTS.get( + language, CONTACT_WIDGET_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) + + # Avatar using smarty_ui avatar component + contact_avatar = avatar(avatar_url, size=40) + + # Name text using text_1 + name_text = text_1(name, color="#111827") + name_text.font_weight = dv.DivFontWeight.MEDIUM + name_text.width = dv.DivWrapContentSize() + + # Subtitle using text_2 + subtitle_text = text_2(subtitle, color="#6B7280") + subtitle_text.width = dv.DivWrapContentSize() + + # Contact info column using VStack + contact_info = VStack([name_text, subtitle_text]) + + # Main row using HStack + base_container = HStack( + [contact_avatar, contact_info], + gap=12, + align_v="center", + padding=12, + background="#FFFFFF", + corner_radius=12, width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets( - bottom=WidgetMargins.BOTTOM.value, - top=WidgetMargins.TOP.value, - left=WidgetMargins.LEFT.value, - right=WidgetMargins.RIGHT.value, - ), ) - + base_container.border = dv.DivBorder( + corner_radius=12, stroke=dv.DivStroke(color="#E5E7EB") + ) + base_container.margins = dv.DivEdgeInsets( + bottom=WidgetMargins.BOTTOM.value, + top=WidgetMargins.TOP.value, + left=WidgetMargins.LEFT.value, + right=WidgetMargins.RIGHT.value, + ) + if clickable: - safe_name = name.replace(' ', '_').replace('+', '') - base_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivImage( - image_url=avatar_url, - width=dv.DivFixedSize(value=40), - height=dv.DivFixedSize(value=40), - scale=dv.DivImageScale.FILL, - margins=dv.DivEdgeInsets(right=12), - border=dv.DivBorder(corner_radius=20), - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=name, - font_size=14, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#111827", - ), - dv.DivText(text=subtitle, font_size=12, text_color="#6B7280"), - ], - ), - ], - paddings=dv.DivEdgeInsets(top=12, bottom=12, right=12, left=12), - background=[dv.DivSolidBackground(color="#FFFFFF")], - border=dv.DivBorder(corner_radius=12, stroke=dv.DivStroke(color="#E5E7EB")), - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets( - bottom=WidgetMargins.BOTTOM.value, - top=WidgetMargins.TOP.value, - left=WidgetMargins.LEFT.value, - right=WidgetMargins.RIGHT.value, + safe_name = name.replace(" ", "_").replace("+", "") + base_container.actions = [ + # Main contact action + dv.DivAction( + log_id=f"contact_select_{safe_name}", + url=f"divkit://contact/select", + payload={ + "contact_name": name, + "contact_id": contact_id or safe_name, + "subtitle": subtitle, + }, ), - actions=[ - # Main contact action - dv.DivAction( - log_id=f"contact_select_{safe_name}", - url=f"divkit://contact/select", - payload={ - "contact_name": name, - "contact_id": contact_id or safe_name, - "subtitle": subtitle, - }, - ), - # Success feedback action - dv.DivAction( - log_id=f"contact_select_{safe_name}_success", - url="div-action://set_variable?name=contact_widget_success&value=1", - ), - ], - ) - + # Success feedback action + dv.DivAction( + log_id=f"contact_select_{safe_name}_success", + url="div-action://set_variable?name=contact_widget_success&value=1", + ), + ] + return base_container @@ -229,104 +191,110 @@ def make_contacts_list( language: LanguageOptions = LanguageOptions.RUSSIAN, ): """ - Create a contacts list container with optional click handling and feedback. - + Create a contacts list container using smarty_ui contacts_list block component. + Args: contacts_data: List of contact data objects title: List title clickable: Whether contacts are clickable language: Language for localization - + Returns: DivContainer for the contacts list """ - feedback_texts = CONTACT_WIDGET_FEEDBACK_TEXTS.get(language, CONTACT_WIDGET_FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) - - items: List[dv.Div] = [ - dv.DivText( - text=title, - font_size=16, - font_weight=dv.DivFontWeight.BOLD, - text_color="#111827", - margins=dv.DivEdgeInsets(bottom=12), - ) - ] + feedback_texts = CONTACT_WIDGET_FEEDBACK_TEXTS.get( + language, CONTACT_WIDGET_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) + # Convert Contact objects to ContactData format expected by smarty_ui + smarty_contacts = [] for idx, contact in enumerate(contacts_data): - items.append( - contact_widget( - name=contact.name, - avatar_url=contact.avatar_url, - subtitle=contact.subtitle, - clickable=clickable, - contact_id=str(idx), - language=language, - ) - ) + contact_data = { + "name": contact.name, + "phone": contact.subtitle, # subtitle is used as phone in our model + } + if contact.avatar_url: + contact_data["avatar_url"] = contact.avatar_url + if clickable: + # Add action URL for clickable contacts + safe_name = contact.name.replace(" ", "_").replace("+", "") + contact_data["action_url"] = f"divkit://contact/select?contact_id={idx}&name={safe_name}" + smarty_contacts.append(contact_data) + + # Use smarty_ui contacts_list block component + contacts_widget = smarty_contacts_list( + contacts=smarty_contacts, + show_index=True, + corner_radius=16, + padding=16, + ) if clickable: - # Success feedback container - success_container = dv.DivContainer( - id="contacts-list-success", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{contact_widget_success == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + # Wrap with title and feedback containers + title_text = title_2(title, color="#111827") + title_text.margins = dv.DivEdgeInsets(bottom=12) + + # Success feedback container using HStack + success_icon = text_1("✅") + success_icon.margins = dv.DivEdgeInsets(right=8) + + success_text = caption_1(feedback_texts["contact_selected"], color="#065F46") + success_text.width = dv.DivMatchParentSize(weight=1) + + dismiss_btn = text_1("✕", color="#065F46") + dismiss_btn.font_weight = dv.DivFontWeight.BOLD + dismiss_btn.paddings = dv.DivEdgeInsets(left=8) + dismiss_btn.actions = [ + dv.DivAction( + log_id="dismiss-contacts-list-success", + url="div-action://set_variable?name=contact_widget_success&value=0", + ) + ] + + success_container = HStack( + [success_icon, success_text, dismiss_btn], + align_v="center", + align_h="center", + padding_top=10, + padding_bottom=10, + padding_left=12, + padding_right=12, + background="#ECFDF5", + corner_radius=10, width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["contact_selected"], - font_size=13, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-contacts-list-success", - url="div-action://set_variable?name=contact_widget_success&value=0", - ) - ], - ), - ], ) - items.append(success_container) - - return dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=items, - paddings=dv.DivEdgeInsets(all=16), - background=[dv.DivSolidBackground(color="#F9FAFB")], - border=dv.DivBorder(corner_radius=16), + success_container.id = "contacts-list-success" + success_container.visibility = Expr( + "@{contact_widget_success == 1 ? 'visible' : 'gone'}" + ) + success_container.border = dv.DivBorder( + corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) + ) + success_container.margins = dv.DivEdgeInsets(top=12) + + # Main container using VStack + container = VStack( + [title_text, contacts_widget, success_container], + padding=16, + background="#F9FAFB", + corner_radius=16, width=dv.DivMatchParentSize(), - variables=[ - dv.IntegerVariable(name="contact_widget_success", value=0), - dv.IntegerVariable(name="contact_widget_error", value=0), - ], ) - - return dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=items, - paddings=dv.DivEdgeInsets(all=16), - background=[dv.DivSolidBackground(color="#F9FAFB")], - border=dv.DivBorder(corner_radius=16), + container.variables = [ + dv.IntegerVariable(name="contact_widget_success", value=0), + dv.IntegerVariable(name="contact_widget_error", value=0), + ] + return container + + # Non-clickable: just wrap with title + title_text = title_2(title, color="#111827") + title_text.margins = dv.DivEdgeInsets(bottom=12) + + return VStack( + [title_text, contacts_widget], + padding=16, + background="#F9FAFB", + corner_radius=16, width=dv.DivMatchParentSize(), ) diff --git a/functions_to_format/functions/functions.py b/functions_to_format/functions/functions.py index 78bde7e..f0ba380 100644 --- a/functions_to_format/functions/functions.py +++ b/functions_to_format/functions/functions.py @@ -1,18 +1,28 @@ +"""Function registry: maps function names to Strategy handler instances. + +Each handler is a ``FunctionStrategy`` instance (callable) that implements +the Strategy Design Pattern. The ``functions_mapper`` dict is the central +dispatch table used at runtime. The ``sdui_functions_map`` maps function +names to their optional pydivkit UI builder functions. +""" + from .chatbot_answer import chatbot_answer, unauthorized_response, build_contacts_list from .balance import get_balance, build_balance_ui, get_home_balances -from .payment import ( +from .transfer import ( get_receiver_id_by_receiver_phone_number, - get_categories, - get_fields_of_supplier, - get_suppliers_by_category, get_number_by_receiver_name, send_money_to_someone_via_card, - pay_for_home_utility, - get_home_utility_suppliers, get_receiver_by_card, build_receiver_by_card_ui, build_send_money_ui, ) +from .payment import ( + get_categories, + get_fields_of_supplier, + get_suppliers_by_category, + pay_for_home_utility, + get_home_utility_suppliers, +) from .contact import build_contact_widget, get_contact from .news import build_news_widget, get_news from .notification import build_notifications_widget, get_notifications diff --git a/functions_to_format/functions/general/text.py b/functions_to_format/functions/general/text.py index 75b33c2..034568f 100644 --- a/functions_to_format/functions/general/text.py +++ b/functions_to_format/functions/general/text.py @@ -1,3 +1,4 @@ +from smarty_ui.composites import assistant_bubble from models.widget import Widget import pydivkit as dv import json @@ -15,35 +16,7 @@ def text_widget( text: str, ): text = text.replace("*", "") - return dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - background=[dv.DivSolidBackground(color="#F8FAFF")], # light bluish-white - border=dv.DivBorder(corner_radius=16), - paddings=dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16), - items=[ - dv.DivText( - text=text + "\n", - font_family="Manrope", - font_size=14, - font_weight=dv.DivFontWeight.LIGHT, - text_color="#111133", # dark navy color - line_height=22, - letter_spacing=0, - # max_lines=0, # Allow unlimited lines - text_alignment_horizontal=dv.DivAlignmentHorizontal.LEFT, - text_alignment_vertical=dv.DivAlignmentVertical.TOP, - ) - ], - margins=dv.DivEdgeInsets( - top=WidgetMargins.TOP.value, - left=WidgetMargins.LEFT.value, - right=WidgetMargins.RIGHT.value, - bottom=WidgetMargins.BOTTOM.value, - ), - width=dv.DivMatchParentSize(), - height=dv.DivWrapContentSize(), - # item_spacing=10, # gap between items - ) + return assistant_bubble(text) def build_text_widget(text: str): diff --git a/functions_to_format/functions/human_approval.py b/functions_to_format/functions/human_approval.py index 946c875..5c37a05 100644 --- a/functions_to_format/functions/human_approval.py +++ b/functions_to_format/functions/human_approval.py @@ -3,14 +3,10 @@ from pydivkit.core import Expr from functions_to_format.functions.buttons import build_buttons_row from .general import ( - add_ui_to_widget, Widget, - TextWidget, - build_text_widget, WidgetInput, ) -from .general.utils import save_builder_output -from models.build import BuildOutput +from .base_strategy import FunctionStrategy from conf import logger from functions_to_format.functions.general.const_values import ( @@ -21,6 +17,20 @@ from models.context import Context import pydivkit as dv +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + title_2, + text_1, + text_2, + caption_1, + caption_2, + primary_button, + secondary_button, + default_theme, +) + logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__) @@ -76,228 +86,191 @@ def human_approval_ui( reject_link_length=len(reject_link), ) - # "tool_call_id": "tool_send_money_to_someone_via_card_wrapper_jD3yQhOEG53QTSWiKUEf", - # "user_id": "60005982", - # "session_id": "a96d3fa0-a3c8-43cd-9090-f55afb285ff9", - # "app_name": "smarty", - # "created_at": { - # "seconds": 1770202179 - # }, + # Build approval info text using smarty_ui + info_text = text_1( + "Please approve the action. \n" + + "user_id: " + + human_approval_input["human_approval_event"]["user_id"] + + "\n" + + "session_id: " + + human_approval_input["human_approval_event"]["session_id"] + + "\n" + + "app_name: " + + human_approval_input["human_approval_event"]["app_name"] + + "\n", + color="#111133", + ) + info_text.font_weight = dv.DivFontWeight.LIGHT + info_text.line_height = 22 + info_text.text_alignment_horizontal = dv.DivAlignmentHorizontal.LEFT + info_text.text_alignment_vertical = dv.DivAlignmentVertical.TOP - # based on the Human Approval Requests - # first lets make some button for approve and reject actually - text_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - background=[dv.DivSolidBackground(color="#F8FAFF")], # light bluish-white - border=dv.DivBorder(corner_radius=16), - paddings=dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16), - items=[ - dv.DivText( - text="Please approve the action. \n" - + "user_id: " - + human_approval_input["human_approval_event"]["user_id"] - + "\n" - + "session_id: " - + human_approval_input["human_approval_event"]["session_id"] - + "\n" - + "app_name: " - + human_approval_input["human_approval_event"]["app_name"] - + "\n" - + "\n", - font_family="Manrope", - font_size=14, - font_weight=dv.DivFontWeight.LIGHT, - text_color="#111133", # dark navy color - line_height=22, - letter_spacing=0, - # max_lines=0, # Allow unlimited lines - text_alignment_horizontal=dv.DivAlignmentHorizontal.LEFT, - text_alignment_vertical=dv.DivAlignmentVertical.TOP, - ) - ], - margins=dv.DivEdgeInsets( - top=WidgetMargins.TOP.value, - left=WidgetMargins.LEFT.value, - right=WidgetMargins.RIGHT.value, - bottom=WidgetMargins.BOTTOM.value, - ), + # Text container using VStack + text_container = VStack( + [info_text], + padding=16, + background="#F8FAFF", + corner_radius=16, width=dv.DivMatchParentSize(), - height=dv.DivWrapContentSize(), - # item_spacing=10, # gap between items - variables=[ - dv.IntegerVariable(name="success_visible", value=0), - dv.IntegerVariable(name="error_visible", value=0), - dv.StringVariable(name="error_text", value=""), - ], ) - # Success container for approval feedback - success_container = dv.DivContainer( - id="success-container", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{success_visible == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + text_container.height = dv.DivWrapContentSize() + text_container.margins = dv.DivEdgeInsets( + top=WidgetMargins.TOP.value, + left=WidgetMargins.LEFT.value, + right=WidgetMargins.RIGHT.value, + bottom=WidgetMargins.BOTTOM.value, + ) + text_container.variables = [ + dv.IntegerVariable(name="success_visible", value=0), + dv.IntegerVariable(name="error_visible", value=0), + dv.StringVariable(name="error_text", value=""), + ] + + # Success container using smarty_ui + success_text = text_1("✅ Action completed successfully!", color="#065F46") + success_text.font_weight = dv.DivFontWeight.MEDIUM + success_text.line_height = 20 + + success_container = HStack( + [success_text], + padding=12, + background="#ECFDF5", + corner_radius=10, width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12, left=16, right=16, bottom=8), - paddings=dv.DivEdgeInsets(top=12, bottom=12, left=14, right=14), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅ Action completed successfully!", - font_size=14, - font_family="Manrope", - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - line_height=20, - ) - ], ) + success_container.id = "success-container" + success_container.visibility = Expr( + "@{success_visible == 1 ? 'visible' : 'gone'}" + ) + success_container.margins = dv.DivEdgeInsets( + top=12, left=16, right=16, bottom=8 + ) + success_container.border = dv.DivBorder( + corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) + ) + + # Error container using smarty_ui + error_icon = caption_1("⚠️") + error_icon.font_size = 16 + error_icon.margins = dv.DivEdgeInsets(right=10) + + error_msg = text_1("@{error_text}", color="#B91C1C") + error_msg.font_weight = dv.DivFontWeight.MEDIUM + error_msg.line_height = 20 + error_msg.width = dv.DivMatchParentSize(weight=1) - # Error container for approval feedback - error_container = dv.DivContainer( - id="error-container", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{error_visible == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + dismiss_error = caption_1("✕", color="#B91C1C") + dismiss_error.font_size = 18 + dismiss_error.font_weight = dv.DivFontWeight.BOLD + dismiss_error.paddings = dv.DivEdgeInsets(left=10, right=4) + dismiss_error.actions = [ + dv.DivAction( + log_id="dismiss-error", + url="div-action://set_variable?name=error_visible&value=0", + ) + ] + + error_container = HStack( + [error_icon, error_msg, dismiss_error], + padding=12, + background="#FEF2F2", + corner_radius=10, width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12, left=16, right=16, bottom=8), - paddings=dv.DivEdgeInsets(top=12, bottom=12, left=14, right=14), - background=[dv.DivSolidBackground(color="#FEF2F2")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#FECACA", width=1) - ), - items=[ - dv.DivText( - text="⚠️", - font_size=16, - margins=dv.DivEdgeInsets(right=10), - ), - dv.DivText( - text="@{error_text}", - font_size=14, - font_family="Manrope", - text_color="#B91C1C", - font_weight=dv.DivFontWeight.MEDIUM, - line_height=20, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=18, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=10, right=4), - actions=[ - dv.DivAction( - log_id="dismiss-error", - url="div-action://set_variable?name=error_visible&value=0", - ) - ], - ), - ], + ) + error_container.id = "error-container" + error_container.visibility = Expr("@{error_visible == 1 ? 'visible' : 'gone'}") + error_container.margins = dv.DivEdgeInsets(top=12, left=16, right=16, bottom=8) + error_container.border = dv.DivBorder( + corner_radius=10, stroke=dv.DivStroke(color="#FECACA", width=1) ) - buttons_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text="Approve", - id="btn-approve", - font_size=14, - text_color="#2563EB", - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#3B82F6") + # Approve button using smarty_ui text component with actions + approve_btn = text_1("Approve", color="#2563EB") + approve_btn.id = "btn-approve" + approve_btn.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#3B82F6") + ) + approve_btn.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + approve_btn.height = dv.DivFixedSize(value=36) + approve_btn.paddings = dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8) + approve_btn.margins = dv.DivEdgeInsets(right=8) + approve_btn.actions = [ + dv.DivAction( + url="divkit://send-request", + log_id="btn-approve", + typed=dv.DivActionSubmit( + container_id="btn-approve", + request=dv.DivActionSubmitRequest( + url=approve_link, + method=dv.RequestMethod.GET, + headers=[dv.RequestHeader(name="api-key", value=api_key)], ), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - height=dv.DivFixedSize(value=36), - paddings=dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8), - margins=dv.DivEdgeInsets(right=8), - actions=[ + on_success_actions=[ + dv.DivAction( + log_id="approve-success", + url="div-action://set_variable?name=success_visible&value=1", + ) + ], + on_fail_actions=[ + dv.DivAction( + log_id="approve-error", + url="div-action://set_variable?name=error_visible&value=1", + ), dv.DivAction( - url="divkit://send-request", - log_id="btn-approve", - typed=dv.DivActionSubmit( - container_id="btn-approve", - request=dv.DivActionSubmitRequest( - url=approve_link, - method=dv.RequestMethod.GET, - headers=[ - dv.RequestHeader(name="api-key", value=api_key) - ], - ), - on_success_actions=[ - dv.DivAction( - log_id="approve-success", - url="div-action://set_variable?name=success_visible&value=1", - ) - ], - on_fail_actions=[ - dv.DivAction( - log_id="approve-error", - url="div-action://set_variable?name=error_visible&value=1", - ), - dv.DivAction( - log_id="set-error-text", - url="div-action://set_variable?name=error_text&value=Failed to approve request", - ), - ], - ), + log_id="set-error-text", + url="div-action://set_variable?name=error_text&value=Failed to approve request", ), ], ), - dv.DivText( - text="Reject", - id="btn-reject", - font_size=14, - text_color="#2563EB", - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#3B82F6") + ), + ] + + # Reject button using smarty_ui text component with actions + reject_btn = text_1("Reject", color="#2563EB") + reject_btn.id = "btn-reject" + reject_btn.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#3B82F6") + ) + reject_btn.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + reject_btn.height = dv.DivFixedSize(value=36) + reject_btn.paddings = dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8) + reject_btn.margins = dv.DivEdgeInsets(right=8) + reject_btn.actions = [ + dv.DivAction( + url="divkit://send-request", + log_id="btn-reject", + typed=dv.DivActionSubmit( + container_id="btn-reject", + request=dv.DivActionSubmitRequest( + url=reject_link, + method=dv.RequestMethod.GET, + headers=[dv.RequestHeader(name="api-key", value=api_key)], ), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - height=dv.DivFixedSize(value=36), - paddings=dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8), - margins=dv.DivEdgeInsets(right=8), - actions=[ + on_success_actions=[ dv.DivAction( - url="divkit://send-request", - log_id="btn-reject", - typed=dv.DivActionSubmit( - container_id="btn-reject", - request=dv.DivActionSubmitRequest( - url=reject_link, - method=dv.RequestMethod.GET, - headers=[ - dv.RequestHeader(name="api-key", value=api_key) - ], - ), - on_success_actions=[ - dv.DivAction( - log_id="reject-success", - url="div-action://set_variable?name=success_visible&value=1", - ) - ], - on_fail_actions=[ - dv.DivAction( - log_id="reject-error", - url="div-action://set_variable?name=error_visible&value=1", - ), - dv.DivAction( - log_id="set-error-text", - url="div-action://set_variable?name=error_text&value=Failed to reject request", - ), - ], - ), + log_id="reject-success", + url="div-action://set_variable?name=success_visible&value=1", + ) + ], + on_fail_actions=[ + dv.DivAction( + log_id="reject-error", + url="div-action://set_variable?name=error_visible&value=1", + ), + dv.DivAction( + log_id="set-error-text", + url="div-action://set_variable?name=error_text&value=Failed to reject request", ), ], ), - ], - ) - container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[text_container, buttons_container], - ) + ), + ] + + # Buttons container using HStack + buttons_container = HStack([approve_btn, reject_btn]) + + # Main container using VStack + container = VStack([text_container, buttons_container]) logger.info("Successfully built human approval UI container") return dv.make_div(container) @@ -314,87 +287,44 @@ def human_approval_ui( raise -def human_approval_requests(context: Context) -> BuildOutput: - context.logger_context.logger.info( - "Starting human approval requests processing", - chat_id=context.logger_context.chat_id, - ) - - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - logger.info( - "Extracted context values", - llm_output_length=len(llm_output) if llm_output else 0, - backend_output_keys=list(backend_output.keys()) - if isinstance(backend_output, dict) - else "not_dict", - version=version, - language=language, - ) - - inp = {} - - try: - logger.info("Creating human approval widget") - human_approval_widget = Widget( - order=2, - type="approval_widget", - name="approval_widget", - layout="vertical", - fields=["approval_widget"], - values=[backend_output], - ) - inp[human_approval_ui] = WidgetInput( - widget=human_approval_widget, - args={ - "human_approval_input": backend_output, - "language": language, - "api_key": api_key, - "text": llm_output, - }, - ) +class HumanApprovalRequests(FunctionStrategy): + """Strategy for building human approval UI with fallback.""" - logger.info("human_approval_requests - widget created successfully") + def build_widget_inputs(self, context): + """Build the happy-path widget inputs (approval widget).""" + return { + human_approval_ui: WidgetInput( + widget=Widget( + order=2, + type="approval_widget", + name="approval_widget", + layout="vertical", + fields=["approval_widget"], + values=[context.backend_output], + ), + args={ + "human_approval_input": context.backend_output, + "language": context.language, + "api_key": context.api_key, + "text": context.llm_output, + }, + ), + } - except Exception as e: - logger.error( - "Error creating widgets, falling back to text widget only", error=str(e) - ) - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - inp[build_text_widget] = WidgetInput( - widget=text_widget, - args={ - "text": llm_output, - }, + def execute(self, context): + logger = context.logger_context.logger + logger.info( + "Starting human approval requests processing", + chat_id=context.logger_context.chat_id, ) + try: + return super().execute(context) + except Exception as e: + logger.error( + "Error creating widgets, falling back to text widget only", error=str(e) + ) + text_builder, text_input = self.make_text_input(context.llm_output) + return self._build_and_save(context, {text_builder: text_input}) - logger.info("Adding UI to widgets", widget_count=len(inp)) - widgets = add_ui_to_widget( - inp, - version, - ) - - logger.info("Widgets processed", final_widget_count=len(widgets)) - - output = BuildOutput( - widgets_count=len(widgets), - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - - logger.info("Saving builder output") - save_builder_output(context, output) - logger.info( - "Human approval requests processing completed successfully", - output_widget_count=output.widgets_count, - ) - return output +human_approval_requests = HumanApprovalRequests() diff --git a/functions_to_format/functions/news.py b/functions_to_format/functions/news.py index 01c5232..f452662 100644 --- a/functions_to_format/functions/news.py +++ b/functions_to_format/functions/news.py @@ -4,13 +4,25 @@ from pydantic import BaseModel from typing import List -from functions_to_format.functions.general.utils import save_builder_output -from .general import Widget, add_ui_to_widget, WidgetInput -from models.build import BuildOutput +from .general import Widget, WidgetInput from functions_to_format.functions.general.const_values import LanguageOptions from conf import logger import structlog from models.context import Context +from .base_strategy import FunctionStrategy + +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + title_2, + text_1, + text_2, + caption_1, + caption_2, + icon, + default_theme, +) # Feedback texts for news actions @@ -46,39 +58,26 @@ class NewsWidgetInput(BaseModel): header_text: str = "TOP NEWS" -def get_news(context: Context) -> BuildOutput: - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - news_widget_input = NewsWidgetInput(**backend_output) - widget = Widget( - name="news_widget", - type="news_widget", - order=1, - layout="vertical", - fields=["news_items", "header_text"], - ) - widgets = add_ui_to_widget( - { +class GetNews(FunctionStrategy): + """Strategy for building news UI.""" + + def build_widget_inputs(self, context): + news_widget_input = NewsWidgetInput(**context.backend_output) + return { build_news_widget: WidgetInput( - widget=widget, + widget=Widget( + name="news_widget", + type="news_widget", + order=1, + layout="vertical", + fields=["news_items", "header_text"], + ), args={"news_widget_input": news_widget_input}, ), - }, - version, - ) - output = BuildOutput( - widgets_count=1, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output + } + + +get_news = GetNews() def news_item( @@ -91,8 +90,8 @@ def news_item( language: LanguageOptions = LanguageOptions.RUSSIAN, ): """ - Create a news item container with action handling and feedback. - + Create a news item container with action handling and feedback using smarty_ui. + Args: title: News article title source: News source name @@ -101,64 +100,63 @@ def news_item( url: Article URL to open index: Item index for unique log IDs language: Language for localization - + Returns: DivContainer for the news item """ - feedback_texts = NEWS_FEEDBACK_TEXTS.get(language, NEWS_FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) + feedback_texts = NEWS_FEEDBACK_TEXTS.get( + language, NEWS_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) item_id = f"news-item-{index}" - - return dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - items=[ - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=title, - font_size=14, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#FFFFFF", - max_lines=1, - ), - dv.DivText( - text=f"{source} · {time}", - font_size=12, - text_color="#D1D5DB", - margins=dv.DivEdgeInsets(top=2), - max_lines=1, - ), - ], - ), - dv.DivImage( - image_url=image_url, - width=dv.DivFixedSize(value=36), - height=dv.DivFixedSize(value=36), - scale=dv.DivImageScale.FILL, - border=dv.DivBorder(corner_radius=8), - margins=dv.DivEdgeInsets(left=12), - ), - ], - margins=dv.DivEdgeInsets(top=12), - actions=[ - # Main action to open news URL - dv.DivAction( - log_id=f"open-news-{index}", - url=url, - payload={ - "news_title": title, - "news_source": source, - "news_url": url, - }, - ), - # Success feedback action - dv.DivAction( - log_id=f"open-news-{index}-success", - url="div-action://set_variable?name=news_opened&value=1", - ), - ], + + # Title text using text_1 + title_text = text_1(title, color="#FFFFFF") + title_text.font_weight = dv.DivFontWeight.MEDIUM + title_text.max_lines = 1 + + # Source and time using caption_1 + source_text = caption_1(f"{source} · {time}", color="#D1D5DB") + source_text.margins = dv.DivEdgeInsets(top=2) + source_text.max_lines = 1 + + # Text container using VStack + text_container = VStack([title_text, source_text]) + + # News thumbnail image + news_image = dv.DivImage( + image_url=image_url, + width=dv.DivFixedSize(value=36), + height=dv.DivFixedSize(value=36), + scale=dv.DivImageScale.FILL, + border=dv.DivBorder(corner_radius=8), + margins=dv.DivEdgeInsets(left=12), + ) + + # Main container using HStack + container = HStack( + [text_container, news_image], + alignment=dv.DivContentAlignmentVertical.CENTER, ) + container.margins = dv.DivEdgeInsets(top=12) + container.actions = [ + # Main action to open news URL + dv.DivAction( + log_id=f"open-news-{index}", + url=url, + payload={ + "news_title": title, + "news_source": source, + "news_url": url, + }, + ), + # Success feedback action + dv.DivAction( + log_id=f"open-news-{index}-success", + url="div-action://set_variable?name=news_opened&value=1", + ), + ] + + return container def build_news_widget( @@ -166,16 +164,18 @@ def build_news_widget( language: LanguageOptions = LanguageOptions.RUSSIAN, ): """ - Build the news widget with feedback handling for news item actions. - + Build the news widget with feedback handling using smarty_ui components. + Args: news_widget_input: Input data containing news items and header text language: Language for localization - + Returns: DivKit JSON for the news widget """ - feedback_texts = NEWS_FEEDBACK_TEXTS.get(language, NEWS_FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) + feedback_texts = NEWS_FEEDBACK_TEXTS.get( + language, NEWS_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) # Create news items with proper indexing news = [ @@ -191,79 +191,62 @@ def build_news_widget( for idx, item in enumerate(news_widget_input.news_items) ] - # Success feedback container for news opened - success_container = dv.DivContainer( - id="news-success-container", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{news_opened == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + # Success feedback container for news opened using smarty_ui + success_icon = caption_1("✅", color="#ECFDF5") + success_icon.margins = dv.DivEdgeInsets(right=8) + + success_text = caption_1(feedback_texts["opened"], color="#ECFDF5") + success_text.font_weight = dv.DivFontWeight.MEDIUM + success_text.width = dv.DivMatchParentSize(weight=1) + + dismiss_btn = caption_1("✕", color="#ECFDF5") + dismiss_btn.font_weight = dv.DivFontWeight.BOLD + dismiss_btn.paddings = dv.DivEdgeInsets(left=8) + dismiss_btn.actions = [ + dv.DivAction( + log_id="dismiss-news-success", + url="div-action://set_variable?name=news_opened&value=0", + ) + ] + + success_container = HStack( + [success_icon, success_text, dismiss_btn], + alignment=dv.DivContentAlignmentVertical.CENTER, + padding=8, + background="#065F46", + corner_radius=8, width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=8), - paddings=dv.DivEdgeInsets(top=8, bottom=8, left=12, right=12), - background=[dv.DivSolidBackground(color="#065F46")], - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=13, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["opened"], - font_size=12, - text_color="#ECFDF5", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=14, - text_color="#ECFDF5", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-news-success", - url="div-action://set_variable?name=news_opened&value=0", - ) - ], - ), - ], + ) + success_container.id = "news-success-container" + success_container.visibility = Expr("@{news_opened == 1 ? 'visible' : 'gone'}") + success_container.margins = dv.DivEdgeInsets(top=8) + success_container.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#A7F3D0", width=1) ) - items = [ - # Header - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text=news_widget_input.header_text, - font_size=12, - font_weight=dv.DivFontWeight.BOLD, - text_color="#9CA3AF", - letter_spacing=0.5, - ), - ], - ), - ] + # Header using caption_2 + header_text = caption_2(news_widget_input.header_text, color="#9CA3AF") + header_text.font_weight = dv.DivFontWeight.BOLD + header_text.letter_spacing = 0.5 + + header_container = HStack([header_text]) + + items = [header_container] items.extend(news) items.append(success_container) - root = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - background=[dv.DivSolidBackground(color="#1F2937")], # dark gray background - border=dv.DivBorder(corner_radius=16), - paddings=dv.DivEdgeInsets( - top=12, bottom=12, left=16, right=16 - ), - margins=dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8), - variables=[ - dv.IntegerVariable(name="news_opened", value=0), - ], - items=items, + # Root container using VStack + root = VStack( + items, + padding=12, + background="#1F2937", # dark gray background + corner_radius=16, ) + root.paddings = dv.DivEdgeInsets(top=12, bottom=12, left=16, right=16) + root.margins = dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8) + root.variables = [ + dv.IntegerVariable(name="news_opened", value=0), + ] return dv.make_div(root) diff --git a/functions_to_format/functions/notification.py b/functions_to_format/functions/notification.py index 7196131..50ee824 100644 --- a/functions_to_format/functions/notification.py +++ b/functions_to_format/functions/notification.py @@ -2,79 +2,71 @@ import json from pydantic import BaseModel from typing import List -from .general import Widget, add_ui_to_widget, WidgetInput -from .general.utils import save_builder_output -from models.build import BuildOutput -from functions_to_format.functions.general.const_values import LanguageOptions -from conf import logger +from .general import Widget, WidgetInput +from .base_strategy import FunctionStrategy from models.context import Context, LoggerContext -import structlog - - -def get_notifications(context: Context) -> BuildOutput: - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - notifications_widget = Widget( - name="notifications_widget", - type="notifications_widget", - layout="vertical", - order=1, - fields=["notifications"], - ) - notification_input = NotificationsInput( - notifications=backend_output["notifications"] - ) - widgets = add_ui_to_widget( - { +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + text_1, + text_2, + caption_1, + default_theme, +) + + +class GetNotifications(FunctionStrategy): + """Strategy for building notifications UI.""" + + def build_widget_inputs(self, context): + notification_input = NotificationsInput( + notifications=context.backend_output["notifications"] + ) + return { build_notifications_widget: WidgetInput( - widget=notifications_widget, + widget=Widget( + name="notifications_widget", + type="notifications_widget", + layout="vertical", + order=1, + fields=["notifications"], + ), args={ "notification_input": notification_input, "context": context.logger_context, }, ), - }, - version, - ) + } - output = BuildOutput( - widgets_count=1, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output + +get_notifications = GetNotifications() def notification_widget(title, description): - return dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=title, - font_size=14, - font_weight=dv.DivFontWeight.BOLD, - text_color="#1E3A8A", - ), - dv.DivText( - text=description, - font_size=13, - text_color="#374151", - margins=dv.DivEdgeInsets(top=4), - ), - ], - paddings=dv.DivEdgeInsets(left=16, right=16, bottom=16, top=16), - background=[dv.DivSolidBackground(color="#EFF6FF")], - border=dv.DivBorder(corner_radius=12, stroke=dv.DivStroke(color="#BFDBFE")), + """Create a notification widget using smarty_ui components.""" + # Title using text_1 with bold weight + title_text = text_1(title, color="#1E3A8A") + title_text.font_weight = dv.DivFontWeight.BOLD + + # Description using text_2 + desc_text = text_2(description, color="#374151") + desc_text.margins = dv.DivEdgeInsets(top=4) + + # Container using VStack + container = VStack( + [title_text, desc_text], + padding=16, + background="#EFF6FF", + corner_radius=12, width=dv.DivMatchParentSize(), ) + container.border = dv.DivBorder( + corner_radius=12, stroke=dv.DivStroke(color="#BFDBFE") + ) + + return container class Notification(BaseModel): @@ -90,8 +82,7 @@ def build_notifications_widget( notification_input: NotificationsInput, context: LoggerContext, ): - # Parse input data - + """Build notifications widget using smarty_ui components.""" # Create notification widgets notification_items = [] for notification in notification_input.notifications: @@ -101,13 +92,13 @@ def build_notifications_widget( ) ) - # Create container for all notifications - notifications_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=notification_items, + # Create container for all notifications using VStack + notifications_container = VStack( + notification_items, + gap=8, width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(left=8, right=8, top=8, bottom=8), ) + notifications_container.margins = dv.DivEdgeInsets(left=8, right=8, top=8, bottom=8) # Return the widget as a JSON-serializable object return dv.make_div(notifications_container) diff --git a/functions_to_format/functions/payment.py b/functions_to_format/functions/payment.py index 9347144..2d79d17 100644 --- a/functions_to_format/functions/payment.py +++ b/functions_to_format/functions/payment.py @@ -1,2166 +1,617 @@ import json - -from functions_to_format.functions.balance import get_home_balances -from functions_to_format.functions.functions import chatbot_answer - +from tool_call_models.paynet import ( + PaymentManagerPaymentResponse, +) +from .base_strategy import FunctionStrategy from .general import ( - add_ui_to_widget, Widget, - TextWidget, ButtonsWidget, - build_text_widget, - build_buttons_row, WidgetInput, - create_feedback_variables, - create_success_actions, - create_failure_actions, - create_success_container, - create_error_container, ) -from .general.utils import save_builder_output +from .buttons import build_buttons_row from pydantic import BaseModel -from typing import List, Dict, Any, Optional, Union +from typing import List, Optional, Dict, Any import pydivkit as dv from pydivkit.core import Expr -from models.build import BuildOutput -from tool_call_models.cards import CardsByPhoneNumberResponse, CardInfoByPhoneNumber from tool_call_models.paynet import ( CategoriesResponse, - PaymentManagerPaymentResponse, SupplierByCategoryResponse, Supplier, Category, ) -from tool_call_models.cards import CardInfoByCardNumberResponse from conf import logger from functions_to_format.functions.general.const_values import LanguageOptions -import structlog -from models.context import Context, LoggerContext - - -# Feedback texts for payment actions -PAYMENT_FEEDBACK_TEXTS = { - LanguageOptions.RUSSIAN: { - "card_selected": "Карта выбрана", - "category_selected": "Категория выбрана", - "supplier_selected": "Поставщик выбран", - "selection_error": "Ошибка выбора", - }, - LanguageOptions.ENGLISH: { - "card_selected": "Card selected", - "category_selected": "Category selected", - "supplier_selected": "Supplier selected", - "selection_error": "Selection error", - }, - LanguageOptions.UZBEK: { - "card_selected": "Karta tanlandi", - "category_selected": "Kategoriya tanlandi", - "supplier_selected": "Ta'minotchi tanlandi", - "selection_error": "Tanlash xatosi", - }, -} -def build_pay_for_home_utility_ui( - payment_response: PaymentManagerPaymentResponse, - language: LanguageOptions = LanguageOptions.UZBEK, -): - """Build a payment success card UI using pydivkit""" +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + text_1, + caption_1, + icon, + avatar, + divider, +) - texts_map = { - LanguageOptions.UZBEK: { - "payment_status": "To'lov miqdori", - "currency": "so'm", - "payment_description": "Mahsulot va xizmatlar uchun to'lov", - "transaction_details": "Tranzaksiya raqami:", - "date": "O'tkazish vaqti:", - "sender": "Jo'natuvchi:", - "receiver": "Qabul qiluvchi:", - }, - LanguageOptions.RUSSIAN: { - "payment_status": "Сумма платежа", - "currency": "cумм.", - "payment_description": "Платеж за товары и услуги", - "transaction_details": "Номер транзакции:", - "date": "Время перевода:", - "sender": "Отправитель:", - "receiver": "Получатель:", - }, - LanguageOptions.ENGLISH: { - "payment_status": "Payment amount", - "currency": "USD", - "payment_description": "Payment for goods and services", - "transaction_details": "Transaction number:", - "date": "Transfer time:", - "sender": "Sender:", - "receiver": "Receiver:", - }, - } +# Import smarty_ui blocks for ready-made components +from smarty_ui.blocks import ( + payment_status_widget as smarty_payment_status_widget, + payment_success_widget, + payment_failed_widget, + payment_pending_widget, + service_list, + home_balance_widget, +) +from smarty_ui.primitives import smarty_button, smarty_button_filled - # get receiver name ---- receiver name is the item in data.response where item["order"] == 1 - receiver_name = "" - for item in payment_response.data.response: - if item.order == 1: - receiver_name = item.value - break - # Create payment details items from response - payment_items = [] - for item in payment_response.data.response: - payment_items.append( - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text=item.name, - text_color="#666666", - font_size=14, - font_weight=dv.DivFontWeight.REGULAR, - width=dv.DivWrapContentSize(), - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=item.value, - text_color="#000000", - font_size=14, - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(), - text_alignment_horizontal=dv.DivAlignmentHorizontal.END, - ), - ], - margins=dv.DivEdgeInsets(bottom=12), - ) - ) +class GetCategories(FunctionStrategy): + """Strategy for payment categories UI.""" - # Create success card matching the image design - success_card = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - background=[ - dv.DivSolidBackground( - color="#FFFFFF", - ) - ], - border=dv.DivBorder( - corner_radius=16, - ), - paddings=dv.DivEdgeInsets(left=24, top=32, right=24, bottom=32), - margins=dv.DivEdgeInsets(left=16, top=16, right=16, bottom=16), - items=[ - # Success checkmark icon - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivContainer( - background=[ - dv.DivSolidBackground( - color="#E8F5E8", - ) - ], - border=dv.DivBorder( - corner_radius=50, - ), - width=dv.DivFixedSize(value=80), - height=dv.DivFixedSize(value=80), - items=[ - dv.DivText( - text="✓", - text_color="#4CAF50", - font_size=36, - font_weight=dv.DivFontWeight.BOLD, - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - text_alignment_vertical=dv.DivAlignmentVertical.CENTER, - ), - ], - ), - ], - content_alignment_horizontal=dv.DivContentAlignmentHorizontal.CENTER, - margins=dv.DivEdgeInsets(bottom=24), - ), - # Payment success title - dv.DivText( - text=texts_map[language]["payment_status"], - text_color="#666666", - font_size=16, - font_weight=dv.DivFontWeight.REGULAR, - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - margins=dv.DivEdgeInsets(bottom=8), - ), - # Amount - dv.DivText( - text=f"{payment_response.additional.amount if payment_response.additional else '0'} {texts_map[language]['currency']}", - text_color="#000000", - font_size=32, - font_weight=dv.DivFontWeight.BOLD, - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - margins=dv.DivEdgeInsets(bottom=8), - ), - # Service description - dv.DivContainer( - background=[ - dv.DivSolidBackground( - color="#E8F5E8", - ) - ], - border=dv.DivBorder( - corner_radius=8, + def build_widget_inputs(self, context): + backend_data = CategoriesResponse(**context.backend_output) + categories = backend_data.payload + text_builder, text_input = self.make_text_input(context.llm_output) + return { + build_get_categories_ui: WidgetInput( + widget=Widget( + name="payments_list_item_widget", + type="payments_list_item_widget", + order=2, + layout="vertical", + fields=["id", "name", "image_url"], + values=[c.model_dump() for c in categories], ), - paddings=dv.DivEdgeInsets(left=12, top=8, right=12, bottom=8), - margins=dv.DivEdgeInsets(bottom=32), - items=[ - dv.DivText( - text=texts_map[language]["payment_description"], - text_color="#4CAF50", - font_size=14, - font_weight=dv.DivFontWeight.MEDIUM, - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - ), - ], - ), - # Transaction details - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text=texts_map[language]["transaction_details"], - text_color="#666666", - font_size=14, - font_weight=dv.DivFontWeight.REGULAR, - width=dv.DivWrapContentSize(), - ), - ], - margins=dv.DivEdgeInsets(bottom=8), + args={"categories": categories}, ), - # Separator line - dv.DivSeparator( - delimiter_style=dv.DivSeparatorDelimiterStyle( - color="#E0E0E0", + build_buttons_row: WidgetInput( + widget=ButtonsWidget( + order=3, values=[{"text": "Cancel", "action": "cancel"}] ), - margins=dv.DivEdgeInsets(bottom=16), - ), - # Date - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text=texts_map[language]["date"], - text_color="#666666", - font_size=14, - font_weight=dv.DivFontWeight.REGULAR, - width=dv.DivWrapContentSize(), - ), - dv.DivText( - text=payment_response.additional.date - if payment_response.additional - else "", - text_color="#000000", - font_size=14, - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(), - text_alignment_horizontal=dv.DivAlignmentHorizontal.END, - ), - ], - margins=dv.DivEdgeInsets(bottom=16), + args={"button_texts": ["cancel"], "language": context.language}, ), - # Recipient - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text=texts_map[language]["sender"], - text_color="#666666", - font_size=14, - font_weight=dv.DivFontWeight.REGULAR, - width=dv.DivWrapContentSize(), - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - width=dv.DivMatchParentSize(), - items=[ - dv.DivText( - text=payment_response.additional.sender_name - if payment_response.additional - else "", - text_color="#000000", - font_size=14, - font_weight=dv.DivFontWeight.MEDIUM, - text_alignment_horizontal=dv.DivAlignmentHorizontal.END, - ), - dv.DivText( - text=payment_response.additional.sender_masked_pan - if payment_response.additional - else "**** **** **** ****", - text_color="#666666", - font_size=12, - font_weight=dv.DivFontWeight.REGULAR, - text_alignment_horizontal=dv.DivAlignmentHorizontal.END, - ), - ], - ), - ], - margins=dv.DivEdgeInsets(bottom=16), - ), - # Receiver - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text=texts_map[language]["receiver"], - text_color="#666666", - font_size=14, - font_weight=dv.DivFontWeight.REGULAR, - width=dv.DivWrapContentSize(), - ), - dv.DivText( - text=receiver_name, - text_color="#000000", - font_size=14, - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(), - text_alignment_horizontal=dv.DivAlignmentHorizontal.END, - ), - ], - margins=dv.DivEdgeInsets(bottom=16), - ), - ], - ) - - return dv.make_div(success_card) - - -def pay_for_home_utility(context: Context) -> BuildOutput: - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - inp = {} - - try: - backend_output = PaymentManagerPaymentResponse.model_validate(backend_output) - payment_status_widget = Widget( - order=2, - type="payment_status_widget", - name="payment_status_widget", - layout="vertical", - fields=["payment_status"], - values=[backend_output.model_dump()], - ) - inp[build_pay_for_home_utility_ui] = WidgetInput( - widget=payment_status_widget, - args={"payment_response": backend_output}, - ) - - logger.info("pay_for_home_utility") - - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - inp[build_text_widget] = WidgetInput( - widget=text_widget, - args={ - "text": llm_output, - }, - ) - - except Exception as e: - inp[build_text_widget] = WidgetInput( - widget=text_widget, - args={ - "text": llm_output, - }, - ) - - widgets = add_ui_to_widget( - inp, - version, - ) - output = BuildOutput( - widgets_count=len(widgets), - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - - save_builder_output(context, output) - return output - - -# ============================================================================ -# Receiver by Card Models and UI -# ============================================================================ - -class ReceiverByCardResponse(BaseModel): - """Model for receiver information retrieved by card number.""" - processingSystem: str - iconMini: Optional[str] = None - isFound: bool - maskedPan: str - errorMessage: Optional[str] = None - icon: Optional[str] = None - fullName: Optional[str] = None - errorCode: Optional[str] = None - token: Optional[str] = None - - -# Feedback texts for money transfer actions -TRANSFER_FEEDBACK_TEXTS = { - LanguageOptions.RUSSIAN: { - "receiver_found": "Получатель найден", - "receiver_not_found": "Получатель не найден", - "transfer_success": "Перевод выполнен успешно!", - "transfer_error": "Ошибка перевода", - "confirm_transfer": "Подтвердить перевод", - "cancel": "Отмена", - "amount": "Сумма", - "recipient": "Получатель", - "card": "Карта", - "currency": "сум", - "send": "Отправить", - "processing": "Обработка...", - }, - LanguageOptions.ENGLISH: { - "receiver_found": "Recipient found", - "receiver_not_found": "Recipient not found", - "transfer_success": "Transfer completed successfully!", - "transfer_error": "Transfer failed", - "confirm_transfer": "Confirm transfer", - "cancel": "Cancel", - "amount": "Amount", - "recipient": "Recipient", - "card": "Card", - "currency": "UZS", - "send": "Send", - "processing": "Processing...", - }, - LanguageOptions.UZBEK: { - "receiver_found": "Qabul qiluvchi topildi", - "receiver_not_found": "Qabul qiluvchi topilmadi", - "transfer_success": "O'tkazma muvaffaqiyatli bajarildi!", - "transfer_error": "O'tkazma xatosi", - "confirm_transfer": "O'tkazmani tasdiqlash", - "cancel": "Bekor qilish", - "amount": "Summa", - "recipient": "Qabul qiluvchi", - "card": "Karta", - "currency": "so'm", - "send": "Yuborish", - "processing": "Qayta ishlanmoqda...", - }, -} + text_builder: text_input, + } -def get_processing_system_icon(processing_system: str) -> str: - """Get icon URL for a processing system.""" - icons = { - "VISA": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/Visa_Inc._logo.svg/200px-Visa_Inc._logo.svg.png", - "MASTERCARD": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Mastercard-logo.svg/200px-Mastercard-logo.svg.png", - "HUMO": "https://humocard.uz/bitrix/templates/main/img/card2.png", - "UZCARD": "https://uzcard.uz/upload/images/logo.png", - } - return icons.get(processing_system.upper(), "https://via.placeholder.com/48x32") +get_categories = GetCategories() -def build_receiver_by_card_ui( - receiver_data: ReceiverByCardResponse, +def build_get_categories_ui( + categories: List[Category], language: LanguageOptions = LanguageOptions.RUSSIAN, ) -> Dict[str, Any]: """ - Build UI for displaying receiver information retrieved by card number. - + Build categories UI using smarty_ui service_list component. + Args: - receiver_data: Receiver information from backend + categories: List of category objects language: Language for localization - + Returns: - DivKit JSON for the receiver card UI + DivKit JSON for the categories UI """ - feedback_texts = TRANSFER_FEEDBACK_TEXTS.get(language, TRANSFER_FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) - - # Determine icon URL - icon_url = receiver_data.icon or get_processing_system_icon(receiver_data.processingSystem) - - if not receiver_data.isFound: - # Error state - receiver not found - error_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - background=[dv.DivSolidBackground(color="#FEF2F2")], - border=dv.DivBorder( - corner_radius=16, stroke=dv.DivStroke(color="#FECACA", width=1) - ), - paddings=dv.DivEdgeInsets(left=20, right=20, top=20, bottom=20), - margins=dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16), - items=[ - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - items=[ - dv.DivText( - text="❌", - font_size=32, - margins=dv.DivEdgeInsets(right=12), - ), - dv.DivText( - text=feedback_texts["receiver_not_found"], - font_size=18, - font_weight=dv.DivFontWeight.BOLD, - text_color="#B91C1C", - ), - ], - margins=dv.DivEdgeInsets(bottom=12), - ), - dv.DivText( - text=receiver_data.errorMessage or feedback_texts["transfer_error"], - font_size=14, - text_color="#7F1D1D", - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - ), - ], + # Get localized title + titles = { + LanguageOptions.RUSSIAN: "Категории платежей", + LanguageOptions.ENGLISH: "Payment Categories", + LanguageOptions.UZBEK: "To'lov kategoriyalari", + } + title = titles.get(language, titles[LanguageOptions.ENGLISH]) + + # Convert categories to service_list format + service_items = [] + for category in categories: + service_items.append( + { + "title": category.name, + "icon_url": category.s3Url, + "action_url": f"divkit://action?type=select_category&id={category.id}&name={category.name}", + } ) - return dv.make_div(error_container) - - # Success state - receiver found - receiver_card = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - background=[dv.DivSolidBackground(color="#FFFFFF")], - border=dv.DivBorder( - corner_radius=16, stroke=dv.DivStroke(color="#E5E7EB", width=1) - ), - paddings=dv.DivEdgeInsets(left=20, right=20, top=20, bottom=20), - margins=dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16), - variables=[ - dv.IntegerVariable(name="receiver_card_success", value=0), - dv.IntegerVariable(name="receiver_card_error", value=0), - ], - items=[ - # Header with success indicator - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - items=[ - dv.DivContainer( - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder(corner_radius=20), - width=dv.DivFixedSize(value=40), - height=dv.DivFixedSize(value=40), - items=[ - dv.DivText( - text="✓", - font_size=20, - font_weight=dv.DivFontWeight.BOLD, - text_color="#059669", - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - text_alignment_vertical=dv.DivAlignmentVertical.CENTER, - ), - ], - margins=dv.DivEdgeInsets(right=12), - ), - dv.DivText( - text=feedback_texts["receiver_found"], - font_size=16, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#059669", - ), - ], - margins=dv.DivEdgeInsets(bottom=20), - ), - # Receiver info card - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - background=[dv.DivSolidBackground(color="#F9FAFB")], - border=dv.DivBorder(corner_radius=12), - paddings=dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16), - items=[ - # Card icon/avatar - dv.DivContainer( - background=[dv.DivSolidBackground(color="#EBF5FF")], - border=dv.DivBorder(corner_radius=24), - width=dv.DivFixedSize(value=48), - height=dv.DivFixedSize(value=48), - items=[ - dv.DivImage( - image_url=icon_url, - width=dv.DivFixedSize(value=32), - height=dv.DivFixedSize(value=20), - scale=dv.DivImageScale.FIT, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - ), - ], - margins=dv.DivEdgeInsets(right=16), - ), - # Receiver details - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=receiver_data.fullName or "Unknown", - font_size=16, - font_weight=dv.DivFontWeight.BOLD, - text_color="#111827", - margins=dv.DivEdgeInsets(bottom=4), - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text=receiver_data.processingSystem, - font_size=13, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#3B82F6", - ), - dv.DivText( - text=f" • {receiver_data.maskedPan}", - font_size=13, - text_color="#6B7280", - ), - ], - ), - ], - ), - ], - margins=dv.DivEdgeInsets(bottom=16), - ), - # Success feedback container - dv.DivContainer( - id="receiver-card-success", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{receiver_card_success == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=8, bottom=8), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["receiver_found"], - font_size=13, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-receiver-card-success", - url="div-action://set_variable?name=receiver_card_success&value=0", - ) - ], - ), - ], - ), - # Action button - Confirm/Select - dv.DivText( - text=feedback_texts["confirm_transfer"], - font_size=15, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#FFFFFF", - background=[dv.DivSolidBackground(color="#3B82F6")], - border=dv.DivBorder(corner_radius=10), - height=dv.DivFixedSize(value=44), - width=dv.DivMatchParentSize(), - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - text_alignment_vertical=dv.DivAlignmentVertical.CENTER, - margins=dv.DivEdgeInsets(top=8), - actions=[ - dv.DivAction( - log_id="confirm_receiver_selection", - url="divkit://confirm_receiver", - payload={ - "token": receiver_data.token, - "fullName": receiver_data.fullName, - "maskedPan": receiver_data.maskedPan, - "processingSystem": receiver_data.processingSystem, - }, - ), - dv.DivAction( - log_id="confirm_receiver_success", - url="div-action://set_variable?name=receiver_card_success&value=1", - ), - ], - ), - ], + + # Use smarty_ui service_list component + categories_widget = service_list( + title=title, + items=service_items, ) - - div = dv.make_div(receiver_card) - with open("logs/json/build_receiver_by_card_ui.json", "w") as f: + + # Wrap with margins + container = VStack([categories_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) + + div = dv.make_div(container) + with open("logs/json/build_get_categories_ui.json", "w") as f: json.dump(div, f, ensure_ascii=False, indent=2) return div -def get_receiver_by_card(context: Context) -> BuildOutput: - """ - Process receiver information from card lookup and create UI widgets. - - Args: - context: Context object containing backend_output, version, language, etc. - - Returns: - BuildOutput with receiver card widget - """ - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - logger.info("get_receiver_by_card", backend_output=backend_output) - - try: - receiver_data = ReceiverByCardResponse(**backend_output) - except Exception as e: - logger.error("Error parsing receiver data", error=str(e)) - receiver_data = ReceiverByCardResponse( - processingSystem="UNKNOWN", - isFound=False, - maskedPan="****", - errorMessage=str(e), - ) - - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - - receiver_widget = Widget( - name="receiver_by_card_widget", - type="receiver_by_card_widget", - order=2, - layout="vertical", - fields=["processingSystem", "isFound", "maskedPan", "fullName", "token"], - values=[receiver_data.model_dump(exclude_none=True)], - ) - - widgets = add_ui_to_widget( - { - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, +class GetSuppliersByCategory(FunctionStrategy): + """Strategy for suppliers list UI.""" + + def build_widget_inputs(self, context): + backend_data = SupplierByCategoryResponse(**context.backend_output) + suppliers = backend_data.payload + text_builder, text_input = self.make_text_input(context.llm_output) + return { + get_suppliers_by_category_ui: WidgetInput( + widget=Widget( + name="payments_list_item_widget", + type="payments_list_item_widget", + order=2, + layout="vertical", + fields=["id", "name", "image_url", "category_id"], + values=[s.model_dump() for s in suppliers], + ), + args={"suppliers": suppliers}, ), - build_receiver_by_card_ui: WidgetInput( - widget=receiver_widget, - args={ - "receiver_data": receiver_data, - "language": language, - }, + build_buttons_row: WidgetInput( + widget=ButtonsWidget( + order=3, values=[{"text": "Cancel", "action": "cancel"}] + ), + args={"button_texts": ["cancel"], "language": context.language}, ), - }, - version, - ) - - output = BuildOutput( - widgets_count=len(widgets), - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output + text_builder: text_input, + } -# ============================================================================ -# Send Money Transfer UI -# ============================================================================ +get_suppliers_by_category = GetSuppliersByCategory() -def build_send_money_ui( - amount: int, - receiver_name: str, - masked_pan: str, - processing_system: str, - token: str, - chat_id: str, - api_key: str, + +def get_suppliers_by_category_ui( + suppliers: List[Supplier], language: LanguageOptions = LanguageOptions.RUSSIAN, ) -> Dict[str, Any]: """ - Build UI for confirming and sending money transfer. - + Build suppliers UI using smarty_ui service_list component. + Args: - amount: Transfer amount in minor units (e.g., tiyin) - receiver_name: Recipient's full name - masked_pan: Masked card PAN - processing_system: Card processing system (VISA, HUMO, etc.) - token: Transfer token for API - chat_id: Chat ID for API call - api_key: API key for authorization + suppliers: List of supplier objects language: Language for localization - + Returns: - DivKit JSON for the transfer confirmation UI + DivKit JSON for the suppliers UI """ - feedback_texts = TRANSFER_FEEDBACK_TEXTS.get(language, TRANSFER_FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) - icon_url = get_processing_system_icon(processing_system) - - # Format amount - formatted_amount = f"{amount:,}".replace(",", " ") - - transfer_card = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - background=[dv.DivSolidBackground(color="#FFFFFF")], - border=dv.DivBorder( - corner_radius=20, stroke=dv.DivStroke(color="#E5E7EB", width=1) - ), - paddings=dv.DivEdgeInsets(left=24, right=24, top=24, bottom=24), - margins=dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16), - variables=[ - dv.IntegerVariable(name="transfer_success", value=0), - dv.IntegerVariable(name="transfer_error", value=0), - dv.IntegerVariable(name="transfer_loading", value=0), - ], - id="transfer_container", - items=[ - # Amount header - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - items=[ - dv.DivText( - text=feedback_texts["amount"], - font_size=14, - text_color="#6B7280", - margins=dv.DivEdgeInsets(bottom=8), - ), - dv.DivText( - text=f"{formatted_amount} {feedback_texts['currency']}", - font_size=32, - font_weight=dv.DivFontWeight.BOLD, - text_color="#111827", - ), - ], - margins=dv.DivEdgeInsets(bottom=24), - ), - # Divider - dv.DivSeparator( - delimiter_style=dv.DivSeparatorDelimiterStyle(color="#E5E7EB"), - margins=dv.DivEdgeInsets(bottom=20), - ), - # Recipient section - dv.DivText( - text=feedback_texts["recipient"], - font_size=12, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#9CA3AF", - margins=dv.DivEdgeInsets(bottom=12), - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - background=[dv.DivSolidBackground(color="#F9FAFB")], - border=dv.DivBorder(corner_radius=12), - paddings=dv.DivEdgeInsets(left=16, right=16, top=14, bottom=14), - items=[ - # Card icon - dv.DivContainer( - background=[dv.DivSolidBackground(color="#EBF5FF")], - border=dv.DivBorder(corner_radius=20), - width=dv.DivFixedSize(value=40), - height=dv.DivFixedSize(value=40), - items=[ - dv.DivImage( - image_url=icon_url, - width=dv.DivFixedSize(value=28), - height=dv.DivFixedSize(value=18), - scale=dv.DivImageScale.FIT, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - ), - ], - margins=dv.DivEdgeInsets(right=14), - ), - # Recipient details - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=receiver_name, - font_size=15, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#111827", - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text=processing_system, - font_size=13, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#3B82F6", - ), - dv.DivText( - text=f" • {masked_pan}", - font_size=13, - text_color="#6B7280", - ), - ], - margins=dv.DivEdgeInsets(top=2), - ), - ], - ), - ], - margins=dv.DivEdgeInsets(bottom=20), - ), - # Success feedback container - dv.DivContainer( - id="transfer-success-container", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{transfer_success == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(bottom=16), - paddings=dv.DivEdgeInsets(top=12, bottom=12, left=14, right=14), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=16, - margins=dv.DivEdgeInsets(right=10), - ), - dv.DivText( - text=feedback_texts["transfer_success"], - font_size=14, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=18, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=10), - actions=[ - dv.DivAction( - log_id="dismiss-transfer-success", - url="div-action://set_variable?name=transfer_success&value=0", - ) - ], - ), - ], - ), - # Error feedback container - dv.DivContainer( - id="transfer-error-container", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{transfer_error == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(bottom=16), - paddings=dv.DivEdgeInsets(top=12, bottom=12, left=14, right=14), - background=[dv.DivSolidBackground(color="#FEF2F2")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#FECACA", width=1) - ), - items=[ - dv.DivText( - text="⚠️", - font_size=16, - margins=dv.DivEdgeInsets(right=10), - ), - dv.DivText( - text=feedback_texts["transfer_error"], - font_size=14, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=18, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=10), - actions=[ - dv.DivAction( - log_id="dismiss-transfer-error", - url="div-action://set_variable?name=transfer_error&value=0", - ) - ], - ), - ], - ), - # Loading indicator - dv.DivContainer( - id="transfer-loading-container", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{transfer_loading == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(bottom=16), - paddings=dv.DivEdgeInsets(top=12, bottom=12, left=14, right=14), - background=[dv.DivSolidBackground(color="#F3F4F6")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#D1D5DB", width=1) - ), - items=[ - dv.DivText( - text="⏳", - font_size=16, - margins=dv.DivEdgeInsets(right=10), - ), - dv.DivText( - text=feedback_texts["processing"], - font_size=14, - text_color="#4B5563", - font_weight=dv.DivFontWeight.MEDIUM, - ), - ], - ), - # Buttons row - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - width=dv.DivMatchParentSize(), - items=[ - # Cancel button - dv.DivText( - text=feedback_texts["cancel"], - font_size=15, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#6B7280", - background=[dv.DivSolidBackground(color="#F3F4F6")], - border=dv.DivBorder(corner_radius=10), - height=dv.DivFixedSize(value=48), - width=dv.DivMatchParentSize(), - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - text_alignment_vertical=dv.DivAlignmentVertical.CENTER, - margins=dv.DivEdgeInsets(right=8), - actions=[ - dv.DivAction( - log_id="cancel_transfer", - url="divkit://cancel_transfer", - payload={"action": "cancel"}, - ), - ], - ), - # Send button with submit action - dv.DivText( - id="send_money_btn", - text=feedback_texts["send"], - font_size=15, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#FFFFFF", - background=[dv.DivSolidBackground(color="#3B82F6")], - border=dv.DivBorder(corner_radius=10), - height=dv.DivFixedSize(value=48), - width=dv.DivMatchParentSize(), - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - text_alignment_vertical=dv.DivAlignmentVertical.CENTER, - margins=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="send_money_action", - url="divkit://send-request", - typed=dv.DivActionSubmit( - container_id="transfer_container", - request=dv.DivActionSubmitRequest( - url=f"https://smarty.smartbank.uz/chat/v3/tools/call?function_name=execute_card_transfer&chat_id={chat_id}&arguments={json.dumps({'token': token, 'amount': amount})}", - method=dv.RequestMethod.POST, - headers=[ - dv.RequestHeader(name="api-key", value=api_key) - ], - ), - on_success_actions=[ - dv.DivAction( - log_id="transfer-success-show", - url="div-action://set_variable?name=transfer_success&value=1", - ), - dv.DivAction( - log_id="transfer-loading-hide", - url="div-action://set_variable?name=transfer_loading&value=0", - ), - dv.DivAction( - log_id="transfer-error-hide", - url="div-action://set_variable?name=transfer_error&value=0", - ), - ], - on_fail_actions=[ - dv.DivAction( - log_id="transfer-error-show", - url="div-action://set_variable?name=transfer_error&value=1", - ), - dv.DivAction( - log_id="transfer-loading-hide-error", - url="div-action://set_variable?name=transfer_loading&value=0", - ), - dv.DivAction( - log_id="transfer-success-hide", - url="div-action://set_variable?name=transfer_success&value=0", - ), - ], - ), - payload={ - "token": token, - "amount": amount, - "receiver_name": receiver_name, - "masked_pan": masked_pan, - }, - ), - # Show loading - dv.DivAction( - log_id="show-loading", - url="div-action://set_variable?name=transfer_loading&value=1", - ), - ], - ), - ], - ), - ], + # Get localized title + titles = { + LanguageOptions.RUSSIAN: "Поставщики услуг", + LanguageOptions.ENGLISH: "Service Providers", + LanguageOptions.UZBEK: "Xizmat ko'rsatuvchilar", + } + title = titles.get(language, titles[LanguageOptions.ENGLISH]) + + # Convert suppliers to service_list format + service_items = [] + for supplier in suppliers: + service_items.append( + { + "title": supplier.name, + "icon_url": supplier.s3Url, + "action_url": f"divkit://open_supplier?id={supplier.id}&name={supplier.name}", + } + ) + + # Use smarty_ui service_list component + suppliers_widget = service_list( + title=title, + items=service_items, ) - - div = dv.make_div(transfer_card) - with open("logs/json/build_send_money_ui.json", "w") as f: + + # Wrap with margins + container = VStack([suppliers_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) + + div = dv.make_div(container) + with open("logs/json/build_get_suppliers_by_category_ui.json", "w") as f: json.dump(div, f, ensure_ascii=False, indent=2) return div -def send_money_to_someone_via_card(context: Context) -> BuildOutput: - """ - Process money transfer and create UI widgets for confirmation/result. - - Args: - context: Context object containing llm_output, backend_output, version, etc. - - Returns: - BuildOutput with transfer confirmation widget - """ - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - logger.info("send_money_to_someone_via_card", backend_output=backend_output) - - # Extract transfer details from backend_output - amount = backend_output.get("amount", 0) - receiver_name = backend_output.get("fullName") or backend_output.get("card_owner_name", "Unknown") - masked_pan = backend_output.get("maskedPan") or backend_output.get("marked_card_pan", "****") - processing_system = backend_output.get("processingSystem", "UNKNOWN") - token = backend_output.get("token") or backend_output.get("to_card_id", "") - - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - - transfer_widget = Widget( - name="send_money_widget", - type="send_money_widget", - order=2, - layout="vertical", - fields=[ - "amount", - "receiver_name", - "masked_pan", - "processing_system", - "token", - ], - values=[{ - "amount": amount, - "receiver_name": receiver_name, - "masked_pan": masked_pan, - "processing_system": processing_system, - "token": token, - }], - ) - - widgets = add_ui_to_widget( - { - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, - ), - build_send_money_ui: WidgetInput( - widget=transfer_widget, - args={ - "amount": amount, - "receiver_name": receiver_name, - "masked_pan": masked_pan, - "processing_system": processing_system, - "token": token, - "chat_id": chat_id, - "api_key": api_key, - "language": language, - }, - ), - }, - version, - ) - - output = BuildOutput( - widgets_count=len(widgets), - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output - - -def get_number_by_receiver_name(context) -> BuildOutput: - from models.context import Context - - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - logger.info("get_number_by_receiver_name") - output = [] - # backend_output_processed = [] - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - - get_contacts_widget = Widget( - name="get_contacts_widget", - type="get_contacts_widget", - order=2, - layout="vertical", - fields=["receiver_name"], - values=[{"receiver_name": backend_output["names"]}], - ) +class Field(BaseModel): + identName: Optional[str] = None + name: Optional[str] = None + order: Optional[int] = None + type: Optional[str] = None + pattern: Optional[str] = None + minValue: Optional[int] = None + maxValue: Optional[int] = None + fieldSize: Optional[int] = None + isMain: Optional[bool] = None + valueList: List[str] = [] - buttons = ButtonsWidget( - order=3, - values=[{"text": "cancel"}], - ) - names = ( - ",".join(backend_output["names"]) - if isinstance(backend_output["names"], list) - else backend_output["names"] - ) +class GetFieldsOfSupplier(FunctionStrategy): + """Strategy for supplier fields UI.""" + + def build_widget_inputs(self, context): + fields_list = [] + for field in context.backend_output["payload"]["fieldList"]: + fields_list.append( + Field( + identName=field["identName"], + name=field["name"], + order=field["order"], + type=field["type"], + pattern=field["pattern"], + minValue=field["minValue"], + maxValue=field["maxValue"], + fieldSize=field["fieldSize"], + isMain=field["isMain"], + valueList=[str(field["valueList"])], + ) + ) - widgets = add_ui_to_widget( - { - build_text_widget: WidgetInput( - widget=text_widget, - args={ - "text": f"Contactlaringiz orasidan {names} ismini qidirishimiz kerak. Qidiraylikmi ?" - }, + inp = {} + if context.llm_output: + text_builder, text_input = self.make_text_input(context.llm_output) + inp[text_builder] = text_input + + inp[get_fields_of_supplier_ui] = WidgetInput( + widget=Widget( + name="fields_widget", + type="fields_widget", + order=2, + layout="vertical", + fields=[ + "identName", + "name", + "order", + "type", + "pattern", + "minValue", + "maxValue", + "fieldSize", + "isMain", + "valueList", + ], + values=[f.model_dump() for f in fields_list], ), - build_buttons_row: WidgetInput( - widget=buttons, - args={ - "button_texts": ["cancel", "search"], - "receiver_name": names, - "language": language, - }, + args={"fields": fields_list}, + ) + inp[build_buttons_row] = WidgetInput( + widget=ButtonsWidget( + order=3, values=[{"text": "Cancel", "action": "cancel"}] ), - }, - version, - ) + args={"button_texts": ["cancel"], "language": context.language}, + ) + return inp - output = BuildOutput( - widgets_count=len(widgets), - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output +get_fields_of_supplier = GetFieldsOfSupplier() -def get_number_by_reciver_number_ui(receiver_name: Union[str, List[str]]): - logger.info("get_number_by_reciver_number_ui") - if isinstance(receiver_name, list): - search_contact_action = dv.DivAction( - log_id="search_contact", - url=f"divkit://search_contact?name={receiver_name}", # Custom scheme the iOS app will catch - payload={"name": receiver_name}, # Optional: structured access - ) - else: - search_contact_action = dv.DivAction( - log_id="search_contact", - url=f"divkit://search_contact?name=[{receiver_name}]", # Custom scheme the iOS app will catch - payload={"name": [receiver_name]}, # Optional: structured access - ) - # Main container - main_container = dv.DivContainer( - items=[ - dv.DivContainer( - items=[], - action=search_contact_action, - visibility=dv.DivVisibility.INVISIBLE, - ) - ] +def get_fields_of_supplier_ui(fields: List[Field]): + logger.info("get_fields_of_supplier_ui", fields=fields) + return + raise NotImplementedError() + div = dv.make_div( + dv.DivContainer( + orientation="vertical", + width=dv.DivFixedSize(value=343), + height=dv.DivFixedSize(value=555), + margins=dv.DivEdgeInsets(top=110, left=16), + items=[ + dv.DivContainer( + orientation="vertical", + width=dv.DivMatchParentSize(), + items=[ + dv.DivInput( + hint_text=str( + field.name + ), # Convert to string to ensure not None + font_size=16, + text_color="#1A1A1A", + hint_color="#999999", + margins=dv.DivEdgeInsets(bottom=16), + keyboard_type="text", # Ensure text input type + ) + for field in fields + ], + ) + ], + ) ) - div = dv.make_div(main_container) - with open("logs/json/build_get_number_by_reciver_number_ui.json", "w") as f: + with open("logs/json/build_get_fields_of_supplier_ui.json", "w") as f: json.dump(div, f, ensure_ascii=False, indent=2) - logger.info("Saved to build_get_number_by_reciver_number_ui.json", div=div) - logger.info("get_number_by_reciver_number_ui done") return div -def get_receiver_id_by_receiver_phone_number(context: Context) -> BuildOutput: - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger +# Feedback texts for payment actions +PAYMENT_FEEDBACK_TEXTS = { + LanguageOptions.RUSSIAN: { + "card_selected": "Карта выбрана", + "category_selected": "Категория выбрана", + "supplier_selected": "Поставщик выбран", + "selection_error": "Ошибка выбора", + }, + LanguageOptions.ENGLISH: { + "card_selected": "Card selected", + "category_selected": "Category selected", + "supplier_selected": "Supplier selected", + "selection_error": "Selection error", + }, + LanguageOptions.UZBEK: { + "card_selected": "Karta tanlandi", + "category_selected": "Kategoriya tanlandi", + "supplier_selected": "Ta'minotchi tanlandi", + "selection_error": "Tanlash xatosi", + }, +} - output = [] - # backend_output_processed = [] - backend_output = CardsByPhoneNumberResponse(**backend_output) - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) +def build_pay_for_home_utility_ui( + payment_response: PaymentManagerPaymentResponse, + language: LanguageOptions = LanguageOptions.UZBEK, +): + """Build a payment success card UI using smarty_ui payment_status_widget block component""" - get_contacts_widget = Widget( - name="get_contacts_widget", - type="get_contacts_widget", - order=2, - layout="vertical", - fields=["receiver_name"], - values=[x.model_dump() for x in backend_output.cards], - ) + texts_map = { + LanguageOptions.UZBEK: { + "currency": "so'm", + }, + LanguageOptions.RUSSIAN: { + "currency": "сум", + }, + LanguageOptions.ENGLISH: { + "currency": "USD", + }, + } - buttons = ButtonsWidget( - order=3, - values=[{"text": "cancel"}], - ) + # Get receiver name from response + receiver_name = "" + for item in payment_response.data.response: + if item.order == 1: + receiver_name = item.value + break - widgets = add_ui_to_widget( - { - get_receiver_id_by_receiver_phone_number_ui: WidgetInput( - widget=get_contacts_widget, - args={"cards": backend_output.cards}, - ), - build_buttons_row: WidgetInput( - widget=buttons, - args={ - "button_texts": ["cancel"], - "language": language, - }, - ), - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, - ), - }, - version, - ) + # Get amount and card info + amount = payment_response.additional.amount if payment_response.additional else "0" + card_info = f"Карта списания: ·· {payment_response.additional.sender_masked_pan[-4:] if payment_response.additional and payment_response.additional.sender_masked_pan else '****'}" - output = BuildOutput( - widgets_count=len(widgets), - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], + # Use smarty_ui payment_status_widget block component + payment_widget = smarty_payment_status_widget( + amount=str(amount), + recipient=receiver_name, + card_info=card_info, + status="success", + currency=texts_map[language]["currency"], ) - save_builder_output(context, output) - return output + # Wrap with margins + container = VStack([payment_widget]) + container.margins = dv.DivEdgeInsets(left=16, top=16, right=16, bottom=16) -def get_receiver_id_by_receiver_phone_number_ui( - cards: List[CardInfoByPhoneNumber], - language: LanguageOptions = LanguageOptions.RUSSIAN, -) -> Dict[str, Any]: - """ - Builds a UI showing a list of cards for a receiver with feedback handling. + return dv.make_div(container) - Args: - cards: List of card information - language: Language for localization - Returns: - DivKit JSON for the card selection UI - """ - feedback_texts = PAYMENT_FEEDBACK_TEXTS.get( - language, PAYMENT_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] - ) +class PayForHomeUtility(FunctionStrategy): + """Strategy for home utility payment UI with fallback.""" - card_items = [] - for idx, card in enumerate(cards, 1): - card_items.append( - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivContainer( - width=dv.DivFixedSize(value=36), - height=dv.DivFixedSize(value=36), - background=[dv.DivSolidBackground(color="#F3F6FA")], - border=dv.DivBorder( - stroke=dv.DivStroke( - color="#E5E7EB", - width=1, - ), - corner_radius=18, - ), - items=[ - dv.DivContainer( - width=dv.DivFixedSize(value=36), - height=dv.DivFixedSize(value=36), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - items=[ - dv.DivText( - text=str(idx), - font_size=18, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#3B82F6", - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - margins=dv.DivEdgeInsets( - top=7, left=12, right=12, bottom=7 - ), - ) - ], - ) - ], - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - margins=dv.DivEdgeInsets(right=12), - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=card.name, - font_size=16, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#222222", - line_height=20, - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivText( - text=card.processing, - font_size=15, - font_weight=dv.DivFontWeight.BOLD, - text_color="#1976D2", - line_height=18, - letter_spacing=0.2, - ), - dv.DivText( - text=" •• " + str(card.mask), - font_size=15, - text_color="#374151", - line_height=18, - margins=dv.DivEdgeInsets(left=1), - max_lines=1, - text_alignment_horizontal=dv.DivAlignmentHorizontal.LEFT, - ), - ], - margins=dv.DivEdgeInsets(top=2), - width=dv.DivWrapContentSize(), - ), - ], - ), - ], - paddings=dv.DivEdgeInsets(bottom=16, left=16, right=16, top=16), - background=[dv.DivSolidBackground(color="#FFFFFF")], - margins=dv.DivEdgeInsets(bottom=0 if idx == len(cards) else 8), - actions=[ - # Main selection action - dv.DivAction( - log_id=f"select_card_{idx}", - url=f"divkit://select?card_id={idx}", - payload={ - "card_id": card.pan, - "card_name": card.name, - "card_processing": card.processing, - "card_mask": card.mask, - }, - ), - # Success feedback action - dv.DivAction( - log_id=f"select_card_{idx}_success", - url="div-action://set_variable?name=card_selection_success&value=1", - ), - ], - ) + def build_widget_inputs(self, context): + backend_data = PaymentManagerPaymentResponse.model_validate( + context.backend_output ) - # Add divider except after last item - if idx < len(cards): - card_items.append( - dv.DivSeparator( - delimiter_style=dv.DivSeparatorDelimiterStyle( - color="#F3F6FA", - ), - width=dv.DivFixedSize(value=1), - margins=dv.DivEdgeInsets(left=52, right=0), - ) - ) - - # Success feedback container - success_container = dv.DivContainer( - id="card-selection-success", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{card_selection_success == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12, left=12, right=12, bottom=4), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["card_selected"], - font_size=13, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-card-selection-success", - url="div-action://set_variable?name=card_selection_success&value=0", - ) - ], - ), - ], - ) - - main_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - variables=[ - dv.IntegerVariable(name="card_selection_success", value=0), - dv.IntegerVariable(name="card_selection_error", value=0), - ], - items=[ - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=card_items, - background=[dv.DivSolidBackground(color="#F9FAFB")], - border=dv.DivBorder( - stroke=dv.DivStroke( - color="#E5E7EB", - width=1, - ), - corner_radius=16, + text_builder, text_input = self.make_text_input(context.llm_output) + return { + text_builder: text_input, + build_pay_for_home_utility_ui: WidgetInput( + widget=Widget( + order=2, + type="payment_status_widget", + name="payment_status_widget", + layout="vertical", + fields=["payment_status"], + values=[backend_data.model_dump()], ), - paddings=dv.DivEdgeInsets(bottom=0, left=0, right=0, top=0), - margins=dv.DivEdgeInsets(bottom=0, left=12, right=12, top=12), + args={"payment_response": backend_data}, ), - success_container, - ], - ) - - div = dv.make_div(main_container) - with open( - "logs/json/build_get_receiver_id_by_receiver_phone_number_ui.json", "w" - ) as f: - json.dump(div, f, ensure_ascii=False, indent=2) - return div - - -def get_categories(context) -> BuildOutput: - from models.context import Context - - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger + } - backend_output = CategoriesResponse(**backend_output) - backend_output_processed: List[Category] = backend_output.payload + def execute(self, context): + try: + return super().execute(context) + except Exception: + text_builder, text_input = self.make_text_input(context.llm_output) + return self._build_and_save(context, {text_builder: text_input}) - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - categories_widget = Widget( - name="payments_list_item_widget", - type="payments_list_item_widget", - order=2, - layout="vertical", - fields=["id", "name", "image_url"], - values=[category.model_dump() for category in backend_output_processed], - ) +pay_for_home_utility = PayForHomeUtility() - buttons = ButtonsWidget( - order=3, - values=[ - {"text": "Cancel", "action": "cancel"}, - ], - ) - widgets = add_ui_to_widget( - { - build_get_categories_ui: WidgetInput( - widget=categories_widget, - args={"categories": backend_output_processed}, - ), - build_buttons_row: WidgetInput( - widget=buttons, - args={ - "button_texts": ["cancel"], - "language": language, - }, - ), - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, - ), - }, - version, - ) - output = BuildOutput( - widgets_count=3, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output +# ============================================================================ +# Payment Status UI using smarty_ui +# ============================================================================ -def build_get_categories_ui( - categories: List[Category], +def build_payment_success_ui( + amount: str, + recipient: str, + card_last_digits: str, + icon_url: str | None = None, language: LanguageOptions = LanguageOptions.RUSSIAN, -): +) -> Dict[str, Any]: """ - Build categories UI with feedback handling for category selection. + Build payment success UI using smarty_ui payment_success_widget. Args: - categories: List of category objects + amount: Payment amount + recipient: Recipient/service name + card_last_digits: Last 4 digits of the card + icon_url: Optional merchant/service icon URL language: Language for localization Returns: - DivKit JSON for the categories UI + DivKit JSON for the payment success UI """ - feedback_texts = PAYMENT_FEEDBACK_TEXTS.get( - language, PAYMENT_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] - ) - - category_items = [] - for idx, category in enumerate(categories): - item_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - width=dv.DivFixedSize(value=303), - items=[ - dv.DivImage( - image_url=category.s3Url, - width=dv.DivFixedSize(value=24), - height=dv.DivFixedSize(value=24), - ), - dv.DivText( - text=category.name, - font_size=16, - font_weight=dv.DivFontWeight.REGULAR, - text_color="#000000", - margins=dv.DivEdgeInsets(left=14), - ), - ], - alignment_vertical=dv.DivAlignmentVertical.CENTER, - actions=[ - # Main selection action - dv.DivAction( - log_id=f"category_{category.id}_selected", - payload={ - "category_id": category.id, - "category_name": category.name, - }, - url=f"divkit://action?type=select_category&id={category.id}&name={category.name}", - ), - # Success feedback action - dv.DivAction( - log_id=f"category_{category.id}_success", - url="div-action://set_variable?name=category_selection_success&value=1", - ), - ], - ) - category_items.append(item_container) - - # Add separator with consistent spacing - if idx < len(categories) - 1: - category_items.append( - dv.DivSeparator( - delimiter_style=dv.DivSeparatorDelimiterStyle(color="#E0E0E0"), - margins=dv.DivEdgeInsets(top=12, bottom=12), - ) - ) + texts_map = { + LanguageOptions.UZBEK: {"currency": "so'm", "card_info": "Yechilgan karta"}, + LanguageOptions.RUSSIAN: {"currency": "сум", "card_info": "Карта списания"}, + LanguageOptions.ENGLISH: {"currency": "UZS", "card_info": "Card used"}, + } + texts = texts_map.get(language, texts_map[LanguageOptions.ENGLISH]) + card_info = f"{texts['card_info']}: ·· {card_last_digits}" - # Success feedback container - success_container = dv.DivContainer( - id="category-selection-success", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{category_selection_success == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12, bottom=8), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["category_selected"], - font_size=13, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-category-selection-success", - url="div-action://set_variable?name=category_selection_success&value=0", - ) - ], - ), - ], + # Use smarty_ui payment_success_widget + success_widget = payment_success_widget( + amount=amount, + recipient=recipient, + card_info=card_info, + icon_url=icon_url, + currency=texts["currency"], ) - category_items.append(success_container) - - main_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - width=dv.DivFixedSize(value=343), - height=dv.DivFixedSize(value=958), - background=[dv.DivSolidBackground(color="#FFFFFF")], - items=category_items, - border=dv.DivBorder(corner_radius=20), - paddings=dv.DivEdgeInsets(top=16, right=16, bottom=8, left=16), - margins=dv.DivEdgeInsets(left=16, top=657), - variables=[ - dv.IntegerVariable(name="category_selection_success", value=0), - ], - ) + # Wrap with margins + container = VStack([success_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) - div = dv.make_div(main_container) - with open("logs/json/build_get_categories_ui.json", "w") as f: + div = dv.make_div(container) + with open("logs/json/build_payment_success_ui.json", "w") as f: json.dump(div, f, ensure_ascii=False, indent=2) return div -def get_suppliers_by_category(context) -> BuildOutput: - from models.context import Context +def build_payment_failed_ui( + amount: str, + recipient: str, + card_last_digits: str, + icon_url: str | None = None, + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Build payment failed UI using smarty_ui payment_failed_widget. - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger + Args: + amount: Payment amount + recipient: Recipient/service name + card_last_digits: Last 4 digits of the card + icon_url: Optional merchant/service icon URL + language: Language for localization - backend_output = SupplierByCategoryResponse(**backend_output) - backend_output_processed: List[Supplier] = backend_output.payload + Returns: + DivKit JSON for the payment failed UI + """ + texts_map = { + LanguageOptions.UZBEK: {"currency": "so'm", "card_info": "Yechilgan karta"}, + LanguageOptions.RUSSIAN: {"currency": "сум", "card_info": "Карта списания"}, + LanguageOptions.ENGLISH: {"currency": "UZS", "card_info": "Card used"}, + } + texts = texts_map.get(language, texts_map[LanguageOptions.ENGLISH]) + card_info = f"{texts['card_info']}: ·· {card_last_digits}" - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], + # Use smarty_ui payment_failed_widget + failed_widget = payment_failed_widget( + amount=amount, + recipient=recipient, + card_info=card_info, + icon_url=icon_url, + currency=texts["currency"], ) - suppliers_widget = Widget( - name="payments_list_item_widget", - type="payments_list_item_widget", - order=2, - layout="vertical", - fields=["id", "name", "image_url", "category_id"], - values=[supplier.model_dump() for supplier in backend_output_processed], - ) - buttons_widget = ButtonsWidget( - order=3, - values=[ - {"text": "Cancel", "action": "cancel"}, - ], - ) + # Wrap with margins + container = VStack([failed_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) - widgets = add_ui_to_widget( - { - get_suppliers_by_category_ui: WidgetInput( - widget=suppliers_widget, - args={"suppliers": backend_output_processed}, - ), - build_buttons_row: WidgetInput( - widget=buttons_widget, - args={ - "button_texts": ["cancel"], - "language": language, - }, - ), - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, - ), - }, - version, - ) - output = BuildOutput( - widgets_count=3, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output + div = dv.make_div(container) + with open("logs/json/build_payment_failed_ui.json", "w") as f: + json.dump(div, f, ensure_ascii=False, indent=2) + return div -def get_suppliers_by_category_ui( - suppliers: List[Supplier], +def build_payment_pending_ui( + amount: str, + recipient: str, + card_last_digits: str, + icon_url: str | None = None, language: LanguageOptions = LanguageOptions.RUSSIAN, -): +) -> Dict[str, Any]: """ - Build suppliers UI with feedback handling for supplier selection. + Build payment pending UI using smarty_ui payment_pending_widget. Args: - suppliers: List of supplier objects + amount: Payment amount + recipient: Recipient/service name + card_last_digits: Last 4 digits of the card + icon_url: Optional merchant/service icon URL language: Language for localization Returns: - DivKit JSON for the suppliers UI + DivKit JSON for the payment pending UI """ - feedback_texts = PAYMENT_FEEDBACK_TEXTS.get( - language, PAYMENT_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] - ) - - supplier_items = [] - for idx, supplier in enumerate(suppliers): - item_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - width=dv.DivMatchParentSize(), - height=dv.DivWrapContentSize(), - alignment_vertical=dv.DivAlignmentVertical.CENTER, - background=[dv.DivSolidBackground(color="#EBF2FA")], - paddings=dv.DivEdgeInsets(top=16, bottom=16, left=16, right=16), - items=[ - dv.DivImage( - image_url=supplier.s3Url, - width=dv.DivFixedSize(value=32), - height=dv.DivFixedSize(value=32), - margins=dv.DivEdgeInsets(right=12), - scale=dv.DivImageScale.FIT, - border=dv.DivBorder(corner_radius=16), - ), - dv.DivText( - text=supplier.name, - font_size=16, - font_weight=dv.DivFontWeight.REGULAR, - text_color="#000000", - ), - ], - actions=[ - # Main selection action - dv.DivAction( - log_id=f"supplier_{supplier.id}_selected", - payload={ - "supplier_id": supplier.id, - "supplier_name": supplier.name, - }, - url=f"divkit://open_supplier?id={supplier.id}&name={supplier.name}", - ), - # Success feedback action - dv.DivAction( - log_id=f"supplier_{supplier.id}_success", - url="div-action://set_variable?name=supplier_selection_success&value=1", - ), - ], - ) - supplier_items.append(item_container) - - if idx < len(suppliers) - 1: - supplier_items.append( - dv.DivSeparator( - delimiter_style=dv.DivSeparatorDelimiterStyle(color="#E0E0E0"), - margins=dv.DivEdgeInsets(left=60, right=16), - ) - ) + texts_map = { + LanguageOptions.UZBEK: {"currency": "so'm", "card_info": "Yechilgan karta"}, + LanguageOptions.RUSSIAN: {"currency": "сум", "card_info": "Карта списания"}, + LanguageOptions.ENGLISH: {"currency": "UZS", "card_info": "Card used"}, + } + texts = texts_map.get(language, texts_map[LanguageOptions.ENGLISH]) + card_info = f"{texts['card_info']}: ·· {card_last_digits}" - # Success feedback container - success_container = dv.DivContainer( - id="supplier-selection-success", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{supplier_selection_success == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12, left=8, right=8, bottom=4), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["supplier_selected"], - font_size=13, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-supplier-selection-success", - url="div-action://set_variable?name=supplier_selection_success&value=0", - ) - ], - ), - ], + # Use smarty_ui payment_pending_widget + pending_widget = payment_pending_widget( + amount=amount, + recipient=recipient, + card_info=card_info, + icon_url=icon_url, + currency=texts["currency"], ) - supplier_items.append(success_container) - - main_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - width=dv.DivMatchParentSize(), - height=dv.DivWrapContentSize(), - background=[dv.DivSolidBackground(color="#FFFFFF")], - items=supplier_items, - border=dv.DivBorder(corner_radius=12), - margins=dv.DivEdgeInsets(top=8, bottom=8, left=8, right=8), - variables=[ - dv.IntegerVariable(name="supplier_selection_success", value=0), - ], - ) + # Wrap with margins + container = VStack([pending_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) - div = dv.make_div(main_container) - with open("logs/json/build_get_suppliers_by_category_ui.json", "w") as f: + div = dv.make_div(container) + with open("logs/json/build_payment_pending_ui.json", "w") as f: json.dump(div, f, ensure_ascii=False, indent=2) return div -class Field(BaseModel): - identName: Optional[str] = None - name: Optional[str] = None - order: Optional[int] = None - type: Optional[str] = None - pattern: Optional[str] = None - minValue: Optional[int] = None - maxValue: Optional[int] = None - fieldSize: Optional[int] = None - isMain: Optional[bool] = None - valueList: List[str] = [] +# ============================================================================ +# Home Balance Widget UI using smarty_ui +# ============================================================================ -def get_fields_of_supplier(context) -> BuildOutput: - from models.context import Context - - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - logger.info("get_fields_of_supplier") - backend_output_processed: List[Field] = [] - for i, field in enumerate(backend_output["payload"]["fieldList"]): - backend_output_processed.append( - Field( - identName=field["identName"], - name=field["name"], - order=field["order"], - type=field["type"], - pattern=field["pattern"], - minValue=field["minValue"], - maxValue=field["maxValue"], - fieldSize=field["fieldSize"], - isMain=field["isMain"], - valueList=[str(field["valueList"])], - ) - ) - if llm_output: - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - - fields_widget = Widget( - name="fields_widget", - type="fields_widget", - order=2, - layout="vertical", - fields=[ - "identName", - "name", - "order", - "type", - "pattern", - "minValue", - "maxValue", - "fieldSize", - "isMain", - "valueList", - ], - values=[field.model_dump() for field in backend_output_processed], - ) +def build_home_balance_widget_ui( + title: str, + subtitle: str, + utilities: List[Dict[str, Any]], + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Build home balance widget UI using smarty_ui home_balance_widget component. - button_widget = ButtonsWidget( - order=3, - values=[ - {"text": "Cancel", "action": "cancel"}, - ], - ) + Args: + title: Widget title (e.g., address) + subtitle: Widget subtitle (e.g., "Балансы") + utilities: List of utility balance data dicts with keys: + - utility_type: "electricity", "gas", "water", "recycling", or "other" + - balance: Formatted balance string + - currency: Currency suffix + - is_negative: Whether balance is negative + - action_url: Optional tap action URL + language: Language for localization - widgets = add_ui_to_widget( - { - get_fields_of_supplier_ui: WidgetInput( - widget=fields_widget, - args={"fields": backend_output_processed}, - ), - build_buttons_row: WidgetInput( - widget=button_widget, - args={ - "button_texts": [ - "cancel", - ], - "language": language, - }, - ), - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, - ), - }, - version, + Returns: + DivKit JSON for the home balance widget UI + """ + # Use smarty_ui home_balance_widget component + home_widget = home_balance_widget( + title=title, + subtitle=subtitle, + utilities=utilities, ) - if len(llm_output) > 0: - output = BuildOutput( - widgets_count=3, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - else: - output = BuildOutput( - widgets_count=2, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets[:2]], - ) - save_builder_output(context, output) - return output + # Wrap with margins + container = VStack([home_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) -def get_fields_of_supplier_ui(fields: List[Field]): - logger.info("get_fields_of_supplier_ui", fields=fields) - return - raise NotImplementedError() - div = dv.make_div( - dv.DivContainer( - orientation="vertical", - width=dv.DivFixedSize(value=343), - height=dv.DivFixedSize(value=555), - margins=dv.DivEdgeInsets(top=110, left=16), - items=[ - dv.DivContainer( - orientation="vertical", - width=dv.DivMatchParentSize(), - items=[ - dv.DivInput( - hint_text=str( - field.name - ), # Convert to string to ensure not None - font_size=16, - text_color="#1A1A1A", - hint_color="#999999", - margins=dv.DivEdgeInsets(bottom=16), - keyboard_type="text", # Ensure text input type - ) - for field in fields - ], - ) - ], - ) - ) - with open("logs/json/build_get_fields_of_supplier_ui.json", "w") as f: + div = dv.make_div(container) + with open("logs/json/build_home_balance_widget_ui.json", "w") as f: json.dump(div, f, ensure_ascii=False, indent=2) return div -def get_home_utility_suppliers(context) -> BuildOutput: - return get_home_balances(context) - +# Import at top level would cause circular import, so keep as function +def get_home_utility_suppliers(context): + from .balance import get_home_balances -if __name__ == "__main__": - # check get_contacts with action first - # output = get_receiver_id_by_receiver_phone_number( - # llm_output="", backend_output={"receiver_name": "Aslon"}, version="v3" - # ) - # with open("logs/json/test_response.json", "w") as f: - # json.dump(output.model_dump(), f) - data = [ - { - "pan": "kkkkkkxxxxxxyyyyyy", - "name": "Aslon", - "processing": "HUMO", - "mask": "*************1234", - }, - { - "pan": "kkkkkkxxxxxxyyasdfafsdfyyyy", - "name": "Aslon", - "processing": "HUMO", - "mask": "*************1235", - }, - { - "pan": "kkkkkkxxxxxxyyasdfafsdfyyyy", - "name": "Aslon", - "processing": "HUMO", - "mask": "*************1236", - }, - ] - context = Context( - llm_output="Hello world", - backend_output=CardsByPhoneNumberResponse.model_validate(data).model_dump(), - version="v3", - language=LanguageOptions.UZBEK, - api_key="test", - logger_context=LoggerContext( - chat_id="test", - logger=logger, - ), - ) - output = get_receiver_id_by_receiver_phone_number(context=context) - with open("logs/json/test_response.json", "w") as f: - json.dump(output.model_dump(), f, ensure_ascii=False, indent=2) + return get_home_balances(context) diff --git a/functions_to_format/functions/products.py b/functions_to_format/functions/products.py index 2545fe5..7b69670 100644 --- a/functions_to_format/functions/products.py +++ b/functions_to_format/functions/products.py @@ -11,9 +11,7 @@ from typing import List, Optional from functions_to_format.functions.general import ( Widget, - add_ui_to_widget, WidgetInput, - TextWidget, create_feedback_variables, create_success_actions, create_failure_actions, @@ -21,13 +19,24 @@ create_error_container, get_feedback_text, ) -from .general.utils import save_builder_output -from models.build import BuildOutput from conf import logger -from .general.text import build_text_widget from tool_call_models.smartbazar import SearchProductsResponse, ProductItem import structlog from models.context import Context +from .base_strategy import FunctionStrategy + +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + title_2, + text_1, + text_2, + caption_1, + caption_2, + icon, + default_theme, +) # ------------------------------------------------------------------- # Функция-шаблон: возвращает dv.DivState с двумя состояниями — collapsed @@ -37,66 +46,37 @@ # import json -def search_products(context: Context) -> BuildOutput: - return get_products(context=context) - - -def get_products(context: Context) -> BuildOutput: - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - backend_output_model: SearchProductsResponse = SearchProductsResponse( - **backend_output - ) - # logger.info(backend_output_model) - widget = Widget( - name="products_list_widget", - type="products_list_widget", - order=1, - layout="vertical", - fields=["products", "title"], - ) +class GetProducts(FunctionStrategy): + """Strategy for building products list UI.""" - products = backend_output_model.products - - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) + def build_widget_inputs(self, context): + backend_output_model = SearchProductsResponse(**context.backend_output) + products = backend_output_model.products + context.logger_context.logger.info("Products", products=products) - logger.info("Products", products=products) - widgets = add_ui_to_widget( - { + text_builder, text_input = self.make_text_input(context.llm_output) + return { build_products_list_widget: WidgetInput( - widget=widget, + widget=Widget( + name="products_list_widget", + type="products_list_widget", + order=1, + layout="vertical", + fields=["products", "title"], + ), args={ "products_list_input": products, - "language": language, - "chat_id": chat_id, - "api_key": api_key, + "language": context.language, + "chat_id": context.logger_context.chat_id, + "api_key": context.api_key, }, ), - build_text_widget: WidgetInput( - widget=text_widget, - args={ - "text": llm_output, - }, - ), - }, - version, - ) - output = BuildOutput( - widgets_count=1, - widgets=[w.model_dump(exclude_none=True) for w in widgets], - ) - save_builder_output(context, output) - return output + text_builder: text_input, + } + + +get_products = GetProducts() +search_products = get_products def make_product_state( @@ -184,133 +164,111 @@ def make_product_state( # elif p.price and p.price.monthly: # installment_price_text = f"{p.price.monthly} {texts_map[language]['summ']} x 12 {texts_map[language]['months']}" # TODO: uncomment this when installment service is back again. - # Collapsed state div - collapsed_div = dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - background=[dv.DivSolidBackground(color="#FFFFFF")], - margins=dv.DivEdgeInsets(bottom=8), - paddings=dv.DivEdgeInsets(left=12, right=12, top=12, bottom=12), - border=dv.DivBorder(corner_radius=8, stroke=dv.DivStroke(color="#E5E7EB")), - items=[ - dv.DivImage( - image_url=main_image_mobile_url, - width=dv.DivFixedSize(value=40), - height=dv.DivFixedSize(value=40), - scale=dv.DivImageScale.FILL, - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - width=dv.DivWrapContentSize(), - items=[ - dv.DivText( - text=p.name, font_size=14, font_weight=dv.DivFontWeight.MEDIUM - ), - dv.DivText( - text=price_text, - font_size=13, - text_color="#6B7280", - ), - ], - ), - ], - actions=[ - dv.DivAction( - log_id=f"expand_product_{p.id}_{index}", - url=f"sample-actions://set_state?state_id=0/{product_state_id}/expanded", - ), - dv.DivAction( - log_id=f"expand_product_{p.id}_{index}_div", - url=f"div-action://set_state?state_id=0/{product_state_id}/expanded", - ), - ], + # Collapsed state div using smarty_ui + product_image = dv.DivImage( + image_url=main_image_mobile_url, + width=dv.DivFixedSize(value=40), + height=dv.DivFixedSize(value=40), + scale=dv.DivImageScale.FILL, ) - # Success feedback container for cart actions - success_container = dv.DivContainer( - id=f"success-{feedback_prefix}", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr( - f"@{{{feedback_prefix}_success_visible == 1 ? 'visible' : 'gone'}}" + product_name = text_1(p.name, color="#111133") + product_name.font_weight = dv.DivFontWeight.MEDIUM + + product_price = caption_1(price_text, color="#6B7280") + + product_info = VStack([product_name, product_price]) + product_info.width = dv.DivWrapContentSize() + + collapsed_div = HStack( + [product_image, product_info], + padding=12, + background="#FFFFFF", + corner_radius=8, + ) + collapsed_div.margins = dv.DivEdgeInsets(bottom=8) + collapsed_div.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#E5E7EB") + ) + collapsed_div.actions = [ + dv.DivAction( + log_id=f"expand_product_{p.id}_{index}", + url=f"sample-actions://set_state?state_id=0/{product_state_id}/expanded", ), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=8, bottom=8), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#A7F3D0", width=1) + dv.DivAction( + log_id=f"expand_product_{p.id}_{index}_div", + url=f"div-action://set_state?state_id=0/{product_state_id}/expanded", ), - items=[ - dv.DivText( - text="✅", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=texts_map[language]["added_to_cart"], - font_size=13, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8, right=4), - actions=[ - dv.DivAction( - log_id=f"dismiss-success-{feedback_prefix}", - url=f"div-action://set_variable?name={feedback_prefix}_success_visible&value=0", - ) - ], - ), - ], + ] + + # Success feedback container for cart actions using smarty_ui + success_icon = caption_1("✅") + success_icon.margins = dv.DivEdgeInsets(right=8) + + success_text = caption_1(texts_map[language]["added_to_cart"], color="#065F46") + success_text.font_weight = dv.DivFontWeight.MEDIUM + success_text.width = dv.DivMatchParentSize(weight=1) + + dismiss_success = caption_1("✕", color="#065F46") + dismiss_success.font_size = 16 + dismiss_success.font_weight = dv.DivFontWeight.BOLD + dismiss_success.paddings = dv.DivEdgeInsets(left=8, right=4) + dismiss_success.actions = [ + dv.DivAction( + log_id=f"dismiss-success-{feedback_prefix}", + url=f"div-action://set_variable?name={feedback_prefix}_success_visible&value=0", + ) + ] + + success_container = HStack( + [success_icon, success_text, dismiss_success], + padding=10, + background="#ECFDF5", + corner_radius=8, + width=dv.DivMatchParentSize(), + ) + success_container.id = f"success-{feedback_prefix}" + success_container.visibility = Expr( + f"@{{{feedback_prefix}_success_visible == 1 ? 'visible' : 'gone'}}" + ) + success_container.margins = dv.DivEdgeInsets(top=8, bottom=8) + success_container.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#A7F3D0", width=1) ) - # Error feedback container for cart actions - error_container = dv.DivContainer( - id=f"error-{feedback_prefix}", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr( - f"@{{{feedback_prefix}_error_visible == 1 ? 'visible' : 'gone'}}" - ), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + # Error feedback container for cart actions using smarty_ui + error_icon = caption_1("⚠️") + error_icon.margins = dv.DivEdgeInsets(right=8) + + error_text = caption_1(texts_map[language]["cart_error"], color="#B91C1C") + error_text.font_weight = dv.DivFontWeight.MEDIUM + error_text.width = dv.DivMatchParentSize(weight=1) + + dismiss_error = caption_1("✕", color="#B91C1C") + dismiss_error.font_size = 16 + dismiss_error.font_weight = dv.DivFontWeight.BOLD + dismiss_error.paddings = dv.DivEdgeInsets(left=8, right=4) + dismiss_error.actions = [ + dv.DivAction( + log_id=f"dismiss-error-{feedback_prefix}", + url=f"div-action://set_variable?name={feedback_prefix}_error_visible&value=0", + ) + ] + + error_container = HStack( + [error_icon, error_text, dismiss_error], + padding=10, + background="#FEF2F2", + corner_radius=8, width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=8, bottom=8), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#FEF2F2")], - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#FECACA", width=1) - ), - items=[ - dv.DivText( - text="⚠️", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=texts_map[language]["cart_error"], - font_size=13, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8, right=4), - actions=[ - dv.DivAction( - log_id=f"dismiss-error-{feedback_prefix}", - url=f"div-action://set_variable?name={feedback_prefix}_error_visible&value=0", - ) - ], - ), - ], + ) + error_container.id = f"error-{feedback_prefix}" + error_container.visibility = Expr( + f"@{{{feedback_prefix}_error_visible == 1 ? 'visible' : 'gone'}}" + ) + error_container.margins = dv.DivEdgeInsets(top=8, bottom=8) + error_container.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#FECACA", width=1) ) # Create on_success actions for cart add @@ -337,217 +295,162 @@ def make_product_state( ), ] - # Expanded state div - expanded_div = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - background=[dv.DivSolidBackground(color="#FFFFFF")], - border=dv.DivBorder(corner_radius=12, stroke=dv.DivStroke(color="#E5E7EB")), - paddings=dv.DivEdgeInsets(left=12, right=12, top=12, bottom=12), - margins=dv.DivEdgeInsets(bottom=8), - variables=[ - dv.IntegerVariable(name=f"{feedback_prefix}_success_visible", value=0), - dv.IntegerVariable(name=f"{feedback_prefix}_error_visible", value=0), - ], - items=[ - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=[ - dv.DivImage( - image_url=main_image_mobile_url, - width=dv.DivFixedSize(value=40), - height=dv.DivFixedSize(value=40), - scale=dv.DivImageScale.FILL, - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - width=dv.DivWrapContentSize(), - items=[ - dv.DivText( - text=p.name, - font_size=14, - font_weight=dv.DivFontWeight.MEDIUM, - ), - dv.DivText( - text=price_text, - font_size=13, - text_color="#6B7280", - ), - ], - ), - ], - actions=[ - dv.DivAction( - log_id=f"collapse_product_{p.id}_{index}", - url=f"sample-actions://set_state?state_id=0/{product_state_id}/collapsed", - ), - dv.DivAction( - log_id=f"collapse_product_{p.id}_{index}_div", - url=f"div-action://set_state?state_id=0/{product_state_id}/collapsed", + # Expanded state header row using smarty_ui + exp_product_image = dv.DivImage( + image_url=main_image_mobile_url, + width=dv.DivFixedSize(value=40), + height=dv.DivFixedSize(value=40), + scale=dv.DivImageScale.FILL, + ) + + exp_product_name = text_1(p.name, color="#111133") + exp_product_name.font_weight = dv.DivFontWeight.MEDIUM + + exp_product_price = caption_1(price_text, color="#6B7280") + + exp_product_info = VStack([exp_product_name, exp_product_price]) + exp_product_info.width = dv.DivWrapContentSize() + + exp_header_row = HStack([exp_product_image, exp_product_info]) + exp_header_row.actions = [ + dv.DivAction( + log_id=f"collapse_product_{p.id}_{index}", + url=f"sample-actions://set_state?state_id=0/{product_state_id}/collapsed", + ), + dv.DivAction( + log_id=f"collapse_product_{p.id}_{index}_div", + url=f"div-action://set_state?state_id=0/{product_state_id}/collapsed", + ), + ] + + # Main product image + main_product_image = dv.DivImage( + image_url=main_image_mobile_url, + width=dv.DivMatchParentSize(), + height=dv.DivWrapContentSize(), + margins=dv.DivEdgeInsets(top=12), + scale=dv.DivImageScale.FIT, + alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + alignment_vertical=dv.DivAlignmentVertical.CENTER, + ) + + # Product details using smarty_ui + detail_name = caption_1(p.name, color="#374151") + detail_name.margins = dv.DivEdgeInsets(top=8) + + rating_text = caption_2(f"⭐ {p.rate or 0}", color="#6B7280") + rating_text.margins = dv.DivEdgeInsets(top=4) + + installment_label = caption_2(installment_price_text, color="#DB2777") + installment_label.background = [dv.DivSolidBackground(color="#FCE7F3")] + installment_label.paddings = dv.DivEdgeInsets(left=6, top=4, right=6, bottom=4) + installment_label.margins = dv.DivEdgeInsets(top=6) + installment_label.border = dv.DivBorder( + corner_radius=4, stroke=dv.DivStroke(color="#FCE7F3") + ) + + # Build buttons using smarty_ui text components + hide_btn = text_1(texts_map[language]["hide"], color="#6B7280") + hide_btn.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#D1D5DB") + ) + hide_btn.height = dv.DivFixedSize(value=36) + hide_btn.width = dv.DivMatchParentSize() + hide_btn.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + hide_btn.alignment_vertical = dv.DivAlignmentVertical.CENTER + hide_btn.text_alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + hide_btn.text_alignment_vertical = dv.DivAlignmentVertical.CENTER + hide_btn.paddings = dv.DivEdgeInsets(left=16, right=16) + hide_btn.margins = dv.DivEdgeInsets(left=4) if p.offer_id else dv.DivEdgeInsets() + hide_btn.actions = [ + dv.DivAction( + log_id=f"collapse_from_button_{p.id}_{index}", + url=f"sample-actions://set_state?state_id=0/{product_state_id}/collapsed", + ), + dv.DivAction( + log_id=f"collapse_from_button_{p.id}_{index}_div", + url=f"div-action://set_state?state_id=0/{product_state_id}/collapsed", + ), + ] + + if p.offer_id: + add_btn = text_1(texts_map[language]["add"], color="#2563EB") + add_btn.border = dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#3B82F6") + ) + add_btn.height = dv.DivFixedSize(value=36) + add_btn.width = dv.DivMatchParentSize() + add_btn.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + add_btn.alignment_vertical = dv.DivAlignmentVertical.CENTER + add_btn.text_alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + add_btn.text_alignment_vertical = dv.DivAlignmentVertical.CENTER + add_btn.paddings = dv.DivEdgeInsets(left=16, right=16) + add_btn.margins = dv.DivEdgeInsets(right=4) + add_btn.actions = [ + dv.DivAction( + url="divkit://send-request", + log_id=f"add_to_cart_{p.id}_{index}", + typed=dv.DivActionSubmit( + container_id=cart_container, + request=dv.DivActionSubmitRequest( + url=f"https://smarty.smartbank.uz/chat/v3/tools/call?function_name=add_product_to_cart&chat_id={chat_id}&arguments={json.dumps({'offer_id': p.offer_id, 'quantity': 1})}", + method=dv.RequestMethod.POST, + headers=[dv.RequestHeader(name="api-key", value=api_key)], ), - ], - ), - dv.DivImage( - image_url=main_image_mobile_url, - width=dv.DivMatchParentSize(), - height=dv.DivWrapContentSize(), - margins=dv.DivEdgeInsets(top=12), - scale=dv.DivImageScale.FIT, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - ), - dv.DivText( - text=p.name, - font_size=13, - text_color="#374151", - margins=dv.DivEdgeInsets(top=8), - ), - dv.DivText( - text=f"⭐ {p.rate or 0}", - font_size=11, - text_color="#6B7280", - margins=dv.DivEdgeInsets(top=4), - ), - dv.DivText( - text=installment_price_text, - font_size=11, - text_color="#DB2777", - background=[dv.DivSolidBackground(color="#FCE7F3")], - paddings=dv.DivEdgeInsets(left=6, top=4, right=6, bottom=4), - margins=dv.DivEdgeInsets(top=6), - border=dv.DivBorder( - corner_radius=4, stroke=dv.DivStroke(color="#FCE7F3") + on_success_actions=cart_success_actions, + on_fail_actions=cart_fail_actions, ), + payload={ + "function_name": "add_product_to_cart", + "chat_id": chat_id, + "arguments": { + "offer_id": p.offer_id, + "quantity": 1, + }, + }, ), + ] + buttons_items = [add_btn, hide_btn] + else: + hide_btn.margins = dv.DivEdgeInsets() + buttons_items = [hide_btn] + + # Buttons container using HStack + buttons_row = HStack(buttons_items, width=dv.DivMatchParentSize()) + buttons_row.margins = dv.DivEdgeInsets(top=12) + buttons_row.id = cart_container + buttons_row.content_alignment_horizontal = ( + dv.DivContentAlignmentHorizontal.SPACE_BETWEEN + if p.offer_id + else dv.DivContentAlignmentHorizontal.CENTER + ) + buttons_row.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + + # Expanded state div using VStack + expanded_div = VStack( + [ + exp_header_row, + main_product_image, + detail_name, + rating_text, + installment_label, # Feedback containers success_container, error_container, - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - margins=dv.DivEdgeInsets(top=12), - width=dv.DivMatchParentSize(), - content_alignment_horizontal=( - dv.DivContentAlignmentHorizontal.SPACE_BETWEEN - if p.offer_id - else dv.DivContentAlignmentHorizontal.CENTER - ), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - id=cart_container, - items=( # pyright: ignore[reportArgumentType] - [ - ( - dv.DivText( - text=texts_map[language]["add"], - border=dv.DivBorder( - corner_radius=8, - stroke=dv.DivStroke(color="#3B82F6"), - ), - text_color="#2563EB", - height=dv.DivFixedSize(value=36), - width=dv.DivMatchParentSize(), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - text_alignment_vertical=dv.DivAlignmentVertical.CENTER, - paddings=dv.DivEdgeInsets(left=16, right=16), - margins=( - dv.DivEdgeInsets(right=4) - if p.offer_id - else dv.DivEdgeInsets() - ), - actions=[ - dv.DivAction( - url="divkit://send-request", - log_id=f"add_to_cart_{p.id}_{index}", - typed=dv.DivActionSubmit( - container_id=cart_container, - request=dv.DivActionSubmitRequest( - url=f"https://smarty.smartbank.uz/chat/v3/tools/call?function_name=add_product_to_cart&chat_id={chat_id}&arguments={json.dumps({'offer_id': p.offer_id, 'quantity': 1})}", - method=dv.RequestMethod.POST, - headers=[ - dv.RequestHeader( - name="api-key", value=api_key - ) - ], - ), - on_success_actions=cart_success_actions, - on_fail_actions=cart_fail_actions, - ), - payload={ - "function_name": "add_product_to_cart", - "chat_id": chat_id, - "arguments": { - "offer_id": p.offer_id, - "quantity": 1, - }, - }, - ), - ], - ) - if p.offer_id - else None - ), - dv.DivText( - text=texts_map[language]["hide"], - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#D1D5DB") - ), - text_color="#6B7280", - height=dv.DivFixedSize(value=36), - width=dv.DivMatchParentSize(), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - text_alignment_vertical=dv.DivAlignmentVertical.CENTER, - paddings=dv.DivEdgeInsets(left=16, right=16), - margins=( - dv.DivEdgeInsets(left=4) - if p.offer_id - else dv.DivEdgeInsets() - ), - actions=[ - dv.DivAction( - log_id=f"collapse_from_button_{p.id}_{index}", - url=f"sample-actions://set_state?state_id=0/{product_state_id}/collapsed", - ), - dv.DivAction( - log_id=f"collapse_from_button_{p.id}_{index}_div", - url=f"div-action://set_state?state_id=0/{product_state_id}/collapsed", - ), - ], - ), - ] - if p.offer_id - else [ - dv.DivText( - text=texts_map[language]["hide"], - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#D1D5DB") - ), - text_color="#6B7280", - height=dv.DivFixedSize(value=36), - width=dv.DivMatchParentSize(), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - alignment_vertical=dv.DivAlignmentVertical.CENTER, - text_alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - text_alignment_vertical=dv.DivAlignmentVertical.CENTER, - paddings=dv.DivEdgeInsets(left=16, right=16), - margins=dv.DivEdgeInsets(), - actions=[ - dv.DivAction( - log_id=f"collapse_from_button_{p.id}_{index}", - url=f"sample-actions://set_state?state_id=0/{product_state_id}/collapsed", - ), - dv.DivAction( - log_id=f"collapse_from_button_{p.id}_{index}_div", - url=f"div-action://set_state?state_id=0/{product_state_id}/collapsed", - ), - ], - ), - ] - ), - ), + buttons_row, ], + padding=12, + background="#FFFFFF", + corner_radius=12, ) + expanded_div.margins = dv.DivEdgeInsets(bottom=8) + expanded_div.border = dv.DivBorder( + corner_radius=12, stroke=dv.DivStroke(color="#E5E7EB") + ) + expanded_div.variables = [ + dv.IntegerVariable(name=f"{feedback_prefix}_success_visible", value=0), + dv.IntegerVariable(name=f"{feedback_prefix}_error_visible", value=0), + ] return dv.DivState( id=product_state_id, @@ -565,7 +468,7 @@ def build_products_list_widget( chat_id: str = "BLABLABLA", api_key: str = "BLABLABLA", ): - """Build products list widget using pydivkit SDK classes""" + """Build products list widget using smarty_ui components.""" logger = structlog.get_logger() logger.info( "Starting build_products_list_widget", @@ -590,19 +493,15 @@ def build_products_list_widget( if not products_list_input: logger.warning("No products provided, returning empty state") - empty_div = dv.DivText( - text=texts_map[language]["no_products"], - paddings=dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16), - ) + empty_text = text_1(texts_map[language]["no_products"], color="#6B7280") + empty_text.paddings = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) - return dv.make_div( - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - width=dv.DivMatchParentSize(), - paddings=dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16), - items=[empty_div], - ) + empty_container = VStack( + [empty_text], + padding=16, + width=dv.DivMatchParentSize(), ) + return dv.make_div(empty_container) logger.info( "Creating product state elements", products_count=len(products_list_input) @@ -622,12 +521,11 @@ def build_products_list_widget( logger.info( "Creating main container with product states", states_count=len(product_states) ) - # Main container holding all products - main_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, + # Main container holding all products using VStack + main_container = VStack( + product_states, + padding=16, width=dv.DivMatchParentSize(), - paddings=dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16), - items=product_states, ) logger.info("Converting to div and saving output") diff --git a/functions_to_format/functions/start_page.py b/functions_to_format/functions/start_page.py index a10781e..5b2eae2 100644 --- a/functions_to_format/functions/start_page.py +++ b/functions_to_format/functions/start_page.py @@ -1,40 +1,23 @@ -from .general import Widget, WidgetInput, TextWidget, add_ui_to_widget -from .general.utils import save_builder_output -from models.build import BuildOutput +from .general import Widget, WidgetInput +from .base_strategy import FunctionStrategy from functions_to_format.functions.general.const_values import LanguageOptions -from conf import logger -import structlog from models.context import Context -def start_page_widget(context: Context) -> BuildOutput: - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger +class StartPageWidget(FunctionStrategy): + """Strategy for building start page UI.""" + def build_widget_inputs(self, context): + return { + build_start_page_widget: WidgetInput( + widget=Widget( + name="start_page", type="start_page", + order=1, layout="vertical", fields=[], + ), + args={}, + ), + } - a = Widget( - name="start_page", - type="start_page", - order=1, - layout="vertical", - fields=[], - ) - widgets = add_ui_to_widget( - widget_inputs={build_start_page_widget: WidgetInput(widget=a, args={})}, - version=version, - ) - - output = BuildOutput( - widgets_count=len(widgets), - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output +start_page_widget = StartPageWidget() def build_start_page_widget( diff --git a/functions_to_format/functions/transfer.py b/functions_to_format/functions/transfer.py new file mode 100644 index 0000000..f1e2333 --- /dev/null +++ b/functions_to_format/functions/transfer.py @@ -0,0 +1,732 @@ +import json +from datetime import datetime + +from .base_strategy import FunctionStrategy +from .general import ( + Widget, + ButtonsWidget, + WidgetInput, +) +from .buttons import build_buttons_row +from pydantic import BaseModel +from typing import List, Dict, Any, Optional, Union +import pydivkit as dv +from pydivkit.core import Expr +from tool_call_models.cards import CardsByPhoneNumberResponse, CardInfoByPhoneNumber + +from conf import logger +from functions_to_format.functions.general.const_values import LanguageOptions +from models.context import Context, LoggerContext +from smarty_ui.blocks.send_money_widget import send_money_widget + + +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + title_1, + title_2, + text_1, +) +from smarty_ui.composites import assistant_bubble + +# Import smarty_ui blocks for ready-made components +from smarty_ui.blocks import ( + receiver_cards_list, + contacts_list, + cards_own_list, + transaction_status_widget, + transaction_success_widget, + transaction_failed_widget, +) +from smarty_ui.blocks.contacts_list import search_for_contacts +from smarty_ui.primitives import smarty_button, smarty_button_filled +# ============================================================================ +# Receiver by Card Models and UI +# ============================================================================ + + +class ReceiverByCardResponse(BaseModel): + """Model for receiver information retrieved by card number.""" + + processingSystem: str + iconMini: Optional[str] = None + isFound: bool + maskedPan: str + errorMessage: Optional[str] = None + icon: Optional[str] = None + fullName: Optional[str] = None + errorCode: Optional[str] = None + token: Optional[str] = None + + +# Feedback texts for money transfer actions +TRANSFER_FEEDBACK_TEXTS = { + LanguageOptions.RUSSIAN: { + "receiver_found": "Получатель найден", + "receiver_not_found": "Получатель не найден", + "transfer_success": "Перевод выполнен успешно!", + "transfer_error": "Ошибка перевода", + "confirm_transfer": "Подтвердить перевод", + "cancel": "Отмена", + "amount": "Сумма", + "recipient": "Получатель", + "card": "Карта", + "currency": "сум", + "send": "Отправить", + "processing": "Обработка...", + }, + LanguageOptions.ENGLISH: { + "receiver_found": "Recipient found", + "receiver_not_found": "Recipient not found", + "transfer_success": "Transfer completed successfully!", + "transfer_error": "Transfer failed", + "confirm_transfer": "Confirm transfer", + "cancel": "Cancel", + "amount": "Amount", + "recipient": "Recipient", + "card": "Card", + "currency": "UZS", + "send": "Send", + "processing": "Processing...", + }, + LanguageOptions.UZBEK: { + "receiver_found": "Qabul qiluvchi topildi", + "receiver_not_found": "Qabul qiluvchi topilmadi", + "transfer_success": "O'tkazma muvaffaqiyatli bajarildi!", + "transfer_error": "O'tkazma xatosi", + "confirm_transfer": "O'tkazmani tasdiqlash", + "cancel": "Bekor qilish", + "amount": "Summa", + "recipient": "Qabul qiluvchi", + "card": "Karta", + "currency": "so'm", + "send": "Yuborish", + "processing": "Qayta ishlanmoqda...", + }, +} + + +def get_processing_system_icon(processing_system: str) -> str: + """Get icon URL for a processing system.""" + icons = { + "VISA": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/Visa_Inc._logo.svg/200px-Visa_Inc._logo.svg.png", + "MASTERCARD": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Mastercard-logo.svg/200px-Mastercard-logo.svg.png", + "HUMO": "https://humocard.uz/bitrix/templates/main/img/card2.png", + "UZCARD": "https://uzcard.uz/upload/images/logo.png", + } + return icons.get(processing_system.upper(), "https://via.placeholder.com/48x32") + + +def build_receiver_by_card_ui( + receiver_data: ReceiverByCardResponse, + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Build UI for displaying receiver information retrieved by card number using smarty_ui. + + Args: + receiver_data: Receiver information from backend + language: Language for localization + + Returns: + DivKit JSON for the receiver card UI + """ + feedback_texts = TRANSFER_FEEDBACK_TEXTS.get( + language, TRANSFER_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) + + # Determine icon URL + icon_url = receiver_data.icon or get_processing_system_icon( + receiver_data.processingSystem + ) + + if not receiver_data.isFound: + # Error state - receiver not found using smarty_ui + error_icon = title_1("❌") + error_icon.margins = dv.DivEdgeInsets(right=12) + + error_title = title_2(feedback_texts["receiver_not_found"], color="#B91C1C") + + error_header = HStack([error_icon, error_title], align_h="center") + error_header.margins = dv.DivEdgeInsets(bottom=12) + + error_message = text_1( + receiver_data.errorMessage or feedback_texts["transfer_error"], + color="#7F1D1D", + ) + error_message.text_alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + + error_container = VStack( + [error_header, error_message], + padding=20, + background="#FEF2F2", + corner_radius=16, + ) + error_container.border = dv.DivBorder( + corner_radius=16, stroke=dv.DivStroke(color="#FECACA", width=1) + ) + error_container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) + + return dv.make_div(error_container) + + # Success state - receiver found using smarty_ui receiver_cards_list + # Build receiver card data for the component + receiver_card_data = [ + { + "holder_name": receiver_data.fullName or "Unknown", + "card_last_digits": receiver_data.maskedPan[-4:] + if receiver_data.maskedPan + else "****", + "provider_logo_url": icon_url, + "action_url": f"divkit://confirm_receiver?token={receiver_data.token}&fullName={receiver_data.fullName}&maskedPan={receiver_data.maskedPan}&processingSystem={receiver_data.processingSystem}", + } + ] + + # Use smarty_ui receiver_cards_list component + cards_widget = receiver_cards_list(receiver_card_data) + + # Success header using smarty_ui + success_icon = text_1("✓", color="#059669") + success_icon.font_weight = dv.DivFontWeight.BOLD + success_icon.font_size = 20 + success_icon.width = dv.DivFixedSize(value=40) + success_icon.height = dv.DivFixedSize(value=40) + success_icon.background = [dv.DivSolidBackground(color="#ECFDF5")] + success_icon.border = dv.DivBorder(corner_radius=20) + success_icon.text_alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + success_icon.text_alignment_vertical = dv.DivAlignmentVertical.CENTER + success_icon.margins = dv.DivEdgeInsets(right=12) + + success_text = text_1(feedback_texts["receiver_found"], color="#059669") + success_text.font_weight = dv.DivFontWeight.MEDIUM + + success_header = HStack( + [success_icon, success_text], align_v="center", align_h="center" + ) + success_header.margins = dv.DivEdgeInsets(bottom=16) + + # Confirm button using smarty_button_filled + confirm_btn = smarty_button_filled( + text=feedback_texts["confirm_transfer"], + action_url=f"divkit://confirm_receiver?token={receiver_data.token}", + ) + confirm_btn.width = dv.DivMatchParentSize() + confirm_btn.height = dv.DivFixedSize(value=44) + confirm_btn.border = dv.DivBorder(corner_radius=10) + confirm_btn.actions = [ + dv.DivAction( + log_id="confirm_receiver_selection", + url="divkit://confirm_receiver", + payload={ + "token": receiver_data.token, + "fullName": receiver_data.fullName, + "maskedPan": receiver_data.maskedPan, + "processingSystem": receiver_data.processingSystem, + }, + ), + ] + confirm_btn.margins = dv.DivEdgeInsets(top=16) + + # Main container using VStack + main_container = VStack( + [success_header, cards_widget, confirm_btn], + padding=20, + background="#FFFFFF", + corner_radius=16, + ) + main_container.border = dv.DivBorder( + corner_radius=16, stroke=dv.DivStroke(color="#E5E7EB", width=1) + ) + main_container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) + + div = dv.make_div(main_container) + with open("logs/json/build_receiver_by_card_ui.json", "w") as f: + json.dump(div, f, ensure_ascii=False, indent=2) + return div + + +class GetReceiverByCard(FunctionStrategy): + """Strategy for receiver by card UI.""" + + def build_widget_inputs(self, context): + logger = context.logger_context.logger + logger.info("get_receiver_by_card", backend_output=context.backend_output) + try: + receiver_data = ReceiverByCardResponse(**context.backend_output) + except Exception as e: + logger.error("Error parsing receiver data", error=str(e)) + receiver_data = ReceiverByCardResponse( + processingSystem="UNKNOWN", + isFound=False, + maskedPan="****", + errorMessage=str(e), + ) + text_builder, text_input = self.make_text_input(context.llm_output) + return { + text_builder: text_input, + build_receiver_by_card_ui: WidgetInput( + widget=Widget( + name="receiver_by_card_widget", + type="receiver_by_card_widget", + order=2, + layout="vertical", + fields=[ + "processingSystem", + "isFound", + "maskedPan", + "fullName", + "token", + ], + values=[receiver_data.model_dump(exclude_none=True)], + ), + args={"receiver_data": receiver_data, "language": context.language}, + ), + } + + +get_receiver_by_card = GetReceiverByCard() + + +# ============================================================================ +# Send Money Transfer UI +# ============================================================================ + + +def build_send_money_ui( + recipient_name: str, + source_card_name: str, + amount: str, + *, + recipient_card_digits: str | None = None, + source_card_digits: str | None = None, + recipient_icon_url: str | None = None, + source_card_icon_url: str | None = None, + wallet_icon_url: str | None = None, + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Build UI for confirming and sending money transfer. + + Args: + amount: Transfer amount in minor units (e.g., tiyin) + receiver_name: Recipient's full name + masked_pan: Masked card PAN + processing_system: Card processing system (VISA, HUMO, etc.) + token: Transfer token for API + chat_id: Chat ID for API call + api_key: API key for authorization + language: Language for localization + + Returns: + DivKit JSON for the transfer confirmation UI + """ + + currency = TRANSFER_FEEDBACK_TEXTS.get( + language, TRANSFER_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + )["currency"] + button_text = TRANSFER_FEEDBACK_TEXTS.get( + language, TRANSFER_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + )["confirm_transfer"] + action_url = f"divkit://confirm_transfer?token=xxxx" + + return dv.make_div( + send_money_widget( + amount=str(amount), + recipient_name=recipient_name, + source_card_name=source_card_name, + recipient_card_digits=recipient_card_digits, + source_card_digits=source_card_digits, + recipient_icon_url=recipient_icon_url, + source_card_icon_url=source_card_icon_url, + wallet_icon_url=wallet_icon_url, + currency=currency, + button_text=button_text, + action_url=action_url, + ) + ) + + +class SendMoneyToSomeoneViaCard(FunctionStrategy): + """Strategy for money transfer confirmation UI.""" + + def build_widget_inputs(self, context): + bo = context.backend_output + context.logger_context.logger.info( + "send_money_to_someone_via_card", backend_output=bo + ) + amount = bo.get("amount", 0) + recipient_name = bo.get("fullName") or bo.get("card_owner_name", "Unknown") + recipient_card_digits = bo.get("maskedPan") or bo.get("marked_card_pan", "****") + recipient_icon_url = bo.get("icon") or bo.get("card_icon", "****") + + source_card_name = bo.get("card_name") or bo.get("card_owner_name", "Unknown") + source_card_digits = bo.get("maskedPan") or bo.get("marked_card_pan", "****") + source_card_icon_url = bo.get("icon") or bo.get("card_icon", "****") + + masked_pan = bo.get("maskedPan") or bo.get("marked_card_pan", "****") + processing_system = bo.get("processingSystem", "UNKNOWN") + token = bo.get("token") or bo.get("to_card_id", "") + + text_builder, text_input = self.make_text_input(context.llm_output) + return { + text_builder: text_input, + build_send_money_ui: WidgetInput( + widget=Widget( + name="send_money_widget", + type="send_money_widget", + order=2, + layout="vertical", + fields=[ + "amount", + "receiver_name", + "masked_pan", + "processing_system", + "token", + ], + values=[ + { + "amount": 1000, + "recipient_name": recipient_name, + "source_card_name": source_card_name, + "recipient_card_digits": recipient_card_digits, + "source_card_digits": source_card_digits, + "recipient_icon_url": recipient_icon_url, + "source_card_icon_url": source_card_icon_url, + "masked_pan": masked_pan, + "processing_system": processing_system, + "token": token, + } + ], + ), + args={ + "recipient_name": recipient_name, + "source_card_name": source_card_name, + "amount": 0, + "recipient_card_digits": recipient_card_digits, + "source_card_digits": source_card_digits, + "recipient_icon_url": recipient_icon_url, + "source_card_icon_url": source_card_icon_url, + }, + ), + } + + +send_money_to_someone_via_card = SendMoneyToSomeoneViaCard() + + +def get_number_by_reciver_name_ui( + receiver_name: Union[str, List[str]], + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + logger.info("get_number_by_reciver_number_ui") + if isinstance(receiver_name, list): + search_contact_action = dv.DivAction( + log_id="search_contact", + url=f"divkit://search_contact?name={receiver_name}", # Custom scheme the iOS app will catch + payload={"name": receiver_name}, # Optional: structured access + ) + else: + search_contact_action = dv.DivAction( + log_id="search_contact", + url=f"divkit://search_contact?name=[{receiver_name}]", # Custom scheme the iOS app will catch + payload={"name": [receiver_name]}, # Optional: structured access + ) + + texts_map = { + LanguageOptions.RUSSIAN: { + "search_contact": f"Мне нужно найти контакты для {', '.join(receiver_name)}. Согласны ли вы на поиск?", + }, + LanguageOptions.ENGLISH: { + "search_contact": f"I need to search contacts for {', '.join(receiver_name)}. Do you approve to search ?", + }, + LanguageOptions.UZBEK: { + "search_contact": f"Men {', '.join(receiver_name)} nomli kontaktlarni topishim kerak. Qidirishga ruxsat bera olami?", + }, + } + + search_contact_text = texts_map.get(language, texts_map[LanguageOptions.RUSSIAN])[ + "search_contact" + ] + + search_button = smarty_button( + text="Search", + action_url=search_contact_action, + ) + cancel_button = smarty_button( + text="Cancel", + action_url=f"divkit://cancel?name={receiver_name}", + ) + + # Main container + main_container = dv.DivContainer( + items=[ + assistant_bubble( + text=search_contact_text, + ), + HStack([search_button, cancel_button]), + ] + ) + div = dv.make_div(main_container) + with open("logs/json/build_get_number_by_reciver_number_ui.json", "w") as f: + json.dump(div, f, ensure_ascii=False, indent=2) + logger.info("Saved to build_get_number_by_reciver_number_ui.json", div=div) + logger.info("get_number_by_reciver_number_ui done") + return div + + +class GetNumberByReceiverName(FunctionStrategy): + """Strategy for receiver name search UI using smarty_ui search_for_contacts.""" + + def build_widget_inputs(self, context): + names = context.backend_output["names"] + if isinstance(names, str): + names = [names] + + return { + get_number_by_reciver_name_ui: WidgetInput( + widget=Widget( + name="search_contacts_widget", + type="search_contacts_widget", + order=2, + layout="vertical", + fields=["names"], + values=[{"names": names}], + ), + args={"receiver_name": names, "language": context.language}, + ), + } + + +get_number_by_receiver_name = GetNumberByReceiverName() + + +class GetReceiverIdByReceiverPhoneNumber(FunctionStrategy): + """Strategy for phone number card selection UI.""" + + def build_widget_inputs(self, context): + backend_data = CardsByPhoneNumberResponse(**context.backend_output) + return { + get_receiver_id_by_receiver_phone_number_ui: WidgetInput( + widget=Widget( + name="get_contacts_widget", + type="get_contacts_widget", + order=2, + layout="vertical", + fields=["receiver_name"], + values=[x.model_dump() for x in backend_data.cards], + ), + args={"cards": backend_data.cards, "language": context.language}, + ), + } + + +get_receiver_id_by_receiver_phone_number = GetReceiverIdByReceiverPhoneNumber() + + +def get_receiver_id_by_receiver_phone_number_ui( + cards: List[CardInfoByPhoneNumber], + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Builds a UI showing a list of cards for a receiver using smarty_ui receiver_cards_list. + + Args: + cards: List of card information + language: Language for localization + + Returns: + DivKit JSON for the card selection UI + """ + # Convert CardInfoByPhoneNumber to receiver_cards_list format + receiver_cards_data = [] + for card in cards: + # Get provider logo URL based on processing system + provider_logo = get_processing_system_icon(card.processing) + + receiver_cards_data.append( + { + "holder_name": card.name, + "card_last_digits": card.mask[-4:] if card.mask else "****", + "provider_logo_url": provider_logo, + "action_url": f"divkit://select?card_id={card.pan}&card_name={card.name}&card_processing={card.processing}&card_mask={card.mask}", + } + ) + + # Use smarty_ui receiver_cards_list component + cards_widget = receiver_cards_list(receiver_cards_data) + + # Wrap with margins + container = VStack([cards_widget]) + container.margins = dv.DivEdgeInsets(left=12, right=12, top=12, bottom=12) + + div = dv.make_div(container) + with open( + "logs/json/build_get_receiver_id_by_receiver_phone_number_ui.json", "w" + ) as f: + json.dump(div, f, ensure_ascii=False, indent=2) + return div + + +# ============================================================================ +# Transaction Status UI using smarty_ui +# ============================================================================ + + +def build_transfer_success_ui( + amount: str, + recipient: str, + card_last_digits: str, + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Build transfer success UI using smarty_ui transaction_success_widget. + + Args: + amount: Transfer amount + recipient: Recipient name and card info + card_last_digits: Last 4 digits of the card + language: Language for localization + + Returns: + DivKit JSON for the transfer success UI + """ + feedback_texts = TRANSFER_FEEDBACK_TEXTS.get( + language, TRANSFER_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) + + date_time = datetime.now().strftime("%-d %B %Y %H:%M") + + # Use smarty_ui transaction_success_widget + success_widget = transaction_success_widget( + amount=amount, + recipient=f"{recipient} **{card_last_digits}", + date_time=date_time, + currency=feedback_texts["currency"], + ) + + # Wrap with margins + container = VStack([success_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) + + div = dv.make_div(container) + with open("logs/json/build_transfer_success_ui.json", "w") as f: + json.dump(div, f, ensure_ascii=False, indent=2) + return div + + +def build_transfer_failed_ui( + amount: str, + recipient: str, + card_last_digits: str, + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Build transfer failed UI using smarty_ui transaction_failed_widget. + + Args: + amount: Transfer amount + recipient: Recipient name and card info + card_last_digits: Last 4 digits of the card + language: Language for localization + + Returns: + DivKit JSON for the transfer failed UI + """ + feedback_texts = TRANSFER_FEEDBACK_TEXTS.get( + language, TRANSFER_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) + + date_time = datetime.now().strftime("%-d %B %Y %H:%M") + + # Use smarty_ui transaction_failed_widget + failed_widget = transaction_failed_widget( + amount=amount, + recipient=f"{recipient} **{card_last_digits}", + date_time=date_time, + currency=feedback_texts["currency"], + ) + + # Wrap with margins + container = VStack([failed_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) + + div = dv.make_div(container) + with open("logs/json/build_transfer_failed_ui.json", "w") as f: + json.dump(div, f, ensure_ascii=False, indent=2) + return div + + +# ============================================================================ +# Cards Own List UI using smarty_ui +# ============================================================================ + + +def build_cards_own_list_ui( + cards_data: List[Dict[str, Any]], + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Build user's own cards list UI using smarty_ui cards_own_list component. + + Args: + cards_data: List of card dicts with balance, card_name, card_thumbnail_url, card_last_digits + language: Language for localization + + Returns: + DivKit JSON for the cards list UI + """ + # Use smarty_ui cards_own_list component + cards_widget = cards_own_list(cards=cards_data) + + # Wrap with margins + container = VStack([cards_widget]) + container.margins = dv.DivEdgeInsets(left=16, right=16, top=16, bottom=16) + + div = dv.make_div(container) + with open("logs/json/build_cards_own_list_ui.json", "w") as f: + json.dump(div, f, ensure_ascii=False, indent=2) + return div + + +if __name__ == "__main__": + # check get_contacts with action first + # output = get_receiver_id_by_receiver_phone_number( + # llm_output="", backend_output={"receiver_name": "Aslon"}, version="v3" + # ) + # with open("logs/json/test_response.json", "w") as f: + # json.dump(output.model_dump(), f) + data = [ + { + "pan": "kkkkkkxxxxxxyyyyyy", + "name": "Aslon", + "processing": "HUMO", + "mask": "*************1234", + }, + { + "pan": "kkkkkkxxxxxxyyasdfafsdfyyyy", + "name": "Aslon", + "processing": "HUMO", + "mask": "*************1235", + }, + { + "pan": "kkkkkkxxxxxxyyasdfafsdfyyyy", + "name": "Aslon", + "processing": "HUMO", + "mask": "*************1236", + }, + ] + context = Context( + llm_output="Hello world", + backend_output=CardsByPhoneNumberResponse.model_validate(data).model_dump(), + version="v3", + language=LanguageOptions.UZBEK, + api_key="test", + logger_context=LoggerContext( + chat_id="test", + logger=logger, + ), + ) + output = get_receiver_id_by_receiver_phone_number(context=context) + with open("logs/json/test_response.json", "w") as f: + json.dump(output.model_dump(), f, ensure_ascii=False, indent=2) diff --git a/functions_to_format/functions/weather.py b/functions_to_format/functions/weather.py index a82f135..aaca32d 100644 --- a/functions_to_format/functions/weather.py +++ b/functions_to_format/functions/weather.py @@ -1,76 +1,64 @@ import pydivkit as dv import json -from .general import ( - Widget, - TextWidget, - build_text_widget, - add_ui_to_widget, - WidgetInput, -) -from .general.utils import save_builder_output +from .general import WidgetInput +from .base_strategy import FunctionStrategy from tool_call_models.weather import WeatherResponse -from models.build import BuildOutput +from models.widget import Widget from functions_to_format.functions.general.const_values import LanguageOptions -from conf import logger -import structlog from models.context import Context +# Import smarty_ui components +from smarty_ui import ( + VStack, + HStack, + title_1, + title_2, + text_1, + text_2, + caption_1, + caption_2, + icon, + default_theme, +) -def get_weather_info(context: Context) -> BuildOutput: - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - weather_data = WeatherResponse(**backend_output) - widget = Widget( - name="weather_widget", - type="weather_widget", - order=2, - layout="vertical", - fields=[ - "city", - "condition", - "temperature", - "feels_like", - "humidity", - "sunrise", - "wind_speed", - "sunset", - ], - ) - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - widgets = add_ui_to_widget( - { - build_text_widget: WidgetInput( - widget=text_widget, args={"text": llm_output} - ), +class GetWeatherInfo(FunctionStrategy): + """Strategy for building weather info UI.""" + + def build_widget_inputs(self, context): + weather_data = WeatherResponse(**context.backend_output) + text_builder, text_input = self.make_text_input(context.llm_output) + return { + text_builder: text_input, build_weather_widget: WidgetInput( - widget=widget, - args={"weather_data": weather_data, "language": language}, + widget=Widget( + name="weather_widget", + type="weather_widget", + order=2, + layout="vertical", + fields=[ + "city", + "condition", + "temperature", + "feels_like", + "humidity", + "sunrise", + "wind_speed", + "sunset", + ], + ), + args={"weather_data": weather_data, "language": context.language}, ), - }, - version, - ) - output = BuildOutput( - widgets_count=1, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output + } + + +get_weather_info = GetWeatherInfo() def weather_widget( data: WeatherResponse, language: LanguageOptions = LanguageOptions.RUSSIAN ): + """Build weather widget using smarty_ui components.""" city_name = data.location.name texts_map = { @@ -134,159 +122,111 @@ def weather_widget( data.current.condition.text, ) - # Main weather card container - main_container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, + # Header with city name using smarty_ui + city_title = title_1(city_name, color="#1F2937") + city_title.font_weight = dv.DivFontWeight.BOLD + + location_text = text_1(f"{city_name}, {country_name}", color="#6B7280") + + city_info = VStack([city_title, location_text]) + + # Weather icon + weather_icon = ( + dv.DivText(text="☀️", font_size=48) + if data.current.condition.icon is None + else dv.DivImage( + image_url="https:" + data.current.condition.icon, + width=dv.DivFixedSize(value=48), + height=dv.DivFixedSize(value=48), + ) + ) + + header_row = HStack([city_info, weather_icon]) + header_row.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + header_row.margins = dv.DivEdgeInsets(bottom=16) + + # Temperature section using smarty_ui + temp_text = title_1(f"{current_temperature}°C", color="#1F2937") + temp_text.font_size = 40 + temp_text.font_weight = dv.DivFontWeight.BOLD + + feels_like_text = text_1( + f"{texts_map[language]['feels_like']} {feels_like_temperature}°C", + color="#6B7280", + ) + wind_text = text_1( + f"{texts_map[language]['wind']} {wind_direction} {wind_speed} {texts_map[language]['wind_speed']}", + color="#6B7280", + ) + humidity_text = text_1( + f"{texts_map[language]['humidity']} {humidity}%", color="#6B7280" + ) + updated_text = text_1(last_updated, color="#6B7280") + + temp_details = VStack( + [temp_text, feels_like_text, wind_text, humidity_text, updated_text] + ) + + # Condition text + condition_label = text_2(condition_text, color="#1F2937") + condition_label.font_size = 18 + condition_label.font_weight = dv.DivFontWeight.MEDIUM + + condition_container = VStack([condition_label]) + condition_container.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + + temp_row = HStack([temp_details, condition_container]) + temp_row.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + temp_row.margins = dv.DivEdgeInsets(bottom=24) + + # Weekly forecast section + forecast_items = [] + for forecast_day in data.forecast.forecastday[:6]: + day_label = caption_2( + forecast_day.date.split("-")[2] + if len(forecast_day.date.split("-")) > 2 + else forecast_day.date[:3], + color="#6B7280", + ) + + day_icon = ( + dv.DivText(text="☀️", font_size=24) + if forecast_day.day.condition.icon is None + else dv.DivImage( + image_url="https:" + forecast_day.day.condition.icon, + width=dv.DivFixedSize(value=24), + height=dv.DivFixedSize(value=24), + ) + ) + + max_temp = caption_2(f"{forecast_day.day.maxtemp_c}°C", color="#1F2937") + max_temp.font_size = 10 + max_temp.font_weight = dv.DivFontWeight.BOLD + + min_temp = caption_2(f"{forecast_day.day.mintemp_c}°C", color="#6B7280") + min_temp.font_size = 9 + + day_container = VStack([day_label, day_icon, max_temp, min_temp]) + day_container.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + day_container.width = dv.DivFixedSize(value=40) + + forecast_items.append(day_container) + + forecast_row = HStack(forecast_items) + forecast_row.alignment_horizontal = dv.DivAlignmentHorizontal.CENTER + + # Main weather card container using VStack + main_container = VStack( + [header_row, temp_row, forecast_row], + padding=20, + background="#FFFFFF", + corner_radius=16, width=dv.DivFixedSize(value=320), - background=[dv.DivSolidBackground(color="#FFFFFF")], - border=dv.DivBorder(corner_radius=16, stroke=dv.DivStroke(color="#E5E7EB")), - paddings=dv.DivEdgeInsets(top=20, bottom=20, left=20, right=20), - margins=dv.DivEdgeInsets(top=16, left=20, right=20, bottom=16), - items=[ - # Header with city name and location - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - items=[ - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - dv.DivText( - text=city_name, - font_size=24, - font_weight=dv.DivFontWeight.BOLD, - text_color="#1F2937", - ), - dv.DivText( - text=f"{city_name}, {country_name}", - font_size=14, - text_color="#6B7280", - ), - ], - ), - # Weather icon - ( - dv.DivText( - text="☀️", - font_size=48, - ) - if data.current.condition.icon is None - else dv.DivImage( - image_url="https:" + data.current.condition.icon, - width=dv.DivFixedSize(value=48), - height=dv.DivFixedSize(value=48), - ) - ), - ], - margins=dv.DivEdgeInsets(bottom=16), - ), - # Main temperature and condition section - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - items=[ - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=[ - # Temperature with inline °C on same line - dv.DivText( - text=f"{current_temperature}°C", - font_size=40, - font_weight=dv.DivFontWeight.BOLD, - text_color="#1F2937", - ), - dv.DivText( - text=f"{texts_map[language]['feels_like']} {feels_like_temperature}°C", - font_size=14, - text_color="#6B7280", - ), - dv.DivText( - text=f"{texts_map[language]['wind']} {wind_direction} {wind_speed} {texts_map[language]['wind_speed']}", - font_size=14, - text_color="#6B7280", - ), - dv.DivText( - text=f"{texts_map[language]['humidity']} {humidity}%", - font_size=14, - text_color="#6B7280", - ), - dv.DivText( - text=last_updated, - font_size=14, - text_color="#6B7280", - ), - ], - ), - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - items=[ - dv.DivText( - text=condition_text, - font_size=18, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#1F2937", - ), - ], - ), - ], - margins=dv.DivEdgeInsets(bottom=24), - ), - # Weekly forecast section - dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - items=[ - # Generate forecast days dynamically - *[ - dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - items=[ - dv.DivText( - text=( - forecast_day.date.split("-")[2] - if len(forecast_day.date.split("-")) > 2 - else forecast_day.date[:3] - ), - font_size=12, - text_color="#6B7280", - ), - ( - dv.DivText( - text="☀️", - font_size=24, - ) - if forecast_day.day.condition.icon is None - else dv.DivImage( - image_url="https:" - + forecast_day.day.condition.icon, - width=dv.DivFixedSize(value=24), - height=dv.DivFixedSize(value=24), - ) - ), - dv.DivText( - text=f"{forecast_day.day.maxtemp_c}°C", - font_size=10, - font_weight=dv.DivFontWeight.BOLD, - text_color="#1F2937", - ), - dv.DivText( - text=f"{forecast_day.day.mintemp_c}°C", - font_size=9, - text_color="#6B7280", - ), - ], - width=dv.DivFixedSize(value=40), - ) - for forecast_day in data.forecast.forecastday[ - :6 - ] # Show up to 6 days - ] - ], - ), - ], ) + main_container.border = dv.DivBorder( + corner_radius=16, stroke=dv.DivStroke(color="#E5E7EB") + ) + main_container.margins = dv.DivEdgeInsets(top=16, left=20, right=20, bottom=16) return main_container diff --git a/pyproject.toml b/pyproject.toml index f53d3b8..8801441 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,12 +26,17 @@ dependencies = [ "redis==5.2.1", "requests==2.32.3", "sentry-sdk>=2.44.0", + "smarty-ui", "structlog>=25.5.0", "tool-call-models==0.1.40", ] +[tool.uv.sources] +smarty-ui = { path = "smarty_ui" } + [dependency-groups] dev = [ "pymongo>=4.15.4", "pyright>=1.1.407", ] + diff --git a/share/approve.json b/share/approve.json new file mode 100644 index 0000000..aa5dc39 --- /dev/null +++ b/share/approve.json @@ -0,0 +1,194 @@ +{ + "card": { + "log_id": "card", + "states": [ + { + "div": { + "items": [ + { + "background": [ + { + "color": "#F8FAFF", + "type": "solid" + } + ], + "border": { + "corner_radius": 16 + }, + "height": { + "type": "wrap_content" + }, + "items": [ + { + "font_family": "Manrope", + "font_size": 14, + "font_weight": "light", + "letter_spacing": 0, + "line_height": 22, + "text": "Please approve the action. \nuser_id: 60005982\nsession_id: cea77f0b-07f8-478b-a1c3-3d06be112300\napp_name: smarty\n\n", + "text_alignment_horizontal": "left", + "text_alignment_vertical": "top", + "text_color": "#111133", + "type": "text" + } + ], + "margins": { + "bottom": 16, + "left": 16, + "right": 16, + "top": 16 + }, + "orientation": "vertical", + "paddings": { + "bottom": 16, + "left": 16, + "right": 16, + "top": 16 + }, + "type": "container", + "width": { + "type": "match_parent" + } + }, + { + "items": [ + { + "actions": [ + { + "log_id": "btn-approve", + "typed": { + "container_id": "btn-approve", + "on_fail_actions": [ + { + "log_id": "approve-error", + "url": "div-action://set_variable?name=error_visible&value=1" + }, + { + "log_id": "set-error-text", + "url": "div-action://set_variable?name=error_text&value=Failed to approve request" + } + ], + "on_success_actions": [ + { + "log_id": "approve-success", + "url": "div-action://set_variable?name=success_visible&value=1" + } + ], + "request": { + "headers": [ + { + "name": "api-key", + "value": "skg_41ca223f9eee407692d679b53d00132a" + } + ], + "method": "get", + "url": "https://smarty-test.smartbank.uz/chat/v3/approval?tool_call_id=tool_pay_for_home_utility_wrapper_yWvDhGlgRpfKCLm7EgwF&user_id=60005982&session_id=cea77f0b-07f8-478b-a1c3-3d06be112300&app_name=smarty&action=approve" + }, + "type": "submit" + }, + "url": "divkit://send-request" + } + ], + "alignment_horizontal": "center", + "border": { + "corner_radius": 8, + "stroke": { + "color": "#3B82F6" + } + }, + "font_size": 14, + "height": { + "type": "fixed", + "value": 36 + }, + "id": "btn-approve", + "margins": { + "right": 8 + }, + "paddings": { + "bottom": 8, + "left": 12, + "right": 12, + "top": 8 + }, + "text": "Approve", + "text_color": "#2563EB", + "type": "text" + }, + { + "actions": [ + { + "log_id": "btn-reject", + "typed": { + "container_id": "btn-reject", + "on_fail_actions": [ + { + "log_id": "reject-error", + "url": "div-action://set_variable?name=error_visible&value=1" + }, + { + "log_id": "set-error-text", + "url": "div-action://set_variable?name=error_text&value=Failed to reject request" + } + ], + "on_success_actions": [ + { + "log_id": "reject-success", + "url": "div-action://set_variable?name=success_visible&value=1" + } + ], + "request": { + "headers": [ + { + "name": "api-key", + "value": "skg_41ca223f9eee407692d679b53d00132a" + } + ], + "method": "get", + "url": "https://smarty-test.smartbank.uz/chat/v3/approval?tool_call_id=tool_pay_for_home_utility_wrapper_yWvDhGlgRpfKCLm7EgwF&user_id=60005982&session_id=cea77f0b-07f8-478b-a1c3-3d06be112300&app_name=smarty&action=reject" + }, + "type": "submit" + }, + "url": "divkit://send-request" + } + ], + "alignment_horizontal": "center", + "border": { + "corner_radius": 8, + "stroke": { + "color": "#3B82F6" + } + }, + "font_size": 14, + "height": { + "type": "fixed", + "value": 36 + }, + "id": "btn-reject", + "margins": { + "right": 8 + }, + "paddings": { + "bottom": 8, + "left": 12, + "right": 12, + "top": 8 + }, + "text": "Reject", + "text_color": "#2563EB", + "type": "text" + } + ], + "orientation": "horizontal", + "type": "container" + } + ], + "orientation": "vertical", + "type": "container" + }, + "state_id": 0 + } + ] + }, + "templates": {} +} \ No newline at end of file diff --git a/tests/fixtures/request_json/request_function_response_activity.json b/tests/fixtures/request_json/request_function_response_activity.json index 41114d8..97f9455 100644 --- a/tests/fixtures/request_json/request_function_response_activity.json +++ b/tests/fixtures/request_json/request_function_response_activity.json @@ -4,11 +4,52 @@ "backend_output": { "function_name": "get_balance", "response": { - "balance": 1500.50, - "currency": "UZS", - "last_updated": "2025-02-07T12:00:00Z" + "body": [ + { + "custNo": "12345", + "phoneNumber": "+998901234567", + "firstname": "Aslon", + "middlename": "", + "lastname": "Hamidov", + "birthDate": "1990-01-01", + "pinfl": "00000000000000", + "createdAt": "2023-01-01", + "cardList": [ + { + "panMask": "8600 **** **** 1234", + "panToken": "token_123", + "requestId": "req_001", + "pan": "8600********1234", + "expiry": "12/28", + "bankIssuer": "SmartBank", + "processingSystem": "UZCARD", + "salaryAmount": 0, + "isVerified": true, + "createdAt": "2023-06-15", + "cardDetails": { + "cardDetailsId": 1, + "cardName": "My Salary Card", + "cardColor": "#1E40AF", + "cardIsPrimary": true + }, + "cardBalance": { + "balance": 1500050, + "status": 0 + }, + "isVirtual": false, + "bankIcon": { + "bankLogo": "https://example.com/logo.png", + "bankLogoMini": "https://example.com/logo_mini.png", + "bankWhiteLogo": "https://example.com/logo_w.png", + "bankWhiteLogoMini": "https://example.com/logo_w_mini.png" + } + } + ] + } + ], + "status": 0 } }, "chat_id": "chat_abc123", "api_key": "" -} +} \ No newline at end of file diff --git a/uv.lock b/uv.lock index 18215ea..78fdcee 100644 --- a/uv.lock +++ b/uv.lock @@ -537,7 +537,7 @@ wheels = [ [[package]] name = "jupyter-client" -version = "8.6.3" +version = "8.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, @@ -546,9 +546,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, ] [[package]] @@ -1594,6 +1594,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smarty-ui" +version = "0.1.2" +source = { directory = "smarty_ui" } +dependencies = [ + { name = "pydantic" }, + { name = "pydivkit" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.12.4" }, + { name = "pydivkit", specifier = ">=32.25.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "ipykernel", specifier = ">=7.2.0" }, + { name = "ipywidgets", specifier = ">=8.1.8" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1770,6 +1791,7 @@ dependencies = [ { name = "redis" }, { name = "requests" }, { name = "sentry-sdk" }, + { name = "smarty-ui" }, { name = "structlog" }, { name = "tool-call-models" }, ] @@ -1803,6 +1825,7 @@ requires-dist = [ { name = "redis", specifier = "==5.2.1" }, { name = "requests", specifier = "==2.32.3" }, { name = "sentry-sdk", specifier = ">=2.44.0" }, + { name = "smarty-ui", directory = "smarty_ui" }, { name = "structlog", specifier = ">=25.5.0" }, { name = "tool-call-models", specifier = "==0.1.40" }, ]