diff --git a/.gitignore b/.gitignore index 89514cb..9d0b48c 100644 --- a/.gitignore +++ b/.gitignore @@ -334,4 +334,7 @@ logs/temp/ logs/usage/ *.ignore.* a.json -a.py \ No newline at end of file +a.py +share/ +jprq_/ +jprq \ No newline at end of file diff --git a/conf/config_models.py b/conf/config_models.py index ec5c546..ab26162 100644 --- a/conf/config_models.py +++ b/conf/config_models.py @@ -28,7 +28,7 @@ class OTELConfig: bsp_export_timeout: int = 30000 traces_sampler: str = "parentbased_traceidratio" traces_sampler_arg: float = 1.0 - exporter_otlp_endpoint: str = "http://tempo:4318/v1/traces" + exporter_otlp_endpoint: str = "http://localhost:4318/v1/traces" exporter_otlp_headers: Optional[str] = None console_export: bool = False @@ -72,7 +72,7 @@ def New() -> AppConfig: traces_sampler=os.getenv("TRACES_SAMPLER", "parentbased_traceidratio"), traces_sampler_arg=float(os.getenv("TRACES_SAMPLER_ARG", 1.0)), exporter_otlp_endpoint=os.getenv( - "OTEL_EXPORTER_OTLP_ENDPOINT", "http://tempo:4318/v1/traces" + "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces" ), exporter_otlp_headers=os.getenv("EXPORTER_OTLP_HEADERS", None), console_export=bool(os.getenv("CONSOLE_EXPORT", False)), diff --git a/conf/logger_conf.py b/conf/logger_conf.py index 60726bb..6b3e9bc 100644 --- a/conf/logger_conf.py +++ b/conf/logger_conf.py @@ -83,8 +83,17 @@ def setup_logging(logfile: str) -> structlog.stdlib.BoundLogger: return get_logger() -logfile_path = "/var/ui_server/" # "/var/ui_server/" +logfile_path = "logs/" # "/var/ui_server/" logfile = "ui_server.log" if not os.path.exists(logfile_path): os.mkdir(logfile_path) logger = setup_logging(os.path.join(logfile_path, logfile)) + +logging.getLogger("opentelemetry").setLevel(logging.ERROR) +logging.getLogger("opentelemetry.sdk").setLevel(logging.ERROR) +logging.getLogger("opentelemetry.exporter").setLevel(logging.ERROR) + +# Suppress specific loggers +logging.getLogger("uvicorn.access").setLevel(logging.WARNING) +logging.getLogger("uvicorn.error").setLevel(logging.WARNING) +logging.getLogger("fastapi").setLevel(logging.WARNING) diff --git a/functions_to_format/functions/__init__.py b/functions_to_format/functions/__init__.py index 7dc8c50..e5500ce 100644 --- a/functions_to_format/functions/__init__.py +++ b/functions_to_format/functions/__init__.py @@ -9,6 +9,9 @@ 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 .contact import build_contact_widget, get_contact from .news import build_news_widget, get_news @@ -34,6 +37,8 @@ "chatbot_answer": None, "text_widget": None, "buttons_widget": build_buttons_row, + "receiver_by_card": build_receiver_by_card_ui, + "send_money": build_send_money_ui, } functions_mapper = { "get_balance": get_balance, @@ -59,4 +64,6 @@ "get_home_utility_suppliers": get_home_utility_suppliers, "human_approval": human_approval_requests, "human_approval_request": human_approval_requests, + "get_receiver_by_card": get_receiver_by_card, + "receiver_by_card": get_receiver_by_card, } diff --git a/functions_to_format/functions/balance.py b/functions_to_format/functions/balance.py index c85aea9..f2a7266 100644 --- a/functions_to_format/functions/balance.py +++ b/functions_to_format/functions/balance.py @@ -1,4 +1,5 @@ import pydivkit as dv +from pydivkit.core import Expr import json from .general import ( add_ui_to_widget, @@ -7,10 +8,13 @@ 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 +from typing import List, Optional from conf import logger from models.build import BuildOutput from models.context import Context, LoggerContext @@ -20,6 +24,26 @@ import structlog +# Feedback texts for balance actions +BALANCE_FEEDBACK_TEXTS = { + LanguageOptions.RUSSIAN: { + "topup_success": "Пополнение выполнено", + "transfer_success": "Перевод выполнен", + "action_error": "Ошибка операции", + }, + LanguageOptions.ENGLISH: { + "topup_success": "Top up completed", + "transfer_success": "Transfer completed", + "action_error": "Operation failed", + }, + LanguageOptions.UZBEK: { + "topup_success": "To'ldirish bajarildi", + "transfer_success": "O'tkazma bajarildi", + "action_error": "Amal xatosi", + }, +} + + class CardInfo(BaseModel): masked_card_pan: str image_url: str @@ -188,9 +212,10 @@ def get_home_balances(context: Context) -> BuildOutput: chat_id = context.logger_context.chat_id api_key = context.api_key logger = context.logger_context.logger - for k, v in backend_output["services"].items(): - backend_output[k] = v - del backend_output["services"] + 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) @@ -247,33 +272,55 @@ def get_balance(context: Context) -> BuildOutput: 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, ] - backend_data: CardsBalanceResponse = CardsBalanceResponse(**backend_output) + + try: + backend_data: CardsBalanceResponse = CardsBalanceResponse(**backend_output) + 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) - - 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, - ) + 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}" ) + + logger.info(f"Processed {len(backend_output_processed)} cards successfully") + text_widget = TextWidget( order=1, values=[{"text": llm_output}], @@ -289,6 +336,8 @@ def get_balance(context: Context) -> BuildOutput: ], ) + logger.debug(f"Created widgets: text_widget (order=1), cards_list (order=2)") + widgets = add_ui_to_widget( { build_text_widget: WidgetInput( @@ -307,10 +356,18 @@ def get_balance(context: Context) -> BuildOutput: }, 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 @@ -424,16 +481,24 @@ def account_block( ) -def action_button(text: str): +def action_button( + text: str, + action_type: str = "default", + language: LanguageOptions = LanguageOptions.RUSSIAN, +): """ - Create an action button UI component. + Create an action button UI component with success/error feedback. Args: text (str): Button text + action_type (str): Type of action for payload + language (LanguageOptions): Language for localization Returns: - dv.DivText: UI component for action button + dv.DivText: UI component for action button with feedback handling """ + button_id = f"action-{text.lower().replace(' ', '_')}" + return dv.DivText( text=text, text_color="#2563EB", @@ -441,10 +506,21 @@ def action_button(text: str): alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, paddings=dv.DivEdgeInsets(top=8, bottom=8), border=dv.DivBorder(corner_radius=8, stroke=dv.DivStroke(color="#3B82F6")), - action=dv.DivAction( - log_id=f"action-{text.lower()}", - url=f"div-action://{text.lower()}", # или https://example.com/action - ), + actions=[ + dv.DivAction( + log_id=button_id, + url=f"div-action://{text.lower().replace(' ', '_')}", + payload={ + "action": action_type, + "button_text": text, + }, + ), + # Success feedback action + dv.DivAction( + log_id=f"{button_id}-success", + url="div-action://set_variable?name=balance_action_success&value=1", + ), + ], ) diff --git a/functions_to_format/functions/buttons.py b/functions_to_format/functions/buttons.py index b300bb0..1de7b8d 100644 --- a/functions_to_format/functions/buttons.py +++ b/functions_to_format/functions/buttons.py @@ -1,28 +1,177 @@ import pydivkit as dv +from pydivkit.core import Expr import json +from typing import List, Optional -def build_buttons_row(button_texts: list): - return dv.DivContainer( +# Feedback texts for button actions +BUTTON_FEEDBACK_TEXTS = { + "ru": { + "action_success": "Действие выполнено", + "action_error": "Ошибка выполнения", + }, + "en": { + "action_success": "Action completed", + "action_error": "Action failed", + }, + "uz": { + "action_success": "Amal bajarildi", + "action_error": "Xatolik yuz berdi", + }, +} + + +def build_buttons_row( + button_texts: list, + language: str = "ru", + include_feedback: bool = True, +): + """ + Build a row of buttons with optional feedback handling. + + 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") + ), + 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 + ] + + if not include_feedback: + return dv.DivContainer( + orientation=dv.DivContainerOrientation.HORIZONTAL, + items=button_items, + ) + + # 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, + 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=txt, + 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="#2563EB", - border=dv.DivBorder( - corner_radius=8, stroke=dv.DivStroke(color="#3B82F6") - ), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - height=dv.DivFixedSize(value=36), - paddings=dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8), + 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, + 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), - action=dv.DivAction( - log_id=f"btn-{txt.lower()}", - url=f"div-action://button/{txt.lower()}", - ), - ) - for txt in button_texts + ), + 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, ], ) @@ -31,4 +180,4 @@ def build_buttons_row(button_texts: list): buttons = ["submit", "cancel"] buttons_widget = build_buttons_row(buttons) with open("logs/json/buttons.json", "w") as f: - json.dump(buttons_widget, f) + json.dump(dv.make_div(buttons_widget), f, indent=2, ensure_ascii=False) diff --git a/functions_to_format/functions/contact.py b/functions_to_format/functions/contact.py index f0c459f..ca16359 100644 --- a/functions_to_format/functions/contact.py +++ b/functions_to_format/functions/contact.py @@ -1,7 +1,8 @@ import pydivkit as dv +from pydivkit.core import Expr import json from pydantic import BaseModel -from typing import List +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 @@ -13,6 +14,26 @@ from models.context import Context, LoggerContext +# Feedback texts for contact widget actions +CONTACT_WIDGET_FEEDBACK_TEXTS = { + LanguageOptions.RUSSIAN: { + "contact_opened": "Контакт открыт", + "contact_selected": "Контакт выбран", + "action_error": "Ошибка", + }, + LanguageOptions.ENGLISH: { + "contact_opened": "Contact opened", + "contact_selected": "Contact selected", + "action_error": "Error", + }, + LanguageOptions.UZBEK: { + "contact_opened": "Kontakt ochildi", + "contact_selected": "Kontakt tanlandi", + "action_error": "Xatolik", + }, +} + + def get_contact(context: Context) -> BuildOutput: # Extract values from context llm_output = context.llm_output @@ -51,8 +72,31 @@ def get_contact(context: Context) -> BuildOutput: return output -def contact_widget(name: str, avatar_url: str, subtitle: str): - return dv.DivContainer( +def contact_widget( + name: str, + avatar_url: str, + subtitle: str, + clickable: bool = False, + contact_id: Optional[str] = None, + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + """ + Create a contact widget with optional click handling and feedback. + + Args: + name: Contact name + avatar_url: Contact avatar image URL + subtitle: Contact subtitle/description + 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( @@ -87,6 +131,63 @@ def contact_widget(name: str, avatar_url: str, subtitle: str): 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, + ), + 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", + ), + ], + ) + + return base_container class Contact(BaseModel): @@ -121,7 +222,26 @@ def build_contact_widget(backend_output: dict, llm_output: str, context: LoggerC return dv.make_div(contact) -def make_contacts_list(contacts_data: List[Contact], title: str = "Contacts"): +def make_contacts_list( + contacts_data: List[Contact], + title: str = "Contacts", + clickable: bool = False, + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + """ + Create a contacts list container with optional click handling and feedback. + + 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, @@ -132,15 +252,75 @@ def make_contacts_list(contacts_data: List[Contact], title: str = "Contacts"): ) ] - for contact in contacts_data: + 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, ) ) + 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, + 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), + 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, diff --git a/functions_to_format/functions/functions.py b/functions_to_format/functions/functions.py index 129bf5a..65d0d80 100644 --- a/functions_to_format/functions/functions.py +++ b/functions_to_format/functions/functions.py @@ -12,9 +12,15 @@ build_buttons_row, build_text_widget, WidgetInput, + create_feedback_variables, + create_success_actions, + create_failure_actions, + create_success_container, + create_error_container, ) from .general.utils import save_builder_output 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 @@ -22,6 +28,23 @@ from models.context import Context +# Feedback texts for contact actions +CONTACT_FEEDBACK_TEXTS = { + LanguageOptions.RUSSIAN: { + "contact_selected": "Контакт выбран", + "contact_error": "Ошибка выбора контакта", + }, + LanguageOptions.ENGLISH: { + "contact_selected": "Contact selected", + "contact_error": "Contact selection error", + }, + LanguageOptions.UZBEK: { + "contact_selected": "Kontakt tanlandi", + "contact_error": "Kontaktni tanlashda xatolik", + }, +} + + def chatbot_answer(context: Context) -> BuildOutput: # Janis Rubins: logic unchanged, just returns text_widget schema and llm_output as data @@ -78,7 +101,22 @@ class Contact(BaseModel): phone: str -def build_contacts_list_widget(contacts: list[Contact]) -> Dict[str, Any]: +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. + + Args: + contacts: List of contact objects + language: Language for localization + + 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", @@ -90,17 +128,7 @@ def build_contacts_list_widget(contacts: list[Contact]) -> Dict[str, Any]: ) ] - for contact in contacts: - send_action = dv.DivAction( - log_id=f"send_contact_{contact.phone}", - url="divkit://send_contact", - payload={ - "first_name": contact.first_name, - "last_name": contact.last_name, - "phone": contact.phone, - }, - ) - + 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), @@ -127,13 +155,120 @@ def build_contacts_list_widget(contacts: list[Contact]) -> Dict[str, Any]: letter_spacing=0, ), ], - action=send_action, + 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", + ) + ], + ), + ], + ) + + # 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", + ) + ], + ), + ], + ) + + 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, diff --git a/functions_to_format/functions/general/__init__.py b/functions_to_format/functions/general/__init__.py index e86e268..762f529 100644 --- a/functions_to_format/functions/general/__init__.py +++ b/functions_to_format/functions/general/__init__.py @@ -1,6 +1,21 @@ from typing import Dict, Callable, Any, List from .text import build_text_widget, TextWidget from .buttons import build_buttons_row, ButtonsWidget +from .action_helpers import ( + create_feedback_variables, + create_success_actions, + create_failure_actions, + create_loading_actions, + create_success_container, + create_error_container, + create_loading_container, + create_submit_action_with_handlers, + create_simple_action_with_feedback, + create_selection_action, + create_feedback_wrapper, + get_feedback_text, + FEEDBACK_TEXTS, +) from models.widget import Widget from pydantic import BaseModel from conf import logger diff --git a/functions_to_format/functions/general/action_helpers.py b/functions_to_format/functions/general/action_helpers.py new file mode 100644 index 0000000..584542a --- /dev/null +++ b/functions_to_format/functions/general/action_helpers.py @@ -0,0 +1,642 @@ +""" +Action Helpers - Utilities for creating pydivkit actions with on_success and on_failure handlers. + +This module provides consistent patterns for: +1. Creating actions with success/error feedback +2. Building feedback containers (success/error UI elements) +3. Managing feedback state variables +""" + +import pydivkit as dv +from pydivkit.core import Expr +from typing import Optional, List, Dict, Any +from .const_values import LanguageOptions + + +# ============================================================================ +# Feedback Text Maps (Localized) +# ============================================================================ + +FEEDBACK_TEXTS = { + LanguageOptions.RUSSIAN: { + "success_default": "Операция выполнена успешно!", + "error_default": "Произошла ошибка. Попробуйте еще раз.", + "added_to_cart": "Товар добавлен в корзину!", + "cart_error": "Не удалось добавить товар в корзину", + "payment_success": "Платеж выполнен успешно!", + "payment_error": "Ошибка платежа. Попробуйте еще раз.", + "action_approved": "Действие подтверждено!", + "action_rejected": "Действие отклонено", + "action_error": "Не удалось выполнить действие", + "contact_selected": "Контакт выбран", + "category_selected": "Категория выбрана", + "card_selected": "Карта выбрана", + "loading": "Загрузка...", + "retry": "Повторить", + "dismiss": "Закрыть", + }, + LanguageOptions.ENGLISH: { + "success_default": "Operation completed successfully!", + "error_default": "An error occurred. Please try again.", + "added_to_cart": "Item added to cart!", + "cart_error": "Failed to add item to cart", + "payment_success": "Payment completed successfully!", + "payment_error": "Payment failed. Please try again.", + "action_approved": "Action approved!", + "action_rejected": "Action rejected", + "action_error": "Failed to perform action", + "contact_selected": "Contact selected", + "category_selected": "Category selected", + "card_selected": "Card selected", + "loading": "Loading...", + "retry": "Retry", + "dismiss": "Dismiss", + }, + LanguageOptions.UZBEK: { + "success_default": "Amal muvaffaqiyatli bajarildi!", + "error_default": "Xatolik yuz berdi. Qayta urinib ko'ring.", + "added_to_cart": "Mahsulot savatga qo'shildi!", + "cart_error": "Mahsulotni savatga qo'shib bo'lmadi", + "payment_success": "To'lov muvaffaqiyatli amalga oshirildi!", + "payment_error": "To'lov xatosi. Qayta urinib ko'ring.", + "action_approved": "Harakat tasdiqlandi!", + "action_rejected": "Harakat rad etildi", + "action_error": "Harakatni bajarib bo'lmadi", + "contact_selected": "Kontakt tanlandi", + "category_selected": "Kategoriya tanlandi", + "card_selected": "Karta tanlandi", + "loading": "Yuklanmoqda...", + "retry": "Qayta urinish", + "dismiss": "Yopish", + }, +} + + +# ============================================================================ +# Feedback State Variables +# ============================================================================ + +def create_feedback_variables(prefix: str = "") -> List[dv.DivVariable]: + """ + Create standard feedback state variables for a container. + + Args: + prefix: Optional prefix for variable names to avoid conflicts + + Returns: + List of DivVariable objects for success/error state management + """ + var_prefix = f"{prefix}_" if prefix else "" + return [ + dv.IntegerVariable(name=f"{var_prefix}success_visible", value=0), + dv.IntegerVariable(name=f"{var_prefix}error_visible", value=0), + dv.IntegerVariable(name=f"{var_prefix}loading_visible", value=0), + dv.StringVariable(name=f"{var_prefix}success_text", value=""), + dv.StringVariable(name=f"{var_prefix}error_text", value=""), + ] + + +# ============================================================================ +# Success/Error Actions Builders +# ============================================================================ + +def create_success_actions( + log_id: str, + success_text: str = "", + prefix: str = "", + additional_actions: Optional[List[dv.DivAction]] = None, +) -> List[dv.DivAction]: + """ + Create standard success actions that show success feedback. + + Args: + log_id: Log identifier for the action + success_text: Text to display on success + prefix: Variable name prefix + additional_actions: Additional actions to include + + Returns: + List of DivAction objects for success handling + """ + var_prefix = f"{prefix}_" if prefix else "" + actions = [ + dv.DivAction( + log_id=f"{log_id}-success-show", + url=f"div-action://set_variable?name={var_prefix}success_visible&value=1", + ), + dv.DivAction( + log_id=f"{log_id}-hide-error", + url=f"div-action://set_variable?name={var_prefix}error_visible&value=0", + ), + dv.DivAction( + log_id=f"{log_id}-hide-loading", + url=f"div-action://set_variable?name={var_prefix}loading_visible&value=0", + ), + ] + + if success_text: + actions.append( + dv.DivAction( + log_id=f"{log_id}-set-success-text", + url=f"div-action://set_variable?name={var_prefix}success_text&value={success_text}", + ) + ) + + if additional_actions: + actions.extend(additional_actions) + + return actions + + +def create_failure_actions( + log_id: str, + error_text: str = "", + prefix: str = "", + additional_actions: Optional[List[dv.DivAction]] = None, +) -> List[dv.DivAction]: + """ + Create standard failure actions that show error feedback. + + Args: + log_id: Log identifier for the action + error_text: Text to display on error + prefix: Variable name prefix + additional_actions: Additional actions to include + + Returns: + List of DivAction objects for failure handling + """ + var_prefix = f"{prefix}_" if prefix else "" + actions = [ + dv.DivAction( + log_id=f"{log_id}-error-show", + url=f"div-action://set_variable?name={var_prefix}error_visible&value=1", + ), + dv.DivAction( + log_id=f"{log_id}-hide-success", + url=f"div-action://set_variable?name={var_prefix}success_visible&value=0", + ), + dv.DivAction( + log_id=f"{log_id}-hide-loading", + url=f"div-action://set_variable?name={var_prefix}loading_visible&value=0", + ), + ] + + if error_text: + actions.append( + dv.DivAction( + log_id=f"{log_id}-set-error-text", + url=f"div-action://set_variable?name={var_prefix}error_text&value={error_text}", + ) + ) + + if additional_actions: + actions.extend(additional_actions) + + return actions + + +def create_loading_actions( + log_id: str, + prefix: str = "", +) -> List[dv.DivAction]: + """ + Create actions to show loading state. + + Args: + log_id: Log identifier for the action + prefix: Variable name prefix + + Returns: + List of DivAction objects for loading state + """ + var_prefix = f"{prefix}_" if prefix else "" + return [ + dv.DivAction( + log_id=f"{log_id}-show-loading", + url=f"div-action://set_variable?name={var_prefix}loading_visible&value=1", + ), + dv.DivAction( + log_id=f"{log_id}-hide-success", + url=f"div-action://set_variable?name={var_prefix}success_visible&value=0", + ), + dv.DivAction( + log_id=f"{log_id}-hide-error", + url=f"div-action://set_variable?name={var_prefix}error_visible&value=0", + ), + ] + + +# ============================================================================ +# Feedback Container Builders +# ============================================================================ + +def create_success_container( + container_id: str = "success-container", + prefix: str = "", + language: LanguageOptions = LanguageOptions.RUSSIAN, + custom_text: Optional[str] = None, +) -> dv.DivContainer: + """ + Create a success feedback container with conditional visibility. + + Args: + container_id: ID for the container + prefix: Variable name prefix + language: Language for localization + custom_text: Custom success text (if None, uses variable) + + Returns: + DivContainer configured for success feedback display + """ + var_prefix = f"{prefix}_" if prefix else "" + texts = FEEDBACK_TEXTS.get(language, FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) + + text_content = custom_text or f"@{{{var_prefix}success_text}}" + if not custom_text: + # Use default text if variable is empty + text_content = texts["success_default"] + + return dv.DivContainer( + id=container_id, + orientation=dv.DivContainerOrientation.HORIZONTAL, + visibility=Expr(f"@{{{var_prefix}success_visible == 1 ? 'visible' : 'gone'}}"), + alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + 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="✅", + font_size=16, + margins=dv.DivEdgeInsets(right=10), + ), + dv.DivText( + text=text_content, + font_size=14, + font_family="Manrope", + text_color="#065F46", + font_weight=dv.DivFontWeight.MEDIUM, + line_height=20, + width=dv.DivMatchParentSize(weight=1), + ), + dv.DivText( + text="✕", + font_size=18, + text_color="#065F46", + font_weight=dv.DivFontWeight.BOLD, + paddings=dv.DivEdgeInsets(left=10, right=4), + actions=[ + dv.DivAction( + log_id=f"dismiss-success-{container_id}", + url=f"div-action://set_variable?name={var_prefix}success_visible&value=0", + ) + ], + ), + ], + ) + + +def create_error_container( + container_id: str = "error-container", + prefix: str = "", + language: LanguageOptions = LanguageOptions.RUSSIAN, + custom_text: Optional[str] = None, +) -> dv.DivContainer: + """ + Create an error feedback container with conditional visibility. + + Args: + container_id: ID for the container + prefix: Variable name prefix + language: Language for localization + custom_text: Custom error text (if None, uses variable) + + Returns: + DivContainer configured for error feedback display + """ + var_prefix = f"{prefix}_" if prefix else "" + texts = FEEDBACK_TEXTS.get(language, FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) + + text_expr = f"@{{{var_prefix}error_text}}" if not custom_text else custom_text + + return dv.DivContainer( + id=container_id, + orientation=dv.DivContainerOrientation.HORIZONTAL, + visibility=Expr(f"@{{{var_prefix}error_visible == 1 ? 'visible' : 'gone'}}"), + alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + 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=text_expr, + 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=f"dismiss-error-{container_id}", + url=f"div-action://set_variable?name={var_prefix}error_visible&value=0", + ) + ], + ), + ], + ) + + +def create_loading_container( + container_id: str = "loading-container", + prefix: str = "", + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> dv.DivContainer: + """ + Create a loading indicator container with conditional visibility. + + Args: + container_id: ID for the container + prefix: Variable name prefix + language: Language for localization + + Returns: + DivContainer configured for loading state display + """ + var_prefix = f"{prefix}_" if prefix else "" + texts = FEEDBACK_TEXTS.get(language, FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) + + return dv.DivContainer( + id=container_id, + orientation=dv.DivContainerOrientation.HORIZONTAL, + visibility=Expr(f"@{{{var_prefix}loading_visible == 1 ? 'visible' : 'gone'}}"), + alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + 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="#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=texts["loading"], + font_size=14, + font_family="Manrope", + text_color="#4B5563", + font_weight=dv.DivFontWeight.MEDIUM, + line_height=20, + ), + ], + ) + + +# ============================================================================ +# Complete Action Builders with Handlers +# ============================================================================ + +def create_submit_action_with_handlers( + log_id: str, + container_id: str, + url: str, + method: dv.RequestMethod = dv.RequestMethod.POST, + headers: Optional[List[dv.RequestHeader]] = None, + payload: Optional[Dict[str, Any]] = None, + success_text: str = "", + error_text: str = "", + prefix: str = "", + language: LanguageOptions = LanguageOptions.RUSSIAN, + additional_success_actions: Optional[List[dv.DivAction]] = None, + additional_failure_actions: Optional[List[dv.DivAction]] = None, +) -> dv.DivAction: + """ + Create a complete DivActionSubmit with on_success and on_fail handlers. + + Args: + log_id: Log identifier for the action + container_id: Container ID for the submit action + url: Request URL + method: HTTP method (default POST) + headers: Optional request headers + payload: Optional action payload + success_text: Text to display on success + error_text: Text to display on error + prefix: Variable name prefix + language: Language for localization + additional_success_actions: Extra actions on success + additional_failure_actions: Extra actions on failure + + Returns: + DivAction configured with submit and handlers + """ + texts = FEEDBACK_TEXTS.get(language, FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) + + final_success_text = success_text or texts["success_default"] + final_error_text = error_text or texts["error_default"] + + action = dv.DivAction( + log_id=log_id, + url="divkit://send-request", + payload=payload, + typed=dv.DivActionSubmit( + container_id=container_id, + request=dv.DivActionSubmitRequest( + url=url, + method=method, + headers=headers or [], + ), + on_success_actions=create_success_actions( + log_id=log_id, + success_text=final_success_text, + prefix=prefix, + additional_actions=additional_success_actions, + ), + on_fail_actions=create_failure_actions( + log_id=log_id, + error_text=final_error_text, + prefix=prefix, + additional_actions=additional_failure_actions, + ), + ), + ) + + return action + + +def create_simple_action_with_feedback( + log_id: str, + url: str, + payload: Optional[Dict[str, Any]] = None, + success_text: str = "", + prefix: str = "", + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> List[dv.DivAction]: + """ + Create a simple action (URL navigation, state change) with success feedback. + For actions that don't involve network requests but should show feedback. + + Args: + log_id: Log identifier for the action + url: Action URL + payload: Optional action payload + success_text: Text to display on success + prefix: Variable name prefix + language: Language for localization + + Returns: + List of DivAction objects (main action + feedback actions) + """ + texts = FEEDBACK_TEXTS.get(language, FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) + final_success_text = success_text or texts["success_default"] + + actions = [ + dv.DivAction( + log_id=log_id, + url=url, + payload=payload, + ), + ] + + # Add success feedback actions + actions.extend( + create_success_actions( + log_id=log_id, + success_text=final_success_text, + prefix=prefix, + ) + ) + + return actions + + +def create_selection_action( + log_id: str, + url: str, + payload: Dict[str, Any], + success_text: str = "", + prefix: str = "", + language: LanguageOptions = LanguageOptions.RUSSIAN, + show_feedback: bool = True, +) -> List[dv.DivAction]: + """ + Create a selection action (for cards, categories, suppliers, etc.) with optional feedback. + + Args: + log_id: Log identifier for the action + url: Action URL (typically divkit:// scheme) + payload: Selection payload data + success_text: Text to display on success + prefix: Variable name prefix + language: Language for localization + show_feedback: Whether to show success feedback + + Returns: + List of DivAction objects + """ + actions = [ + dv.DivAction( + log_id=log_id, + url=url, + payload=payload, + ), + ] + + if show_feedback: + texts = FEEDBACK_TEXTS.get(language, FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) + final_success_text = success_text or texts["success_default"] + + actions.extend( + create_success_actions( + log_id=log_id, + success_text=final_success_text, + prefix=prefix, + ) + ) + + return actions + + +# ============================================================================ +# Utility Functions +# ============================================================================ + +def get_feedback_text( + key: str, + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> str: + """ + Get localized feedback text by key. + + Args: + key: Text key (e.g., 'success_default', 'added_to_cart') + language: Language for localization + + Returns: + Localized text string + """ + texts = FEEDBACK_TEXTS.get(language, FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) + return texts.get(key, texts.get("success_default", "")) + + +def create_feedback_wrapper( + content: dv.Div, + prefix: str = "", + language: LanguageOptions = LanguageOptions.RUSSIAN, + include_loading: bool = False, +) -> dv.DivContainer: + """ + Wrap content with feedback containers (success, error, optionally loading). + + Args: + content: The main content div + prefix: Variable name prefix + language: Language for localization + include_loading: Whether to include loading indicator + + Returns: + DivContainer with content and feedback containers + """ + items = [] + + # Add feedback containers at the top + items.append(create_success_container(prefix=prefix, language=language)) + items.append(create_error_container(prefix=prefix, language=language)) + + if include_loading: + items.append(create_loading_container(prefix=prefix, language=language)) + + # Add the main content + items.append(content) + + return dv.DivContainer( + orientation=dv.DivContainerOrientation.VERTICAL, + items=items, + variables=create_feedback_variables(prefix=prefix), + width=dv.DivMatchParentSize(), + ) diff --git a/functions_to_format/functions/general/buttons.py b/functions_to_format/functions/general/buttons.py index ac440c0..e07ceed 100644 --- a/functions_to_format/functions/general/buttons.py +++ b/functions_to_format/functions/general/buttons.py @@ -1,8 +1,10 @@ import pydivkit as dv +from pydivkit.core import Expr import json from models.widget import Widget from .const_values import WidgetMargins, WidgetPaddings, ButtonInRowMargins from .const_values import LanguageOptions +from typing import Optional, List, Dict, Any class ButtonsWidget(Widget): @@ -12,19 +14,93 @@ class ButtonsWidget(Widget): fields: list[str] = ["text", "action"] -def make_contacts_search_button(txt, receiver_name): +# Feedback texts for button actions +BUTTON_FEEDBACK_TEXTS = { + LanguageOptions.RUSSIAN: { + "search_success": "Поиск выполнен", + "action_success": "Действие выполнено", + "action_error": "Ошибка выполнения", + "cancel_success": "Отменено", + }, + LanguageOptions.ENGLISH: { + "search_success": "Search completed", + "action_success": "Action completed", + "action_error": "Action failed", + "cancel_success": "Cancelled", + }, + LanguageOptions.UZBEK: { + "search_success": "Qidiruv bajarildi", + "action_success": "Amal bajarildi", + "action_error": "Xatolik yuz berdi", + "cancel_success": "Bekor qilindi", + }, +} + + +def create_button_success_actions( + log_id: str, + prefix: str = "btn", +) -> List[dv.DivAction]: + """Create success actions for button feedback.""" + return [ + dv.DivAction( + log_id=f"{log_id}-success-show", + url=f"div-action://set_variable?name={prefix}_success_visible&value=1", + ), + dv.DivAction( + log_id=f"{log_id}-hide-error", + url=f"div-action://set_variable?name={prefix}_error_visible&value=0", + ), + ] + + +def create_button_error_actions( + log_id: str, + prefix: str = "btn", +) -> List[dv.DivAction]: + """Create error actions for button feedback.""" + return [ + dv.DivAction( + log_id=f"{log_id}-error-show", + url=f"div-action://set_variable?name={prefix}_error_visible&value=1", + ), + dv.DivAction( + log_id=f"{log_id}-hide-success", + url=f"div-action://set_variable?name={prefix}_success_visible&value=0", + ), + ] + + +def make_contacts_search_button( + txt: str, + receiver_name, + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + """Create a search contacts button with feedback handling.""" + feedback_texts = BUTTON_FEEDBACK_TEXTS.get( + language, BUTTON_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) + 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 - ) + search_payload = {"name": receiver_name} + search_url = f"divkit://search_contact?name={receiver_name}" 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 - ) + search_payload = {"name": [receiver_name]} + search_url = f"divkit://search_contact?name=[{receiver_name}]" + + # Main search action + search_contact_action = dv.DivAction( + log_id="search_contact", + url=search_url, + payload=search_payload, + ) + + # Success feedback action (triggered after native handler) + success_action = dv.DivAction( + log_id="search_contact_success", + url="div-action://set_variable?name=btn_search_success_visible&value=1", + ) + return dv.DivText( text=txt, font_size=14, @@ -39,7 +115,7 @@ def make_contacts_search_button(txt, receiver_name): bottom=ButtonInRowMargins.BOTTOM.value, left=ButtonInRowMargins.LEFT.value, ), - action=search_contact_action, + actions=[search_contact_action, success_action], ) @@ -48,33 +124,96 @@ def build_buttons_row( receiver_name: str | None = None, language: LanguageOptions = LanguageOptions.RUSSIAN, ): + """ + Build a row of buttons with consistent styling and feedback handling. + + Args: + button_texts: List of button identifiers (e.g., ['search', 'cancel']) + receiver_name: Optional name for search contact functionality + language: Language for button text localization + + Returns: + DivKit JSON for the buttons row with feedback containers + """ items = [] + feedback_texts = BUTTON_FEEDBACK_TEXTS.get( + language, BUTTON_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) texts_map = { LanguageOptions.RUSSIAN: { "search": "Поиск", "cancel": "Отмена", + "confirm": "Подтвердить", + "retry": "Повторить", + "back": "Назад", + "next": "Далее", + "submit": "Отправить", }, LanguageOptions.ENGLISH: { "search": "Search", "cancel": "Cancel", + "confirm": "Confirm", + "retry": "Retry", + "back": "Back", + "next": "Next", + "submit": "Submit", }, LanguageOptions.UZBEK: { "search": "Qidirish", "cancel": "Bekor qilish", + "confirm": "Tasdiqlash", + "retry": "Qayta urinish", + "back": "Orqaga", + "next": "Keyingi", + "submit": "Yuborish", }, } for txt in button_texts: + button_text = texts_map[language].get(txt, txt) + if txt == "search": items.append( - make_contacts_search_button(texts_map[language][txt], receiver_name) + make_contacts_search_button(button_text, receiver_name, language) + ) + elif txt == "cancel": + # Cancel button with feedback + items.append( + dv.DivText( + text=button_text, + font_size=14, + text_color="#6B7280", + border=dv.DivBorder( + corner_radius=8, stroke=dv.DivStroke(color="#D1D5DB") + ), + 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=ButtonInRowMargins.RIGHT.value, + top=ButtonInRowMargins.TOP.value, + bottom=ButtonInRowMargins.BOTTOM.value, + left=ButtonInRowMargins.LEFT.value, + ), + actions=[ + dv.DivAction( + log_id=f"btn-cancel", + url=f"divkit://button/cancel", + payload={"action": "cancel"}, + ), + dv.DivAction( + log_id="btn-cancel-feedback", + url="div-action://set_variable?name=btn_action_completed&value=1", + ), + ], + ) ) - else: + # Standard button with feedback items.append( dv.DivText( - text=texts_map[language][txt], + text=button_text, font_size=14, text_color="#2563EB", border=dv.DivBorder( @@ -89,17 +228,122 @@ def build_buttons_row( bottom=ButtonInRowMargins.BOTTOM.value, left=ButtonInRowMargins.LEFT.value, ), - action=dv.DivAction( - log_id=f"btn-{txt.lower()}", - url=f"divkit://button/{txt.lower()}", - ), + actions=[ + dv.DivAction( + log_id=f"btn-{txt.lower()}", + url=f"divkit://button/{txt.lower()}", + payload={"action": txt.lower()}, + ), + dv.DivAction( + log_id=f"btn-{txt.lower()}-success", + url="div-action://set_variable?name=btn_success_visible&value=1", + ), + ], ) ) + # Create success feedback container + success_container = dv.DivContainer( + id="btn-success-container", + orientation=dv.DivContainerOrientation.HORIZONTAL, + visibility=Expr("@{btn_success_visible == 1 ? 'visible' : 'gone'}"), + alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + width=dv.DivMatchParentSize(), + margins=dv.DivEdgeInsets(top=8, bottom=4), + 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-btn-success", + url="div-action://set_variable?name=btn_success_visible&value=0", + ) + ], + ), + ], + ) + + # Create error feedback container + error_container = dv.DivContainer( + id="btn-error-container", + orientation=dv.DivContainerOrientation.HORIZONTAL, + visibility=Expr("@{btn_error_visible == 1 ? 'visible' : 'gone'}"), + alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + width=dv.DivMatchParentSize(), + margins=dv.DivEdgeInsets(top=8, bottom=4), + 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-btn-error", + url="div-action://set_variable?name=btn_error_visible&value=0", + ) + ], + ), + ], + ) + div = dv.make_div( dv.DivContainer( - orientation=dv.DivContainerOrientation.HORIZONTAL, - items=items, + orientation=dv.DivContainerOrientation.VERTICAL, + variables=[ + dv.IntegerVariable(name="btn_success_visible", value=0), + dv.IntegerVariable(name="btn_error_visible", value=0), + dv.IntegerVariable(name="btn_action_completed", value=0), + ], + items=[ + # Buttons row + dv.DivContainer( + orientation=dv.DivContainerOrientation.HORIZONTAL, + items=items, + ), + # Feedback containers + success_container, + error_container, + ], margins=dv.DivEdgeInsets( top=WidgetMargins.TOP.value, bottom=WidgetMargins.BOTTOM.value, diff --git a/functions_to_format/functions/human_approval.py b/functions_to_format/functions/human_approval.py index 55d7c32..946c875 100644 --- a/functions_to_format/functions/human_approval.py +++ b/functions_to_format/functions/human_approval.py @@ -1,4 +1,6 @@ from typing import Any + +from pydivkit.core import Expr from functions_to_format.functions.buttons import build_buttons_row from .general import ( add_ui_to_widget, @@ -122,7 +124,81 @@ def human_approval_ui( 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, + 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, + ) + ], ) + + # 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, + 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", + ) + ], + ), + ], + ) + buttons_container = dv.DivContainer( orientation=dv.DivContainerOrientation.HORIZONTAL, items=[ @@ -151,6 +227,22 @@ def human_approval_ui( 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", + ), + ], ), ), ], @@ -180,6 +272,22 @@ def human_approval_ui( 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", + ), + ], ), ), ], diff --git a/functions_to_format/functions/news.py b/functions_to_format/functions/news.py index ee56995..01c5232 100644 --- a/functions_to_format/functions/news.py +++ b/functions_to_format/functions/news.py @@ -1,4 +1,5 @@ import pydivkit as dv +from pydivkit.core import Expr import json from pydantic import BaseModel from typing import List @@ -12,6 +13,26 @@ from models.context import Context +# Feedback texts for news actions +NEWS_FEEDBACK_TEXTS = { + LanguageOptions.RUSSIAN: { + "opening": "Открываем новость...", + "opened": "Новость открыта", + "error": "Не удалось открыть новость", + }, + LanguageOptions.ENGLISH: { + "opening": "Opening article...", + "opened": "Article opened", + "error": "Failed to open article", + }, + LanguageOptions.UZBEK: { + "opening": "Yangilik ochilmoqda...", + "opened": "Yangilik ochildi", + "error": "Yangilikni ochib bo'lmadi", + }, +} + + class NewsItem(BaseModel): title: str source: str @@ -60,14 +81,39 @@ def get_news(context: Context) -> BuildOutput: return output -def news_item(title, source, time, image_url, url): +def news_item( + title: str, + source: str, + time: str, + image_url: str, + url: str, + index: int = 0, + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + """ + Create a news item container with action handling and feedback. + + Args: + title: News article title + source: News source name + time: Publication time + image_url: Thumbnail image URL + 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]) + 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, - # width=dv.DivWeight(1), items=[ dv.DivText( text=title, @@ -75,7 +121,6 @@ def news_item(title, source, time, image_url, url): font_weight=dv.DivFontWeight.MEDIUM, text_color="#FFFFFF", max_lines=1, - # ellipsis=dv.DivTextEllipsis(), ), dv.DivText( text=f"{source} · {time}", @@ -83,7 +128,6 @@ def news_item(title, source, time, image_url, url): text_color="#D1D5DB", margins=dv.DivEdgeInsets(top=2), max_lines=1, - # ellipsis=dv.DivTextEllipsis(), ), ], ), @@ -97,14 +141,43 @@ def news_item(title, source, time, image_url, url): ), ], margins=dv.DivEdgeInsets(top=12), - action=dv.DivAction(log_id="open-news", url=url), # this will open the web page + 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", + ), + ], ) -def build_news_widget(news_widget_input: NewsWidgetInput): - # Parse input data +def build_news_widget( + news_widget_input: NewsWidgetInput, + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + """ + Build the news widget with feedback handling for news item actions. + + 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]) - # Create news items + # Create news items with proper indexing news = [ news_item( title=item.title, @@ -112,10 +185,54 @@ def build_news_widget(news_widget_input: NewsWidgetInput): time=item.time, image_url=item.image_url, url=item.url, + index=idx, + language=language, ) - for item in news_widget_input.news_items + 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, + 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", + ) + ], + ), + ], + ) + items = [ # Header dv.DivContainer( @@ -132,6 +249,7 @@ def build_news_widget(news_widget_input: NewsWidgetInput): ), ] items.extend(news) + items.append(success_container) root = dv.DivContainer( orientation=dv.DivContainerOrientation.VERTICAL, @@ -139,8 +257,11 @@ def build_news_widget(news_widget_input: NewsWidgetInput): 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), # внешние отступы + ), + margins=dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8), + variables=[ + dv.IntegerVariable(name="news_opened", value=0), + ], items=items, ) diff --git a/functions_to_format/functions/payment.py b/functions_to_format/functions/payment.py index b19f862..9347144 100644 --- a/functions_to_format/functions/payment.py +++ b/functions_to_format/functions/payment.py @@ -1,5 +1,6 @@ import json +from functions_to_format.functions.balance import get_home_balances from functions_to_format.functions.functions import chatbot_answer from .general import ( @@ -10,12 +11,18 @@ 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 pydantic import BaseModel -from typing import List, Dict, Any, Optional, Union, Any -from models.build import BuildOutput +from typing import List, Dict, Any, Optional, Union 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, @@ -31,6 +38,29 @@ 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, @@ -357,8 +387,757 @@ def pay_for_home_utility(context: Context) -> BuildOutput: 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...", + }, +} + + +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. + + 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 + 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, + ), + ], + ) + 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", + ), + ], + ), + ], + ) + + div = dv.make_div(receiver_card) + with open("logs/json/build_receiver_by_card_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}, + ), + build_receiver_by_card_ui: WidgetInput( + widget=receiver_widget, + args={ + "receiver_data": receiver_data, + "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 + + +# ============================================================================ +# Send Money Transfer UI +# ============================================================================ + +def build_send_money_ui( + amount: int, + receiver_name: str, + masked_pan: str, + processing_system: str, + token: str, + chat_id: str, + api_key: str, + 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 + """ + 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", + ), + ], + ), + ], + ), + ], + ) + + div = dv.make_div(transfer_card) + with open("logs/json/build_send_money_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: - # Extract values from context + """ + 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 @@ -367,32 +1146,66 @@ def send_money_to_someone_via_card(context: Context) -> BuildOutput: api_key = context.api_key logger = context.logger_context.logger - backend_output = { - "amount": 1000, - "card_owner_name": "John Doe", - "marked_card_pan": "*XXXX", - "processingSystem": "SmartBank", - "to_card_id": "12345", - } - widgets = [ - Widget( - name="send_money_to_someone_via_card", - type="send_money_to_someone_via_card", - order=1, - layout="vertical", - fields=[ - "amount", - "card_owner_name", - "marked_card_pan", - "processingSystem", - "to_card_id", - ], - values=[{k: v} for k, v in backend_output.items()], - ui=backend_output, - ) - ] + 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=1, + widgets_count=len(widgets), widgets=[widget.model_dump(exclude_none=True) for widget in widgets], ) save_builder_output(context, output) @@ -425,7 +1238,7 @@ def get_number_by_receiver_name(context) -> BuildOutput: order=2, layout="vertical", fields=["receiver_name"], - values=[{"receiver_name": backend_output["receiver_name"]}], + values=[{"receiver_name": backend_output["names"]}], ) buttons = ButtonsWidget( @@ -434,9 +1247,9 @@ def get_number_by_receiver_name(context) -> BuildOutput: ) names = ( - ",".join(backend_output["receiver_name"]) - if isinstance(backend_output["receiver_name"], list) - else backend_output["receiver_name"] + ",".join(backend_output["names"]) + if isinstance(backend_output["names"], list) + else backend_output["names"] ) widgets = add_ui_to_widget( @@ -447,10 +1260,6 @@ def get_number_by_receiver_name(context) -> BuildOutput: "text": f"Contactlaringiz orasidan {names} ismini qidirishimiz kerak. Qidiraylikmi ?" }, ), - # get_number_by_reciver_number_ui: WidgetInput( - # widget=get_contacts_widget, - # args={"receiver_name": backend_output["receiver_name"]}, - # ), build_buttons_row: WidgetInput( widget=buttons, args={ @@ -568,19 +1377,22 @@ def get_receiver_id_by_receiver_phone_number(context: Context) -> BuildOutput: def get_receiver_id_by_receiver_phone_number_ui( cards: List[CardInfoByPhoneNumber], + language: LanguageOptions = LanguageOptions.RUSSIAN, ) -> Dict[str, Any]: """ - Builds a UI similar to the provided image, showing a list of cards for a receiver. - Expects backend_output to be a dict with a "cards" key containing a list of cards, - each with fields: "name", "processing", "mask". + Builds a UI showing a list of cards for a receiver with feedback handling. + + Args: + cards: List of card information + language: Language for localization + + Returns: + DivKit JSON for the card selection UI """ - # Example backend_output: - # { - # "cards": [ - # {"name": "ROMAN GORBUNOV", "processing": "HUMO", "mask": "2269"}, - # {"name": "ROMAN GORBUNOV", "processing": "HUMO", "mask": "3525"}, - # ] - # } + feedback_texts = PAYMENT_FEEDBACK_TEXTS.get( + language, PAYMENT_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) + card_items = [] for idx, card in enumerate(cards, 1): card_items.append( @@ -664,6 +1476,7 @@ def get_receiver_id_by_receiver_phone_number_ui( 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}", @@ -673,7 +1486,12 @@ def get_receiver_id_by_receiver_phone_number_ui( "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", + ), ], ) ) @@ -689,19 +1507,71 @@ def get_receiver_id_by_receiver_phone_number_ui( ) ) + # 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, - items=card_items, - background=[dv.DivSolidBackground(color="#F9FAFB")], - border=dv.DivBorder( - stroke=dv.DivStroke( - color="#E5E7EB", - width=1, + 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, + ), + paddings=dv.DivEdgeInsets(bottom=0, left=0, right=0, top=0), + margins=dv.DivEdgeInsets(bottom=0, left=12, right=12, top=12), ), - corner_radius=16, - ), - paddings=dv.DivEdgeInsets(bottom=0, left=0, right=0, top=0), - margins=dv.DivEdgeInsets(bottom=12, left=12, right=12, top=12), + success_container, + ], ) div = dv.make_div(main_container) @@ -776,15 +1646,29 @@ def get_categories(context) -> BuildOutput: return output -def build_get_categories_ui(categories: List[Category]): +def build_get_categories_ui( + categories: List[Category], + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + """ + Build categories UI with feedback handling for category selection. + + Args: + categories: List of category objects + language: Language for localization + + Returns: + DivKit JSON for the categories 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), - # height=dv.DivFixedSize( - # value=56 - # ), # Increased height to allow for vertical centering items=[ dv.DivImage( image_url=category.s3Url, @@ -799,12 +1683,23 @@ def build_get_categories_ui(categories: List[Category]): margins=dv.DivEdgeInsets(left=14), ), ], - alignment_vertical=dv.DivAlignmentVertical.CENTER, # Added vertical centering - 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}", - ), + 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) @@ -817,6 +1712,50 @@ def build_get_categories_ui(categories: List[Category]): ) ) + # 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", + ) + ], + ), + ], + ) + + category_items.append(success_container) + main_container = dv.DivContainer( orientation=dv.DivContainerOrientation.VERTICAL, width=dv.DivFixedSize(value=343), @@ -826,6 +1765,9 @@ def build_get_categories_ui(categories: List[Category]): 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), + ], ) div = dv.make_div(main_container) @@ -897,7 +1839,24 @@ def get_suppliers_by_category(context) -> BuildOutput: return output -def get_suppliers_by_category_ui(suppliers: List[Supplier]): +def get_suppliers_by_category_ui( + suppliers: List[Supplier], + language: LanguageOptions = LanguageOptions.RUSSIAN, +): + """ + Build suppliers UI with feedback handling for supplier selection. + + Args: + suppliers: List of supplier objects + language: Language for localization + + Returns: + DivKit JSON for the suppliers 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( @@ -924,6 +1883,7 @@ def get_suppliers_by_category_ui(suppliers: List[Supplier]): ), ], actions=[ + # Main selection action dv.DivAction( log_id=f"supplier_{supplier.id}_selected", payload={ @@ -932,14 +1892,11 @@ def get_suppliers_by_category_ui(suppliers: List[Supplier]): }, url=f"divkit://open_supplier?id={supplier.id}&name={supplier.name}", ), - # dv.DivAction( - # log_id=f"supplier_{supplier.id}_selected", - # payload={ - # "supplier_id": supplier.id, - # "supplier_name": supplier.name, - # }, - # url=f"divkit://select?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) @@ -952,6 +1909,50 @@ def get_suppliers_by_category_ui(suppliers: List[Supplier]): ) ) + # 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", + ) + ], + ), + ], + ) + + supplier_items.append(success_container) + main_container = dv.DivContainer( orientation=dv.DivContainerOrientation.VERTICAL, width=dv.DivMatchParentSize(), @@ -960,6 +1961,9 @@ def get_suppliers_by_category_ui(suppliers: List[Supplier]): 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), + ], ) div = dv.make_div(main_container) @@ -1007,7 +2011,7 @@ def get_fields_of_supplier(context) -> BuildOutput: maxValue=field["maxValue"], fieldSize=field["fieldSize"], isMain=field["isMain"], - valueList=field["valueList"], + valueList=[str(field["valueList"])], ) ) if llm_output: @@ -1116,7 +2120,7 @@ def get_fields_of_supplier_ui(fields: List[Field]): def get_home_utility_suppliers(context) -> BuildOutput: - return chatbot_answer(context) + return get_home_balances(context) if __name__ == "__main__": diff --git a/functions_to_format/functions/products.py b/functions_to_format/functions/products.py index 7688466..2545fe5 100644 --- a/functions_to_format/functions/products.py +++ b/functions_to_format/functions/products.py @@ -4,6 +4,7 @@ sys.path.append("/home/aslonhamidov/Desktop/work/ui_server") from functions_to_format.functions.general.const_values import LanguageOptions import pydivkit as dv +from pydivkit.core import Expr from datetime import timedelta import json, sys from pydantic import BaseModel @@ -13,6 +14,12 @@ add_ui_to_widget, WidgetInput, TextWidget, + create_feedback_variables, + create_success_actions, + create_failure_actions, + create_success_container, + create_error_container, + get_feedback_text, ) from .general.utils import save_builder_output from models.build import BuildOutput @@ -63,6 +70,7 @@ def get_products(context: Context) -> BuildOutput: values=[{"text": llm_output}], ) + logger.info("Products", products=products) widgets = add_ui_to_widget( { build_products_list_widget: WidgetInput( @@ -101,6 +109,7 @@ def make_product_state( """Create a product state element using pydivkit SDK classes""" product_state_id = f"product_{p.id}_{index}" # Make unique by adding index cart_container = f"cart_container_{p.id}_{index}_2" + feedback_prefix = f"product_{p.id}_{index}" texts_map = { LanguageOptions.ENGLISH: { @@ -110,6 +119,8 @@ def make_product_state( "more": "More", "hide": "Hide", "add": "Add to cart", + "added_to_cart": "Added to cart!", + "cart_error": "Failed to add to cart", }, LanguageOptions.UZBEK: { "price": "Narxi: {price}", @@ -118,6 +129,8 @@ def make_product_state( "more": "Ko'proq", "hide": "Yopish", "add": "Savatga qo'shish", + "added_to_cart": "Savatga qo'shildi!", + "cart_error": "Savatga qo'shib bo'lmadi", }, LanguageOptions.RUSSIAN: { "price": "Цена: {price}", @@ -126,6 +139,8 @@ def make_product_state( "more": "Подробнее", "hide": "Скрыть", "add": "Добавить в корзину", + "added_to_cart": "Добавлено в корзину!", + "cart_error": "Не удалось добавить в корзину", }, } @@ -197,28 +212,131 @@ def make_product_state( ), ], ), - # dv.DivText( - # text="⌄", # Changed to a down arrow - # font_size=24, - # text_color="#9CA3AF", - # width=dv.DivWrapContentSize(), - # alignment_horizontal=dv.DivAlignmentHorizontal.END, - # alignment_vertical=dv.DivAlignmentVertical.CENTER, - # # margins=dv.DivEdgeInsets(left="auto"), - # ), ], actions=[ dv.DivAction( - log_id="expand_product", + 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="expand_product", + log_id=f"expand_product_{p.id}_{index}_div", url=f"div-action://set_state?state_id=0/{product_state_id}/expanded", ), ], ) + # 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'}}" + ), + 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) + ), + 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", + ) + ], + ), + ], + ) + + # 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, + 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", + ) + ], + ), + ], + ) + + # Create on_success actions for cart add + cart_success_actions = [ + dv.DivAction( + log_id=f"cart-success-show-{feedback_prefix}", + url=f"div-action://set_variable?name={feedback_prefix}_success_visible&value=1", + ), + dv.DivAction( + log_id=f"cart-hide-error-{feedback_prefix}", + url=f"div-action://set_variable?name={feedback_prefix}_error_visible&value=0", + ), + ] + + # Create on_fail actions for cart add + cart_fail_actions = [ + dv.DivAction( + log_id=f"cart-error-show-{feedback_prefix}", + url=f"div-action://set_variable?name={feedback_prefix}_error_visible&value=1", + ), + dv.DivAction( + log_id=f"cart-hide-success-{feedback_prefix}", + url=f"div-action://set_variable?name={feedback_prefix}_success_visible&value=0", + ), + ] + # Expanded state div expanded_div = dv.DivContainer( orientation=dv.DivContainerOrientation.VERTICAL, @@ -226,6 +344,10 @@ def make_product_state( 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, @@ -252,23 +374,14 @@ def make_product_state( ), ], ), - # dv.DivText( - # text="⌃", # Changed to an up arrow - # font_size=24, - # text_color="#9CA3AF", - # width=dv.DivWrapContentSize(), - # alignment_horizontal=dv.DivAlignmentHorizontal.END, - # alignment_vertical=dv.DivAlignmentVertical.CENTER, - # margins=dv.DivEdgeInsets(), - # ), ], actions=[ dv.DivAction( - log_id="collapse_product", + 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="collapse_product", + log_id=f"collapse_product_{p.id}_{index}_div", url=f"div-action://set_state?state_id=0/{product_state_id}/collapsed", ), ], @@ -305,6 +418,9 @@ def make_product_state( corner_radius=4, stroke=dv.DivStroke(color="#FCE7F3") ), ), + # Feedback containers + success_container, + error_container, dv.DivContainer( orientation=dv.DivContainerOrientation.HORIZONTAL, margins=dv.DivEdgeInsets(top=12), @@ -315,7 +431,7 @@ def make_product_state( else dv.DivContentAlignmentHorizontal.CENTER ), alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - id=cart_container, # ✅ matches the ID above + id=cart_container, items=( # pyright: ignore[reportArgumentType] [ ( @@ -341,9 +457,9 @@ def make_product_state( actions=[ dv.DivAction( url="divkit://send-request", - log_id="1234", + log_id=f"add_to_cart_{p.id}_{index}", typed=dv.DivActionSubmit( - container_id=cart_container, # ✅ matches the ID above + 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, @@ -353,6 +469,8 @@ def make_product_state( ) ], ), + on_success_actions=cart_success_actions, + on_fail_actions=cart_fail_actions, ), payload={ "function_name": "add_product_to_cart", @@ -388,11 +506,11 @@ def make_product_state( ), actions=[ dv.DivAction( - log_id="collapse_from_button", + 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="collapse_from_button", + log_id=f"collapse_from_button_{p.id}_{index}_div", url=f"div-action://set_state?state_id=0/{product_state_id}/collapsed", ), ], @@ -416,11 +534,11 @@ def make_product_state( margins=dv.DivEdgeInsets(), actions=[ dv.DivAction( - log_id="collapse_from_button", + 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="collapse_from_button", + log_id=f"collapse_from_button_{p.id}_{index}_div", url=f"div-action://set_state?state_id=0/{product_state_id}/collapsed", ), ], @@ -448,6 +566,14 @@ def build_products_list_widget( api_key: str = "BLABLABLA", ): """Build products list widget using pydivkit SDK classes""" + logger = structlog.get_logger() + logger.info( + "Starting build_products_list_widget", + products_count=len(products_list_input) if products_list_input else 0, + language=language.value, + chat_id=chat_id, + ) + card_log_id = "products_list_card" texts_map = { @@ -463,6 +589,7 @@ 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), @@ -477,12 +604,24 @@ def build_products_list_widget( ) ) + logger.info( + "Creating product state elements", products_count=len(products_list_input) + ) # Create product state elements product_states = [] for index, product in enumerate(products_list_input): + logger.debug( + "Processing product", + product_id=product.id, + product_name=product.name, + index=index, + ) product_state = make_product_state(product, index, language, chat_id, api_key) product_states.append(product_state) + 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, @@ -491,10 +630,12 @@ def build_products_list_widget( items=product_states, ) + logger.info("Converting to div and saving output") div = dv.make_div(main_container) with open("logs/json/build_products_list_widget.json", "w", encoding="utf-8") as f: json.dump(div, f, indent=2, ensure_ascii=False) + logger.info("Successfully built products list widget") return div diff --git a/pyrightconfig.json b/pyrightconfig.json index f984215..6d968ff 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,13 +1,11 @@ { "exclude": [ + "build", + "dist", ".venv", "tests", "test_requests", - "configuration_manager", - "adapters", "temp", - "utils", - "telemetry/examples.py" ], "reportMissingImports": true, "reportUnusedImport": "warning", diff --git a/src/server.py b/src/server.py index da8cc50..308902a 100644 --- a/src/server.py +++ b/src/server.py @@ -1,3 +1,7 @@ +import sys +import os + +sys.path.append("/home/aslonhamidov/Desktop/work/ui_server") from conf import logger import os import time @@ -22,6 +26,8 @@ get_prometheus_metrics, ) +from fastapi.middleware.cors import CORSMiddleware + try: import tomllib @@ -44,6 +50,14 @@ metrics_collector = MetricsCollector() tracer = get_tracer() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + # Add telemetry middleware with metrics collector app.add_middleware(TelemetryMiddleware, metrics_collector=metrics_collector) @@ -57,6 +71,7 @@ # Serve static files app.mount("/ui_server/static", StaticFiles(directory="static"), name="static") + # https://www.youtbe.com/watch?v=NTP4XdTjRK0 if os.getenv("ENVIRONMENT") == "production": sentry_sdk.init( @@ -132,95 +147,93 @@ async def format_data_v3(request: Request, input_data: InputV3): logger = logger.bind(chat_id=input_data.chat_id) logger.info("BUILD UI V3") - with tracer.start_as_current_span("build_ui_v3") as span: - try: - logger.info("Step 1: Entering /chat/v3/build_ui") - language = LanguageOptions(request.headers.get("language", "ru")) - func_name = input_data.function_name - llm_output = input_data.llm_output or "" - backend_output = ( - input_data.backend_output if input_data.backend_output else {} - ) - api_key = input_data.api_key or "" + # with tracer.start_as_current_span("build_ui_v3") as span: + try: + logger.info("Step 1: Entering /chat/v3/build_ui") + language = LanguageOptions(request.headers.get("language", "ru")) + func_name = input_data.function_name + llm_output = input_data.llm_output or "" + backend_output = input_data.backend_output if input_data.backend_output else {} + api_key = input_data.api_key or "" - # Add telemetry attributes - span.set_attribute("function.name", func_name) - span.set_attribute("function.version", version) - span.set_attribute("language", language.value) + # Add telemetry attributes + # span.set_attribute("function.name", func_name) + # span.set_attribute("function.version", version) + # span.set_attribute("language", language.value) - logger.info( - "Received request parameters", - function_name=func_name, - llm_output=llm_output, - backend_output=backend_output, - api_key=input_data.api_key, - ) + logger.info( + "Received request parameters", + function_name=func_name, + llm_output=llm_output, + backend_output=backend_output, + api_key=input_data.api_key, + ) - if not func_name or func_name not in functions_mapper: - logger.warning(f"Invalid or missing function_name: {func_name}") - span.set_attribute("error", "invalid_function_name") - return JSONResponse( - status_code=400, - content=ErrorResponse( - error=f"Invalid or missing function_name: {func_name}", - traceback="", - ).model_dump(), - ) - - logger.info(f"Step 2: Found function {func_name}, invoking now") - - # Time function execution - func_start = time.time() - context = Context( - logger_context=LoggerContext( - chat_id=input_data.chat_id or "", logger=logger - ), - llm_output=llm_output, - backend_output=backend_output, # pyright: ignore[reportArgumentType] - version=version, - language=language, - api_key=api_key, + if not func_name or func_name not in functions_mapper: + logger.warning(f"Invalid or missing function_name: {func_name}") + # span.set_attribute("error", "invalid_function_name") + return JSONResponse( + status_code=400, + content=ErrorResponse( + error=f"Invalid or missing function_name: {func_name}", + traceback="", + ).model_dump(), ) - result: BuildOutput = functions_mapper[func_name]( - context=context, - ) - func_duration = (time.time() - func_start) * 1000 + logger.info(f"Step 2: Found function {func_name}, invoking now") + + # Time function execution + func_start = time.time() + context = Context( + logger_context=LoggerContext( + chat_id=input_data.chat_id or "", logger=logger + ), + llm_output=llm_output, + backend_output=backend_output, # pyright: ignore[reportArgumentType] + version=version, + language=language, + api_key=api_key, + ) + + result: BuildOutput = functions_mapper[func_name]( + context=context, + ) + func_duration = (time.time() - func_start) * 1000 + + # Record function metrics + metrics_collector.record_function_invocation( + function_name=func_name, + duration_ms=func_duration, + success=True, + version=version, + ) - # Record function metrics + # span.set_attribute("function.duration_ms", func_duration) + logger.info(f"Step 3: Function result={result}") + + return result + except Exception as e: + duration = (time.time() - start_time) * 1000 + + # Record error metrics + if "func_name" in locals(): metrics_collector.record_function_invocation( function_name=func_name, - duration_ms=func_duration, - success=True, + duration_ms=duration, + success=False, version=version, ) - span.set_attribute("function.duration_ms", func_duration) - logger.info(f"Step 3: Function result={result}") + # span.record_exception(e) + # span.set_attribute("error.message", str(e)) - return result - except Exception as e: - duration = (time.time() - start_time) * 1000 - - # Record error metrics - if "func_name" in locals(): - metrics_collector.record_function_invocation( - function_name=func_name, - duration_ms=duration, - success=False, - version=version, - ) - - span.record_exception(e) - span.set_attribute("error.message", str(e)) - - logger.exception(f"Exception in /chat/v3/build_ui: {str(e)}") - return JSONResponse( - status_code=500, - content=ErrorResponse( - error=str(e), traceback=str(e.__traceback__) - ).model_dump(), - ) + logger.exception(f"Exception in /chat/v3/build_ui: {str(e)}") + return JSONResponse( + status_code=500, + content=ErrorResponse( + error=str(e), traceback=str(e.__traceback__) + ).model_dump(), + ) @app.get("/chat") diff --git a/telemetry/setup.py b/telemetry/setup.py index ad72ba4..6555649 100644 --- a/telemetry/setup.py +++ b/telemetry/setup.py @@ -66,10 +66,10 @@ def setup_telemetry( ) trace_provider.add_span_processor(BatchSpanProcessor(otlp_trace_exporter)) - if environment == "development" or config.otel.console_export: - # Add console exporter for development - console_exporter = ConsoleSpanExporter() - trace_provider.add_span_processor(BatchSpanProcessor(console_exporter)) + # if environment == "development" or config.otel.console_export: + # # Add console exporter for development + # console_exporter = ConsoleSpanExporter() + # trace_provider.add_span_processor(BatchSpanProcessor(console_exporter)) trace.set_tracer_provider(trace_provider) _tracer = trace.get_tracer(__name__)