Spaces:
Running
Running
import streamlit as st | |
import folium | |
from streamlit_folium import folium_static | |
import streamlit.components.v1 as components | |
from datetime import datetime | |
import textwrap | |
import gspread | |
from google.oauth2.service_account import Credentials | |
import os | |
import json | |
import random | |
import time | |
import requests | |
# تنظیمات اولیهه | |
st.set_page_config(layout="wide", page_title="راهیار - تحلیل انصاف قیمتی", page_icon="🚖") | |
# ========== تنظیمات دیتا ========== | |
SHEET_ID = "1gpv_Li_p8SqiG-wBLeb0Mvs0qXJXKOnC_UuCGgs0RJg" | |
SHEET_NAME = "Sheet1" | |
# ========== استایلهای سفارشی یکپارچه ========== | |
st.markdown(""" | |
<style> | |
/* تنظیمات کلی استایلها */ | |
@font-face { | |
font-family: 'B Nazanin'; | |
src: url('https://cdn.jsdelivr.net/gh/rastikerdar/fonts@master/fonts/B%20Nazanin/B%20Nazanin.woff') format('woff'); | |
} | |
/* این کد را در ابتدای استایلهایتان قرار دهید */ | |
:root { | |
color-scheme: light !important; | |
} | |
body, [data-testid="stAppViewContainer"] { | |
color-scheme: light !important; | |
background-color: #ffffff !important; | |
color: #000000 !important; | |
} | |
/* بازنشانی تمام المانها به حالت لایت */ | |
* { | |
color-scheme: light !important; | |
} | |
:root { | |
color-scheme: light only !important; | |
--primary: #6a0dad; | |
--text: #333333; | |
--background: #ffffff; | |
--border: #dddddd; | |
--input-bg: #ffffff; | |
--secondary-bg: #f8f9fa; | |
--green: #f0ff0; | |
--dgreen: #006400; | |
} | |
* { | |
font-family: 'B Nazanin', 'B Nazanin Bold', sans-serif !important; | |
text-align: right !important; | |
direction: rtl !important; | |
} | |
/* ======== تنظیمات پایه ======== */ | |
* { | |
font-family: 'B Nazanin', 'B Nazanin Bold', sans-serif !important; | |
text-align: right !important; | |
direction: rtl !important; | |
box-sizing: border-box !important; | |
} | |
/* تنظیمات برای موبایل و نمایش موبایل در تمام دستگاهها */ | |
html, body, .stApp { | |
max-width: 768px !important; /* حداکثر عرض صفحه */ | |
margin: 0 auto; /* وسطچین کردن محتوا */ | |
width: 100% !important | |
background-color: var(--background) !important; | |
color: var(--text) !important; | |
} | |
@media (min-width: 768px) { | |
/* Container for the mobile frame */ | |
body { | |
background: #f0f0f0 !important; | |
display: flex !important; | |
justify-content: center !important; | |
align-items: center !important; | |
height: 100vh !important; | |
overflow: hidden !important; | |
} | |
/* Mobile frame styling */ | |
[data-testid="stAppViewContainer"] { | |
width: 475px !important; | |
height: 667px !important; | |
margin: 0 auto !important; | |
border: 12px solid #000 !important; | |
border-radius: 40px !important; | |
box-shadow: | |
0 0 0 6px #666, | |
0 0 30px rgba(0,0,0,0.5) !important; | |
background-color: var(--background) !important; | |
color: var(--text) !important; | |
overflow-y: auto !important; | |
overflow-x: hidden !important; | |
scrollbar-width: thin !important; | |
scrollbar-color: #6a0dad #f0f0f0 !important; | |
direction: rtl !important; | |
} | |
/* Remove any potential dark background from parent elements */ | |
#root, .stApp { | |
background: transparent !important; | |
margin: 0 auto; | |
} | |
/* Ensure Streamlit's main container doesn't add extra space */ | |
[data-testid="stAppViewContainer"] > div { | |
max-width: 100% !important; | |
margin: 0 auto; | |
} | |
/* Webkit scrollbar styles */ | |
[data-testid="stAppViewContainer"]::-webkit-scrollbar { | |
width: 6px !important; | |
right: 0 !important; | |
left: auto !important; | |
} | |
[data-testid="stAppViewContainer"]::-webkit-scrollbar-track { | |
background: #f0f0f0 !important; | |
border-radius: 3px !important; | |
margin: 10px 0 !important; | |
} | |
[data-testid="stAppViewContainer"]::-webkit-scrollbar-thumb { | |
background-color: #6a0dad !important; | |
border-radius: 3px !important; | |
margin-left: 10px !important; | |
} | |
/* Reset direction for content */ | |
[data-testid="stAppViewContainer"] > * { | |
direction: ltr !important; | |
text-align: right !important; | |
} | |
/* Virtual home button */ | |
[data-testid="stAppViewContainer"]::after { | |
content: ""; | |
position: absolute; | |
bottom: 15px; | |
left: 50%; | |
transform: translateX(-50%); | |
width: 100px; | |
height: 4px; | |
background: #333; | |
border-radius: 2px; | |
} | |
/* تنظیم پدینگ عمومی با * */ | |
[data-testid="stAppViewContainer"] * { | |
padding-right: 5px !important; | |
padding-left: 8px !important; | |
box-sizing: border-box !important; | |
} | |
/* استثناهای اصلی */ | |
[data-testid="stAppViewContainer"]::after, | |
[data-testid="stAppViewContainer"]::-webkit-scrollbar, | |
[data-testid="stAppViewContainer"] .stButton>button, | |
[data-testid="stAppViewContainer"] .stTextInput>div>div>input, | |
[data-testid="stAppViewContainer"] .stNumberInput>div>div>input, | |
[data-testid="stAppViewContainer"] .stSelectbox>div>div>select, | |
[data-testid="stAppViewContainer"] .stTextArea>div>textarea, | |
[data-testid="stAppViewContainer"] .stDateInput>div>div>input, | |
[data-testid="stAppViewContainer"] .stTimeInput>div>div>input, | |
[data-testid="stAppViewContainer"] .price-container, | |
[data-testid="stAppViewContainer"] .stAlert, | |
[data-testid="stAppViewContainer"] .stMarkdown, | |
[data-testid="stAppViewContainer"] .folium-map, | |
[data-testid="stAppViewContainer"] .stRadio>div>div>div>label, | |
[data-testid="stAppViewContainer"] .stCheckbox>div>div>label, | |
[data-testid="stAppViewContainer"] .stSlider>div>div>div>div, | |
[data-testid="stAppViewContainer"] .stMultiSelect>div>div>div { | |
padding-right: 0 !important; | |
padding-left: 0 !important; | |
} | |
/* تنظیمات خاص برای کانتینرهای Streamlit */ | |
[data-testid="stAppViewContainer"] [data-testid="stVerticalBlock"]>div, | |
[data-testid="stAppViewContainer"] [data-testid="stHorizontalBlock"]>div { | |
padding-right: 5px !important; | |
padding-left: 8px !important; | |
} | |
/* بازنشانی پدینگ برای المانهای داخلی خاص */ | |
[data-testid="stAppViewContainer"] .stTextInput>div, | |
[data-testid="stAppViewContainer"] .stNumberInput>div, | |
[data-testid="stAppViewContainer"] .stSelectbox>div, | |
[data-testid="stAppViewContainer"] .stTextArea>div, | |
[data-testid="stAppViewContainer"] .stDateInput>div, | |
[data-testid="stAppViewContainer"] .stTimeInput>div { | |
padding-right: 0 !important; | |
padding-left: 0 !important; | |
} | |
} | |
/* هدر راهیار */ | |
.rahyar-title { | |
color: var(--primary) !important; | |
margin: 0 !important; | |
} | |
.rahyar-subtitle { | |
color: var(--primary) !important; | |
margin: 0 !important; | |
font-size: 12px !important; | |
} | |
.warning { | |
color: black !important; | |
margin: 0 !important; | |
font-weight: bold !important; | |
font-size: 16px !important; | |
} | |
.little { | |
color: black !important; | |
margin: 0 !important; | |
font-size: 16px !important; | |
} | |
/* توضیحات */ | |
.explanation-title { | |
color: var(--primary) !important; | |
font-weight: bold !important; | |
margin: 20px 0 10px 0 !important; | |
font-size: 18px !important; | |
} | |
.explanation-item { | |
background-color: var(--secondary-bg) !important; | |
border-radius: 8px !important; | |
padding: 12px 15px !important; | |
margin: 8px 0 !important; | |
border-right: 3px solid var(--primary) !important; | |
font-size: 18px !important; | |
} | |
/* ========== استایلهای ورودی یکپارچه ========== */ | |
/* استایل پایه برای تمام کانتینرهای ورودی */ | |
div[data-baseweb="select"] > div:first-child, | |
div[data-baseweb="input"] > div:first-child, | |
div[data-baseweb="textarea"] > div:first-child { | |
color: var(--text) !important; | |
border-radius: 6px !important; | |
border: 1px solid var(--primary) !important; | |
background-color: white !important; | |
} | |
/* استایل داخلی برای المانهای ورودی */ | |
div[data-baseweb="select"] input, | |
div[data-baseweb="input"] input, | |
div[data-baseweb="textarea"] textarea { | |
color: var(--text) !important; | |
background-color: white !important; | |
border: none !important; | |
min-height: 40px !important; | |
font-family: 'B Nazanin' !important; | |
direction: rtl !important; | |
} | |
/* استایل خاص برای سلکت باکس */ | |
div[data-baseweb="select"] > div:first-child { | |
-webkit-appearance: none !important; | |
-moz-appearance: none !important; | |
appearance: none !important; | |
background-image: none !important; | |
padding-right: 30px !important; /* فضای برای فلش */ | |
} | |
/* فلش سفارشی برای سلکت باکس */ | |
div[data-baseweb="select"]::after { | |
content: "▼"; | |
position: absolute; | |
left: 7px !important; | |
top: 50%; | |
transform: translateY(-50%); | |
color: var(--primary) !important; | |
background-color: white !important; /* پسزمینه سفید برای پوشاندن فلش پیشفرض */ | |
font-size: 14px !important; | |
pointer-events: none; | |
} | |
/* استایل dropdown */ | |
div[data-baseweb="popover"] { | |
border: 1px solid var(--primary) !important; | |
border-radius: 6px !important; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important; | |
} | |
/* آیتمهای dropdown */ | |
div[data-baseweb="popover"] [role="option"] { | |
color: var(--text) !important; | |
background-color: white !important; | |
padding: 10px 12px !important; | |
font-family: 'B Nazanin' !important; | |
direction: rtl !important; | |
} | |
/* آیتم انتخاب شده در dropdown */ | |
div[data-baseweb="popover"] [role="option"][aria-selected="true"] { | |
background-color: #f0e6ff !important; | |
} | |
/* استایل hover برای تمام المانهای ورودی */ | |
div[data-baseweb="select"] > div:first-child:hover, | |
div[data-baseweb="input"] > div:first-child:hover, | |
div[data-baseweb="textarea"] > div:first-child:hover { | |
border-color: var(--primary) !important; | |
box-shadow: 0 0 0 3px rgba(106, 13, 173, 0.15) !important; | |
} | |
/* استایل focus */ | |
div[data-baseweb="select"] > div:first-child:focus-within, | |
div[data-baseweb="input"] > div:first-child:focus-within, | |
div[data-baseweb="textarea"] > div:first-child:focus-within { | |
border-color: var(--primary) !important; | |
box-shadow: 0 0 0 3px rgba(106, 13, 173, 0.15) !important; | |
outline: none !important; | |
} | |
/* استایل لیبلها */ | |
.stTextInput > label, | |
.stNumberInput > label, | |
.stSelectbox > label, | |
.stRadio > label, | |
.stSlider > label { | |
color: var(--text) !important; | |
font-weight: bold !important; | |
margin-bottom: 8px !important; | |
font-size: 16px !important; | |
display: block !important; | |
} | |
/* استایل placeholder */ | |
::placeholder { | |
color: var(--text) !important; | |
opacity: 0.5 !important; | |
font-family: 'B Nazanin' !important; | |
} | |
/* پاکسازی عناصر سیاه رنگ ناخواسته */ | |
div[data-testid="stNumberInput"] > div > div:first-child, | |
div[data-testid="stNumberInput"] > div > div:last-child { | |
background-color: transparent !important; | |
border: none !important; | |
} | |
/* حذف عناصر سیاه پسزمینه */ | |
div[data-testid="stNumberInput"] button > div { | |
display: none !important; | |
} | |
/* حذف کامل عناصر داخلی ناخواسته */ | |
div[data-testid="stNumberInput"] button > div, | |
div[data-testid="stNumberInput"] button > svg { | |
display: none !important; | |
visibility: hidden !important; | |
} | |
/* ایجاد آیکونهای سفارشی با رنگ بنفش */ | |
div[data-testid="stNumberInput"] button { | |
position: relative !important; | |
color: transparent !important; /* مخفی کردن متن پیشفرض */ | |
} | |
/* دکمه سمت راست (کاهش) */ | |
div[data-testid="stNumberInput"] button:first-child::after { | |
content: "-" !important; | |
position: absolute !important; | |
top: 50% !important; | |
left: 50% !important; | |
transform: translate(-50%, -50%) !important; | |
color: var(--primary) !important; | |
font-size: 1.2rem !important; | |
font-weight: bold !important; | |
} | |
/* دکمه سمت چپ (افزایش) */ | |
div[data-testid="stNumberInput"] button:last-child::after { | |
content: "+" !important; | |
position: absolute !important; | |
top: 50% !important; | |
left: 50% !important; | |
transform: translate(-50%, -50%) !important; | |
color: var(--primary) !important; | |
font-size: 1.2rem !important; | |
font-weight: bold !important; | |
} | |
/* بازنشانی کامل استایلهای Streamlit */ | |
div[data-testid="stNumberInput"] button { | |
all: unset !important; | |
width: 36px !important; | |
height: 100% !important; | |
position: relative !important; | |
cursor: pointer !important; | |
z-index: 10 !important; | |
} | |
/* تضمین رنگ در حالتهای مختلف */ | |
div[data-testid="stNumberInput"] button:hover::after, | |
div[data-testid="stNumberInput"] button:focus::after { | |
color: var(--primary) !important; | |
} | |
/* استایل hover برای دکمهها */ | |
div[data-testid="stNumberInput"] button:hover { | |
background-color: #f0e6ff !important; | |
} | |
/* استایل focus */ | |
div[data-testid="stNumberInput"] > div:focus-within { | |
border-color: var(--primary) !important; | |
box-shadow: 0 0 0 3px rgba(106, 13, 173, 0.15) !important; | |
outline: none !important; | |
} | |
/* کامپوننتهای سفارشی */ | |
.rahyar-header { | |
background-color: var(--primary) !important; | |
color: white !important; | |
padding: 15px !important; | |
border-radius: 10px !important; | |
margin-bottom: 20px !important; | |
text-align: center !important; | |
} | |
.price-container { | |
background-color: var(--secondary-bg) !important; | |
border-radius: 10px !important; | |
padding: 15px !important; | |
margin: 15px 0 !important; | |
border-right: 5px solid var(--primary) !important; | |
} | |
/* دکمه اصلی (بنفش با متن سفید) */ | |
.stButton>button, | |
[data-testid="baseButton-primary"], | |
.accept-btn, | |
div[data-testid="stVerticalBlock"] > div[data-testid="stHorizontalBlock"] > div > div > button { | |
background-color: var(--primary) !important; | |
color: white !important; | |
border: none !important; | |
border-radius: 8px !important; | |
padding: 10px 20px !important; | |
font-weight: bold !important; | |
transition: all 0.3s ease !important; /* افزودن transition برای انیمیشن نرم */ | |
} | |
/* افکت hover برای دکمه اصلی */ | |
.stButton>button:hover, | |
[data-testid="baseButton-primary"]:hover, | |
.accept-btn:hover, | |
div[data-testid="stVerticalBlock"] > div[data-testid="stHorizontalBlock"] > div > div > button:hover { | |
background-color: #5a0a96 !important; /* بنفش تیرهتر */ | |
transform: translateY(-1px) !important; /* حرکت جزئی به بالا */ | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important; /* سایه بیشتر */ | |
} | |
/* دکمه ثانویه (سفید با حاشیه بنفش) */ | |
.stFormSubmitButton>button, | |
[data-testid="baseButton-secondary"], | |
.reject-btn { | |
background-color: var(--primary) !important; | |
color: white !important; | |
border: none !important; | |
border-radius: 8px !important; | |
padding: 10px 20px !important; | |
font-weight: bold !important; | |
transition: all 0.3s ease !important; /* افزودن transition برای انیمیشن نرم */ | |
} | |
/* افکت hover برای دکمه ثانویه */ | |
.stFormSubmitButton>button:hover, | |
[data-testid="baseButton-secondary"]:hover, | |
.reject-btn:hover { | |
background-color: #5a0a96 !important; /* بنفش تیرهتر */ | |
transform: translateY(-1px) !important; /* حرکت جزئی به بالا */ | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important; /* سایه بیشتر */ | |
} | |
/* استایل تضمینی برای رادیو باتنها - نسخه نهایی */ | |
.stRadio > div > div > div > div > div > label, | |
.stRadio > div > div > div > div > label, | |
.stRadio > div > div > div > label, | |
.stRadio > div > div > label, | |
.stRadio > div > label, | |
.stRadio > label { | |
color: #000000 !important; | |
-webkit-text-fill-color: #000000 !important; /* برای مرورگرهای وبکیت */ | |
font-size: 20px !important | |
} | |
.stRadio span { | |
color: #000000 !important; | |
-webkit-text-fill-color: #000000 !important; | |
font-size: 20px !important | |
} | |
/* اگر باز هم مشکل داشت این را اضافه کنید */ | |
.stRadio > div > div > div > div > div > label > div:first-child, | |
.stRadio > div > div > div > div > label > div:first-child, | |
.stRadio > div > div > div > label > div:first-child, | |
.stRadio > div > div > label > div:first-child, | |
.stRadio > div > label > div:first-child, | |
.stRadio > label > div:first-child { | |
color: #000000 !important; | |
-webkit-text-fill-color: #000000 !important; | |
font-size: 20px !important | |
} | |
/* استایل پایه برای هشدارها */ | |
.stAlert .st-ae { | |
border-right: 4px solid #ffc107 !important; | |
background-color: #fff3cd !important; | |
border-radius: 8px !important; | |
padding: 16px !important; | |
color: #856404 !important; | |
} | |
/* آیکون هشدار */ | |
.stAlert .st-af { | |
color: #ffc107 !important; | |
font-size: 20px !important; | |
} | |
/* متن هشدار */ | |
.stAlert .st-ag { | |
color: #856404 !important; | |
font-family: 'B Nazanin' !important; | |
font-size: 16px !important; | |
margin-right: 8px !important; | |
} | |
/* حالت hover برای هشدار */ | |
.stAlert .st-ae:hover { | |
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.2) !important; | |
transform: translateX(2px); | |
transition: all 0.3s ease; | |
} | |
/* استایل برای دکمه بستن هشدار */ | |
.stAlert .st-ah { | |
color: #856404 !important; | |
} | |
/* استایل مخصوص موبایل */ | |
@media (max-width: 768px) { | |
.stAlert .st-ae { | |
padding: 12px !important; | |
font-size: 15px !important; | |
} | |
} | |
/* متن گزینهها */ | |
.stRadio span { | |
color: #000000 !important; /* مشکی خالص */ | |
font-size: 20px !important; | |
padding-right: 5px !important; | |
} | |
/* حالت hover */ | |
.stRadio label:hover span { | |
color: #333333 !important; /* مشکی کمی روشنتر */ | |
font-size: 20px !important; | |
} | |
/* تنظیمات مخصوص موبایل */ | |
@media (max-width: 768px) { | |
.folium-map { | |
height: 300px !important; | |
} | |
/* فرمها در موبایل */ | |
.stTextInput>label, | |
.stNumberInput>label, | |
.stSelectbox>label { | |
font-size: 16px !important; | |
padding: 4px 0 !important; | |
} | |
.stTextInput input, | |
.stNumberInput input, | |
.stSelectbox select { | |
font-size: 16px !important; | |
padding: 12px !important; | |
height: auto !important; | |
} | |
.stButton>button { | |
font-size: 17px !important; | |
padding: 8px 16px !important; | |
} | |
.stMarkdown h2 { | |
font-size: 22px !important; | |
} | |
.stMarkdown h3 { | |
font-size: 19px !important; | |
} | |
.stMarkdown p { | |
font-size: 17px !important; | |
} | |
.stSelectbox [role="listbox"] { | |
font-size: 18px !important; | |
} | |
.stRadio span { | |
color: #000000 !important; /* مشکی خالص */ | |
font-size: 20px !important; | |
padding-right: 5px !important; | |
} | |
/* حالت hover */ | |
.stRadio label:hover span { | |
color: #333333 !important; /* مشکی کمی روشنتر */ | |
font-size: 20px !important; | |
} | |
} | |
/* ======== تنظیمات پایه واکنشگرا ======== */ | |
:root { | |
/* اندازههای پایه بر اساس عرض 375px (آیفون X) */ | |
--base-font-size: 20px; | |
--base-heading1-size: 24px; | |
--base-heading2-size: 22px; | |
--base-heading3-size: 20px; | |
--base-input-font: 16px; | |
--base-button-font: 16px; | |
} | |
/* ======== تنظیمات پویا بر اساس عرض دستگاه ======== */ | |
@media (max-width: 400px) { | |
:root { | |
color-scheme: light only !important; | |
--base-font-size: 17px; | |
--base-heading1-size: 22px; | |
--base-heading2-size: 18px; | |
--base-heading3-size: 16px; | |
--base-input-font: 14px; | |
--base-button-font: 14px; | |
} | |
} | |
@media (min-width: 401px) and (max-width: 768px) { | |
:root { | |
color-scheme: light only !important; | |
--base-font-size: 17px; | |
--base-heading1-size: 24px; | |
--base-heading2-size: 20px; | |
--base-heading3-size: 18px; | |
--base-input-font: 16px; | |
--base-button-font: 16px; | |
} | |
} | |
/* ======== اعمال اندازههای پویا ======== */ | |
* { | |
font-size: var(--base-font-size) !important; | |
} | |
h1 { | |
font-size: var(--base-heading1-size) !important; | |
} | |
h2 { | |
font-size: var(--base-heading2-size) !important; | |
} | |
h3 { | |
font-size: var(--base-heading3-size) !important; | |
} | |
.stTextInput input, | |
.stNumberInput input, | |
.stSelectbox select, | |
.stTextArea textarea { | |
font-size: var(--base-input-font) !important; | |
} | |
.stButton>button { | |
font-size: var(--base-button-font) !important; | |
} | |
/* ======== تنظیمات نقشه واکنشگرا ======== */ | |
.folium-map { | |
height: calc(100vw * 0.8) !important; /* ارتفاع متناسب با عرض */ | |
max-height: 400px !important; | |
} | |
/* ======== تنظیمات کانتینرها ======== */ | |
.price-container, | |
.explanation-item { | |
padding: calc(var(--base-font-size) * 0.8) !important; | |
margin: calc(var(--base-font-size) * 0.5) 0 !important; | |
} | |
/* ======== تنظیمات دکمهها ======== */ | |
.stButton>button { | |
padding: calc(var(--base-font-size) * 0.7) calc(var(--base-font-size) * 1.2) !important; | |
} | |
/* اضافه کردن این بخش جدید */ | |
[data-testid="stAppViewContainer"] img { | |
max-width: 100% !important; | |
height: auto !important; | |
display: block !important; | |
padding: 0 !important; | |
margin: 0 auto !important; | |
} | |
/* برای تضمین نمایش در حالت موبایل شبیهسازی شده */ | |
@media (min-width: 768px) { | |
[data-testid="stAppViewContainer"] img { | |
max-width: 200px !important; | |
} | |
} | |
/* تنظیم justify برای تمام متنها */ | |
[data-testid="stAppViewContainer"] * { | |
text-align: justify !important; | |
text-justify: inter-word !important; | |
} | |
/* استثناها برای عناصری که نباید justify شوند */ | |
[data-testid="stAppViewContainer"] .stRadio>label, | |
[data-testid="stAppViewContainer"] .stCheckbox>label, | |
[data-testid="stAppViewContainer"] .stSelectbox>label, | |
[data-testid="stAppViewContainer"] .stTextInput>label, | |
[data-testid="stAppViewContainer"] .stNumberInput>label, | |
[data-testid="stAppViewContainer"] input, | |
[data-testid="stAppViewContainer"] select, | |
[data-testid="stAppViewContainer"] textarea, | |
[data-testid="stAppViewContainer"] .rahyar-title, | |
[data-testid="stAppViewContainer"] .rahyar-subtitle { | |
direction: rtl !important; | |
text-align: right !important; | |
text-justify: auto !important; | |
} | |
/* تنظیمات برای لیستها */ | |
[data-testid="stAppViewContainer"] ul, | |
[data-testid="stAppViewContainer"] ol { | |
padding-right: 25px !important; | |
} | |
[data-testid="stAppViewContainer"] p:not(.stButton p):not(.stDownloadButton p):not(.stFormSubmitButton p) { | |
margin-bottom: 1em !important; | |
line-height: 1.8 !important; | |
} | |
body, .stApp { | |
color-scheme: light only !important; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# ========== توابع اصلی ========== | |
def enhanced_likert_scale(question_data): | |
question = question_data["question"] | |
key = question_data["key"] | |
scale = question_data["scale"] | |
labels = question_data.get("labels", ["کاملاً مخالفم", "کاملاً موافقم"]) | |
# نمایش سوال | |
st.markdown(f"<div style='text-align:right; font-weight:bold; margin-bottom:15px; direction: rtl;'>{question}</div>", | |
unsafe_allow_html=True) | |
# رادیو باتن اصلی (مخفی) | |
selected_value = st.radio( | |
"", | |
options=list(range(1, scale+1)), | |
index=st.session_state.get(key, 0) - 1 if st.session_state.get(key, 0) > 0 else None, | |
label_visibility="collapsed", | |
horizontal=True, | |
key=f"{key}_radio" | |
) | |
# مخفی کردن رادیو باتن | |
st.markdown( | |
""" | |
<style> | |
div[data-testid="stRadio"] { | |
display: none; | |
} | |
</style> | |
""", | |
unsafe_allow_html=True | |
) | |
# ذخیره مقدار انتخاب شده | |
if selected_value is not None: | |
st.session_state[key] = selected_value | |
# ایجاد JavaScript | |
js_code = """ | |
<script> | |
function handleLikertClick(index) { | |
const radios = parent.document.querySelectorAll('div[data-testid="stRadio"] input[type="radio"]'); | |
if (radios.length > index) { | |
radios[index].click(); | |
} | |
} | |
</script> | |
""" | |
# ایجاد دکمههای سفارشی | |
options_html = "".join([ | |
f'<div class="likert-option {"selected" if st.session_state.get(key) == i+1 else ""} {"middle-option" if (scale % 2 == 1 and i+1 == (scale//2 + 1)) else ""}" ' | |
f'onclick="handleLikertClick({i})">' | |
f'<span class="likert-number">{i+1}</span>' | |
'</div>' | |
for i in range(scale) | |
]) | |
# تنظیم لیبل وسط برای مقیاسهای فرد | |
middle_label = "" | |
middle_label_text = "متوسط" | |
if scale % 2 == 1: # اگر مقیاس فرد باشد | |
middle_label = f""" | |
<div class='label-container middle-label'> | |
<span>{middle_label_text}</span> | |
</div> | |
""" | |
# ترکیب تمام بخشها با استایل بهبود یافته | |
components.html( | |
f""" | |
{js_code} | |
<style> | |
.likert-wrapper {{ | |
width: 100%; | |
max-width: 475px; | |
margin: 0 auto; | |
direction: rtl; | |
position: relative; | |
}} | |
.likert-container {{ | |
display: flex; | |
justify-content: space-between; | |
width: 100%; | |
padding-top: 50px; /* فضای کافی برای لیبلها و خطوط */ | |
}} | |
.likert-option {{ | |
width: 36px; | |
height: 36px; | |
border-radius: 50%; | |
background: white; | |
border: 2px solid #6a0dad; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
cursor: pointer; | |
margin: 0 3px; | |
transition: all 0.2s; | |
position: relative; | |
z-index: 1; | |
}} | |
.likert-option:hover {{ | |
transform: scale(1.1); | |
box-shadow: 0 0 10px rgba(106, 13, 173, 0.4); | |
}} | |
.likert-option.selected {{ | |
background: #6a0dad; | |
}} | |
.likert-number {{ | |
color: #6a0dad; | |
font-weight: bold; | |
font-size: 16px; | |
user-select: none; | |
transition: all 0.2s; | |
}} | |
.likert-option.selected .likert-number {{ | |
color: white; | |
}} | |
.labels-container {{ | |
display: flex; | |
justify-content: space-between; | |
width: 100%; | |
position: absolute; | |
top: 0; | |
direction: rtl; | |
}} | |
.label-container {{ | |
font-size: 14px; | |
font-weight: bold; | |
color: #6a0dad; | |
text-align: center; | |
flex: 1; | |
position: relative; | |
}} | |
.label-container span {{ | |
display: inline-block; | |
white-space: normal; /* اجازه میدهد متن بهصورت کامل نمایش داده شود */ | |
line-height: 1.2; /* فاصله خطوط برای خوانایی بهتر */ | |
}} | |
.left-label {{ | |
text-align: right; | |
padding-right: 10px; | |
}} | |
.right-label {{ | |
text-align: left; | |
padding-left: 10px; | |
}} | |
.middle-label {{ | |
position: absolute; | |
left: 50%; | |
transform: translateX(-50%); | |
}} | |
/* خطوط ارتباطی فقط از سمت لیبلها */ | |
.left-label::before {{ | |
content: ''; | |
position: absolute; | |
bottom: -30px; | |
right: 10%; | |
width: 2px; | |
height: 25px; | |
background-color: #6a0dad; | |
}} | |
.right-label::before {{ | |
content: ''; | |
position: absolute; | |
bottom: -30px; | |
left: 10%; | |
width: 2px; | |
height: 25px; | |
background-color: #6a0dad; | |
}} | |
.middle-label::before {{ | |
content: ''; | |
position: absolute; | |
bottom: -30px; | |
left: 50%; | |
transform: translateX(-50%); | |
width: 2px; | |
height: 25px; | |
background-color: #6a0dad; | |
}} | |
</style> | |
<div class='likert-wrapper'> | |
<div class='labels-container'> | |
<div class='label-container left-label'> | |
<span>{labels[0]}</span> | |
</div> | |
{middle_label} | |
<div class='label-container right-label'> | |
<span>{labels[1]}</span> | |
</div> | |
</div> | |
<div class="likert-container"> | |
{options_html} | |
</div> | |
</div> | |
""", | |
height=120 # ارتفاع برای جا دادن لیبلها و خطوط | |
) | |
# نمایش وضعیت انتخاب | |
status = f"پاسخ شما: {st.session_state[key]}" if st.session_state.get(key) else "پاسخ شما: هنوز انتخاب نشده" | |
st.markdown( | |
f""" | |
<p style=' | |
text-align: right; | |
color: #6a0dad; | |
direction: rtl; | |
margin-top: 20px; | |
padding: 10px; | |
background-color: #f8f0ff; | |
border-radius: 8px; | |
border-right: 3px solid #6a0dad; | |
'> | |
{status} | |
</p> | |
""", | |
unsafe_allow_html=True | |
) | |
return st.session_state.get(key) | |
def create_ride_map(): | |
origin = [35.7665280, 51.3300394] # پونک شمالی | |
destination = [35.7552343, 51.4204264] # پل طبیعت | |
purple = "#6a0dad" | |
# ساخت نقشه | |
m = folium.Map( | |
location=[(origin[0] + destination[0]) / 2, (origin[1] + destination[1]) / 2], | |
zoom_start=12 # 👈 عدد زوم اینجاست | |
) | |
# آیکون مبدأ | |
origin_icon = folium.CustomIcon( | |
icon_image='origin.png', | |
icon_size=(80, 48) | |
) | |
folium.Marker( | |
location=origin, | |
icon=origin_icon, | |
tooltip="مبدأ: پونک شمالی" | |
).add_to(m) | |
# آیکون مقصد | |
destination_icon = folium.CustomIcon( | |
icon_image='destination.png', | |
icon_size=(80, 46) | |
) | |
folium.Marker( | |
location=destination, | |
icon=destination_icon, | |
tooltip="مقصد: پل طبیعت" | |
).add_to(m) | |
# خط مسیر بنفش | |
folium.PolyLine( | |
[origin, destination], | |
color=purple, | |
weight=4, | |
opacity=0.8, | |
dash_array="5,5" | |
).add_to(m) | |
return m | |
def show_explanation(exp_type): | |
"""نمایش توضیحات قیمت""" | |
explanations = { | |
"input": [ | |
" سطح تقاضا در منطقه: زیاد (+)", | |
" تعداد رانندگان فعال: کم (+)", | |
" زمان روز: ساعت اوج ترافیک (+)", | |
"شرایط جوی: هوای بارانی (++)" | |
], | |
"counterfactual": [ | |
f"اگر عجله ندارید و درخواست خود را 1 ساعت بعد تکرار کنید، احتمالاً تقاضا کمتر خواهد بود، رانندگان فعال در اطراف شما بیشتر خواهد بود، زمان روز و شرایط جوی بهتر خواهد بود؛ پس قیمت حدوداً 40٪ کمتر ({int(st.session_state.price * 0.6):,} تومان) خواهد بود.", | |
] | |
} | |
if exp_type in ["input", "counterfactual"]: | |
st.markdown("<p class='explanation-title'>توضیح رهیار درمورد علت قیمت گذاری:</p>", unsafe_allow_html=True) | |
if exp_type == "input": | |
st.markdown(""" | |
<div style="direction: rtl; text-align: right;"> | |
<span style="font-size: 0.9em; color: #666;">(تعداد علامت + نشان دهنده شدت اثر عامل بر قیمت است)</span> | |
</div> | |
""", unsafe_allow_html=True) | |
for item in explanations.get(exp_type, []): | |
st.markdown(f"<p class='explanation-item'>• {item}</p>", unsafe_allow_html=True) | |
# ========== توابع مدیریت دادهها ========== | |
def get_credentials(): | |
"""دریافت اعتبارنامه از Secrets""" | |
try: | |
service_account_json = os.environ.get('GCP_SERVICE_ACCOUNT') | |
if not service_account_json: | |
st.error("مقدار GCP_SERVICE_ACCOUNT در محیط یافت نشد") | |
return None | |
service_account_info = json.loads(service_account_json) | |
creds = Credentials.from_service_account_info( | |
service_account_info, | |
scopes=[ | |
"https://www.googleapis.com/auth/spreadsheets", | |
"https://www.googleapis.com/auth/drive.file" | |
] | |
) | |
return creds | |
except Exception as e: | |
st.error(f"خطا در دریافت اعتبارنامه: {str(e)}") | |
return None | |
def save_to_sheet(data): | |
try: | |
creds = get_credentials() | |
if not creds: | |
return False | |
client = gspread.authorize(creds) | |
spreadsheet = client.open_by_key(SHEET_ID) | |
worksheet = spreadsheet.worksheet(SHEET_NAME) | |
row_data = [ | |
data.get("start_time", ""), # زمان شروع | |
data.get("end_time", ""), # زمان پایان | |
data.get("completion_time", ""), # مدت زمان تکمیل (ثانیه) | |
data.get("scenario_type", ""), | |
data.get("price", ""), | |
data.get("age", ""), | |
data.get("gender", ""), | |
data.get("education", ""), | |
data.get("ride_frequency", ""), | |
data.get("related_education_job",""), | |
data.get("city",""), | |
data.get("user_contact", ""), | |
data.get("price_accepted", ""), | |
# سوالات توجه | |
data.get("attention_check1", ""), | |
data.get("attention_check2", ""), | |
# سوالات distributive (7 گزینهای) | |
data.get("distributive_1", ""), | |
data.get("distributive_2", ""), | |
data.get("distributive_3", ""), | |
# سوالات procedural (7 گزینهای) | |
data.get("procedural_1", ""), | |
data.get("procedural_2", ""), | |
data.get("procedural_3", ""), | |
# سوالات informational (5 گزینهای) | |
data.get("informational_1", ""), | |
data.get("informational_2", ""), | |
data.get("informational_3", ""), | |
data.get("informational_4", ""), | |
data.get("informational_5", ""), | |
# سوالات manipulation | |
data.get ("trust", ""), | |
data.get("pricing_method", ""), | |
data.get("price_increase", ""), | |
data.get("increase_amount", ""), | |
data.get("explanation_received", ""), | |
data.get("explanation_type", "") | |
] | |
worksheet.append_row(row_data) | |
return True | |
except Exception as e: | |
st.error(f"خطا در ذخیرهسازی: {str(e)}") | |
return False | |
# ========== بخشهای فرم ========== | |
def welcome_page(): | |
"""صفحه خوشامدگویی""" | |
st.markdown(""" | |
<div style="display: flex; flex-direction: column; align-items: center; background-color: #f0f2f6; border-radius: 10px; padding: 20px; gap: 15px;"> | |
<div style="text-align: center;"> | |
<img src="https://huggingface.co/spaces/maryamilka/surge-pricing/resolve/main/shariflogo.png" | |
alt="لوگو دانشگاه شریف" | |
style="width: 200px; height: auto; margin-bottom: 10px;"> | |
</div> | |
<div style="flex: 1;"> | |
<p>با سلام و احترام،</p> | |
<p>این پرسشنامه بخشی از یک پژوهش دانشگاهی است که در قالب پایاننامه کارشناسیارشد در دانشگاه صنعتی شریف انجام میشود. این تحقیق به <strong> بررسی ادراک مصرفکنندگان از انصاف در قیمتگذاریِ اپلیکیشنهای تاکسی اینترنتی (مانند اسنپ و تپسی 🚖) </strong> میپردازد.</p> | |
<p> پر کردن این پرسشنامه کمتر از 5 دقیقه وقت شما را میگیرد. شرکت در این مطالعه کاملاً داوطلبانه است؛ پاسخ درست یا غلطی برای سوالات وجود ندارد و نظرات شخصی شما است که ارزشمند است و برای پیشبرد اهداف علمی تحلیل خواهند شد. </p> | |
<p> پاسخهای شما کمک شایانی به ما، به عنوان یک تیم تحقیقاتی در دانشگاه صنعتی شریف، برای ارتقای دانش علمی خواهد کرد. پیشاپیش از مشارکت شما صمیمانه سپاسگزاریم 🙏</p> | |
<p>برای آغاز پرسشنامه، لطفاً روی دکمه زیر کلیک کنید 👇🏻</p> | |
</div> | |
</div> | |
""", unsafe_allow_html=True) | |
if st.button("شروع پرسشنامه", key="start_btn", type="primary"): | |
st.session_state.current_page = "scenario_explanation" | |
st.session_state.start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
st.rerun() | |
def scenario_explanation(): | |
"""توضیح سناریو""" | |
col1, col2 = st.columns([2, 4]) # افزایش نسبت ستون اول | |
with col1: | |
st.markdown('<div style="padding-right: 25px;">', unsafe_allow_html=True) | |
try: | |
st.image("rahyar.png", width=80) | |
except: | |
st.image("https://via.placeholder.com/80/6a0dad/FFFFFF?text=LOGO", width=80) | |
st.markdown('</div>', unsafe_allow_html=True) | |
with col2: | |
st.markdown(""" | |
<h2 class="rahyar-title">رهیار 🚖</h2> | |
<p class="rahyar-subtitle">همراه سفرهای درونشهری شما، راهی مطمئن، راهی روشن، رهیار</p> | |
""", unsafe_allow_html=True) | |
st.markdown("### سناریوی تحقیق") | |
st.markdown(""" | |
<p class="warning">در این بخش با یک سناریو فرضی مواجه خواهید شد. خواهشمندیم صفحه را به پایین بکشید، سناریو را به دقت مطالعه فرمایید، خودتان را در موقعیت تصور کنید و طبق آن پرسشنامه را ادامه دهید.</p> | |
""", unsafe_allow_html=True) | |
st.markdown(""" | |
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 10px;"> | |
<p>فرض کنید در روزی از روزها شما قصد دارید از محل زندگیتان در پونک شمالی به پاتوق همیشگیتان در اطراف پل طبیعت بروید.</p> | |
<p>گوشیتان را از کیف درمیآورید. اپلیکیشنی که معمولاً برای حمل و نقل استفاده میکنید را باز میکنید؛ اپلیکیشنی به نام <strong>رهیار</strong> — نه اسنپ است و نه تپسی، اما خیلی شبیه آنها و رقیب جدید آنهاست. رنگ بنفش جذابی هم دارد. تصمیم میگیرید این بار هم برای این سفر از این اپلیکیشن استفاده کنید.</p> | |
<p>شما همیشه و البته این بار نیز از حالت سفر معمولی رهیار استفاده میکنید.</p> | |
<p>از آنجایی که این مسیر را معمولاً با این اپلیکیشن طی میکنید، متوجه شدهاید که در شرایط معمول که ترافیک شدیدی وجود ندارد، زمان شلوغی در روز نیست، هوا مناسب است، رویداد خاصی نیست و.. قیمت این مسیر حدود 70 هزار تومان است. اما وقتی شرایط متفاوت است، رهیار با کمک هوش مصنوعی و تشخیص شرایط و شدت آنها، قیمتها را افزایش میدهد تا رانندگان انگیزه بیشتری برای قبول سفر داشته باشند و عرضه و تقاضا تعدیل شود.</p> | |
<p> الان که شما قصد این سفر را دارید، شرایط معمولی نیست. پس وقتی مبدأ و مقصد را انتخاب میکنید، با قیمت بیشتری مواجه میشوید.</p> | |
<p>با کلیک روی «ادامه»، اطلاعات سفر را مشاهده کنید 👇🏻</p> | |
</div> | |
""", unsafe_allow_html=True) | |
if st.button("ادامه", key="continue_btn", type="primary"): | |
st.session_state.current_page = "map_view" | |
st.rerun() | |
def map_view(): | |
col1, col2 = st.columns([2, 4]) # افزایش نسبت ستون اول | |
with col1: | |
st.markdown('<div style="padding-right: 25px;">', unsafe_allow_html=True) | |
try: | |
st.image("rahyar.png", width=80) | |
except: | |
st.image("https://via.placeholder.com/80/6a0dad/FFFFFF?text=LOGO", width=80) | |
st.markdown('</div>', unsafe_allow_html=True) | |
with col2: | |
st.markdown(""" | |
<h2 class="rahyar-title">رهیار 🚖</h2> | |
<p class="rahyar-subtitle">همراه سفرهای درونشهری شما، راهی مطمئن، راهی روشن، رهیار</p> | |
""", unsafe_allow_html=True) | |
st.markdown(""" | |
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 10px;"> | |
<p class="warning">مسیر سفر شما از پونک شمالی به پل طبیعت است. مسیری که در شرایط عادی 70 هزار تومان قیمت دارد. اما در شرایط خاص مانند ترافیک شدید، تایم شلوغی روز، شرایط خاص جوی و.. افزایش مییابد.</p> | |
<p class="warning"> لطفاً صفحه را به پایین بکشید. سپس با توجه به اطلاعاتی که بعد از نقشه دریافت میکنید، تصمیم بگیرید که سفر را میپذیرید یا رد میکنید.</p> | |
<p class="warning">سپس با کلیک بر دکمه مربوطه به بخش بعدی بروید.</p> | |
</div> | |
""", unsafe_allow_html=True) | |
st.markdown("### مسیر سفر شما") | |
folium_static(create_ride_map(), width=1000 if st.session_state.is_desktop else 800, | |
height=500 if st.session_state.is_desktop else 400) | |
# قیمت | |
st.markdown(f""" | |
<div class="price-container"> | |
<div style="display: flex; justify-content: space-between; align-items: center;"> | |
<span>رهیار <span style="background-color: #e6e6fa; color: #6a0dad; padding: 2px 8px; border-radius: 12px; font-size: 14px;">معمولی</span></span> | |
<span class="rahyar-price">{st.session_state.price:,} تومان</span> | |
</div> | |
</div> | |
""", unsafe_allow_html=True) | |
show_explanation(st.session_state.scenario_type) | |
# دکمهها | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("درخواست سفر", key="accept_btn", use_container_width=True): | |
st.session_state.price_accepted = 1 | |
st.session_state.current_page = "attention_check1" | |
st.rerun() | |
with col2: | |
if st.button("رد سفر", key="reject_btn", use_container_width=True): | |
st.session_state.price_accepted = 0 | |
st.session_state.current_page = "attention_check1" | |
st.rerun() | |
def attention_check1(): | |
"""سوال توجه اول با دکمه سبز کاملاً عملی""" | |
# 1. تزریق استایلهای سفارشی | |
st.markdown(""" | |
<style> | |
/* مخفی کردن دکمه پیشفرض Streamlit */ | |
div[data-testid="stButton"] > button[kind="primary"] { | |
display: none !important; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
st.markdown("### سؤال") | |
answer = st.radio( | |
"رنگ لوگو اپلیکیشن رهیار و دکمهها در صفحات قبلی چگونه بود؟", | |
["قرمز", "سبز", "بنفش", "آبی", "فراموش کردم"], | |
index=None, | |
key="att1_radio" | |
) | |
# کامپوننت HTML با دکمه سبز اصلی | |
st.components.v1.html(f""" | |
<script> | |
function handleClick() {{ | |
// فعال کردن دکمه مخفی Streamlit | |
parent.document.querySelector('div[data-testid="stButton"] > button[kind="primary"]').click(); | |
}} | |
</script> | |
<button onclick="handleClick()" | |
style=" | |
background-color: #28a745 !important; | |
color: white !important; | |
border: none !important; | |
border-radius: 8px !important; | |
padding: 10px 20px !important; | |
font-weight: bold !important; | |
font-family: 'Vazir', sans-serif !important; | |
cursor: pointer !important; | |
margin-left: auto; /* این خط برای راستچین کردن مهم است */ | |
transition: all 0.3s ease !important; | |
float: right; | |
margin-left: 15px; | |
" | |
onmouseover="this.style.backgroundColor='#218838'; this.style.boxShadow='0 4px 8px rgba(0,0,0,0.15)';" | |
onmouseout="this.style.backgroundColor='#28a745'; this.style.boxShadow='0 2px 5px rgba(0,0,0,0.1)';"> | |
ادامه | |
</button> | |
""", height=70) | |
# 3. منطق اصلی دکمه (مخفی) | |
if st.button("ادامه", key="att1_real_btn", type="primary"): | |
if answer: | |
st.session_state.attention_check1 = answer | |
st.session_state.current_page = "random_likert_questions" | |
st.rerun() | |
else: | |
st.warning("لطفاً یک گزینه را انتخاب کنید") | |
def random_likert_questions(): | |
"""نمایش سوالات لیکرت با دکمههای دایرهای""" | |
question_groups = [ | |
{ | |
"title": "سری اول سؤالات", | |
"key": "distributive", | |
"guide": textwrap.dedent(""" | |
<h4 class="warning">راهنمای پاسخ به سری اول:</h4> | |
<p class="little"> | |
در این بخش، با یک سری سؤال درمورد قیمتی که در صفحه اطلاعات سفر و در زیر نقشه دیدید، مواجه خواهید شد. در زیر سوالات طیفی قرار دارد: <br> | |
- سمت راست (۱): کاملاً نامنصفانه، غیرمعقول یا غیرقابل قبول<br> | |
- سمت چپ (۷): کاملاً منصفانه، معقول یا قابل قبول<br> | |
لطفاً با دقت عدد مناسب را بین ۱ تا ۷ را انتخاب نمایید. بدین گونه شما انتخاب خواهید کرد که چقدر قیمت به نظرتان منصفانه بوده. چقدر با توجه به شرایط منطقی بوده و چقدر قابل قبول بوده. | |
</p> | |
"""), | |
"questions": [ | |
{ | |
"key": "distributive_1", | |
"question": "قیمتی که به شما ارائه شد، چگونه بود؟", | |
"scale": 7, | |
"labels": ["کاملاً نامنصفانه", "کاملاً منصفانه"] | |
}, | |
{ | |
"key": "distributive_2", | |
"question": "قیمتی که به شما ارائه شد، چگونه بود؟", | |
"scale": 7, | |
"labels": ["کاملاً غیرمعقول", "کاملاً معقول"] | |
}, | |
{ | |
"key": "distributive_3", | |
"question": "قیمتی که به شما ارائه شد، چگونه بود؟", | |
"scale": 7, | |
"labels": ["کاملاً غیرقابل قبول", "کاملاً قابل قبول"] | |
} | |
] | |
}, | |
{ | |
"title": "لطفاً به این سوال پاسخ دهید.", | |
"key": "attention_check", | |
"questions": [ | |
{"key": "attention_check2", "question": "لطفاً خیلی زیاد (عدد 7) را انتخاب کنید.", "scale": 7, "labels": ["خیلی کم", "خیلی زیاد"]} | |
] | |
}, | |
{ | |
"title": "سری دوم سؤالات", | |
"key": "procedural", | |
"guide": textwrap.dedent(""" | |
<h4 class="warning">راهنمای پاسخ به سری دوم:</h4> | |
<p class="little"> | |
در این بخش با یک سری جمله خبری درمورد فرآیند و رویه قیمتگذاری رهیار مواجه خواهید شد. در زیر جملات یک طیف قرار دارد:<br> | |
- سمت راست (۱): کاملاً مخالفم<br> | |
- سمت چپ (۷): کاملاً موافقم<br> | |
لطفاً نظر خود را با انتخاب عدد مناسب بیان کنید. | |
</p> | |
"""), | |
"questions": [ | |
{"key": "procedural_1", "question": "فرآیند و رویه قیمتگذاری رهیار قابل قبول است.", "scale": 7, "labels": ["کاملاً مخالفم", "کاملاً موافقم"]}, | |
{"key": "procedural_2", "question": "فرآیند و رویه قیمتگذاری رهیار منصفانه است.", "scale": 7, "labels": ["کاملاً مخالفم", "کاملاً موافقم"]}, | |
{"key": "procedural_3", "question": "فرآیند و رویه قیمتگذاری رهیار معقول است.", "scale": 7, "labels": ["کاملاً مخالفم", "کاملاً موافقم"]} | |
] | |
}, | |
{ | |
"title": "سری سوم سؤالات", | |
"key": "informational", | |
"guide": textwrap.dedent(""" | |
<h4 class="warning">راهنمای پاسخ به سری سوم:</h4> | |
<p class="little"> | |
در این بخش، با یک سری سؤال درمورد توضیحاتی که در صفحه اطلاعات سفر و در زیر نقشه درمورد قیمت به شما ارائه شد، مواجه خواهید شد. در زیر سوالات طیفی قرار دارد: <br> | |
- سمت راست (۱): به هیچ وجه<br> | |
- سمت چپ (۷): خیلی زیاد<br> | |
لطفاً با دقت عدد مناسب را بین ۱ تا ۷ را انتخاب نمایید. بدین گونه شما انتخاب خواهید کرد که از هیچ مقدار تا خیلی زیاد به چه مقدار به شما توضیح با ویژگیهای سوال ارائه شده است. | |
</p> | |
"""), | |
"questions": [ | |
{"key": "informational_1", "question": "تا چه حد رهیار دلایل تعیین قیمت را به صورت صادقانه توضیح داد؟", "scale": 7, "labels": ["به هیچ وجه", "خیلی زیاد"]}, | |
{"key": "informational_2", "question": "تا چه حد رهیار عوامل مؤثر بر تعیین قیمت را به طور کامل شرح داد؟", "scale": 7, "labels": ["به هیچ وجه", "خیلی زیاد"]}, | |
{"key": "informational_3", "question": "تا چه حد دلایل ارائهشده توسط رهیار برای تعیین قیمت منطقی و قابل قبول بود؟", "scale": 7, "labels": ["به هیچ وجه", "خیلی زیاد"]}, | |
{"key": "informational_4", "question": "تا چه حد توضیحات درباره تعیین قیمت بلافاصله و در زمان مناسب نمایش داده شد؟", "scale": 7, "labels": ["به هیچ وجه", "خیلی زیاد"]}, | |
{"key": "informational_5", "question": "تا چه حد توضیحات رهیار درباره تعیین قیمت، متناسب با شرایط سفر شما(مثلاً ترافیک، ساعت روز و..) بود؟", "scale": 7, "labels": ["به هیچ وجه", "خیلی زیاد"]} | |
] | |
} | |
] | |
# مقداردهی اولیه | |
if 'current_likert_group' not in st.session_state: | |
st.session_state.current_likert_group = 0 | |
st.session_state.current_question_index = 0 | |
st.session_state.show_guide = True | |
current_group = question_groups[st.session_state.current_likert_group] | |
current_question = current_group['questions'][st.session_state.current_question_index] | |
# نمایش راهنما فقط برای اولین سوال هر گروه | |
if st.session_state.show_guide and 'guide' in current_group: | |
st.markdown(f"## {current_group['title']}") | |
guide_html = textwrap.dedent(""" | |
<div class="guide-text" style=" | |
display: flex; | |
flex-direction: column; | |
background-color: #f0f2f6; | |
border-radius: 10px; | |
padding: 15px; | |
gap: 10px; | |
direction: rtl; | |
text-align: justify; | |
"> | |
{} | |
</div> | |
""").format(current_group['guide']) | |
st.markdown(guide_html, unsafe_allow_html=True) | |
else: | |
st.markdown(f"## {current_group['title']}") | |
# نمایش سوال جاری | |
answer = enhanced_likert_scale(current_question) | |
# اگر پاسخ داده شد، به سوال بعدی برو | |
if answer is not None: | |
st.session_state.answers[current_question["key"]] = answer | |
st.session_state.show_guide = False | |
# تاخیر برای نمایش پاسخ قبل از رفتن به سوال بعدی | |
time.sleep(0.5) | |
# بررسی آیا سوالات این گروه تمام شده یا نه | |
if st.session_state.current_question_index < len(current_group['questions']) - 1: | |
st.session_state.current_question_index += 1 | |
else: | |
# رفتن به گروه بعدی | |
st.session_state.current_likert_group += 1 | |
st.session_state.current_question_index = 0 | |
st.session_state.show_guide = True | |
# اگر همه گروهها تمام شدند، به صفحه بعدی برو | |
if st.session_state.current_likert_group >= len(question_groups): | |
st.session_state.current_page = "explanation_questions" | |
st.rerun() | |
def explanation_questions(): | |
"""نمایش سوالات تکمیلی به صورت مرحلهای با دکمه ادامه""" | |
st.markdown("### 📋 سؤالات تکمیلی") | |
# لیست سوالات به ترتیب نمایش | |
questions = [ | |
{ | |
"key": "price_increase", | |
"label": "در سناریوی کیس رهیار، آیا به نظر شما قیمت ارائهشده برای مسیر موردنظر، در مقایسه با قیمت در شرایط معمولی روز (ترافیک عادی، تایم غیر شلوغی، عرضه و تقاضا مناسب، روز غیرخاص و..)، افزایش داشته است یا خیر؟ ", | |
"options": ["بله", "خیر", "مطمئن نیستم"], | |
"required": True | |
}, | |
{ | |
"key": "increase_amount", | |
"label": "اگر به نظرتان افزایش قیمتی وجود داشته؛ این افزایش قیمتی چه مقدار بوده؟", | |
"options": ["زیاد","متوسط","کم", "تفاوت قیمتی وجود نداشت"], | |
"required": True | |
}, | |
{ | |
"key": "pricing_method", | |
"label": "به نظر شما رهیار چگونه قیمتها را بالا/پایین میکند؟", | |
"options": [ | |
"به صورت دستی توسط تیم رهیار", | |
"به صورت خودکار توسط هوش مصنوعی و الگوریتمها", | |
"ترکیبی از هر دو روش", | |
"نظری ندارم" | |
], | |
"required": True | |
}, | |
{ | |
"key": "trust", | |
"label": "در زندگی روزمره ممکن است با تصمیمات هوش مصنوعی مواجه شوید، مانند پیشنهاد مسیر برای اجتناب از ترافیک، ارائه دستور پخت بر اساس مواد غذایی، توصیه پوشش یا نکات ارائه برای جلسات کاری، پیشنهاد سهام برای سرمایهگذاری، یا معرفی افراد در برنامههای دوستیابی. فارغ از تصمیمات حیاتی (مثل تشخیص پزشکی یا احکام قضایی)، آیا به تصمیمگیریهای هوش مصنوعی در این موارد روزمره اعتماد دارید؟", | |
"options": ["بله", "تا حدودی", "خیر", "نظری ندارم"], | |
"required": True | |
}, | |
{ | |
"key": "explanation_received", | |
"label": "در صفحهای که اطلاعات سفر را دریافت کردید، زیر بخش قیمت، آیا رهیار توضیحی در مورد علت قیمتگذاری به شما ارائه داد؟", | |
"options": ["بله", "خیر"], | |
"required": True | |
}, | |
{ | |
"key": "explanation_type", | |
"label": "اگر توضیحی دریافت کردید، این توضیح بیشتر به کدام مورد شباهت داشت؟", | |
"options": [ | |
"به من صرفاً عوامل مؤثر در تعیین قیمت و شدت تأثیر آنها نمایش داده شد.", | |
"به من اطلاع دادند که در صورت انتظار و درخواست سفر در شرایط متفاوتِ زمانی دیگر، ممکن است قیمت دیگری دریافت کنم", | |
"توضیحی دریافت نکردم" | |
], | |
"required": False, | |
"condition": lambda: st.session_state.get("explanation_received") == "بله" | |
} | |
] | |
# مقداردهی اولیه step اگر وجود ندارد | |
if "explanation_step" not in st.session_state: | |
st.session_state.explanation_step = 0 | |
# اگر همه سوالات پاسخ داده شدهاند، به صفحه بعدی برو | |
if st.session_state.explanation_step >= len(questions): | |
st.session_state.current_page = "demographic" | |
st.rerun() | |
return | |
# دریافت سوال جاری | |
current_q = questions[st.session_state.explanation_step] | |
# بررسی شرط نمایش برای سوالات اختیاری | |
if "condition" in current_q and not current_q["condition"](): | |
st.session_state[current_q["key"]] = "N/A" | |
st.session_state.explanation_step += 1 | |
st.rerun() | |
return | |
# نمایش سوال جاری | |
answer = st.radio( | |
current_q["label"], | |
current_q["options"], | |
index=None, | |
key=f"explanation_q_{current_q['key']}" | |
) | |
if current_q["key"] == "explanation_received": | |
with st.expander("نمونه توضیحات قیمتگذاری", expanded=False): | |
# ایجاد یک کانتینر برای وسط چین کردن | |
col1, col2, col3 = st.columns([1, 6, 1]) | |
with col2: | |
try: | |
st.image("control.png", | |
width=400, # اندازه بزرگتر | |
caption="نمونه بدون توضیح", | |
use_container_width=True) # پارامتر جدید جایگزین | |
st.image("input.png", | |
width=400, | |
caption="نمونه توضیح", | |
use_container_width=True) | |
st.image("counterfactual.png", | |
width=400, | |
caption="نمونه توضیح", | |
use_container_width=True) | |
except Exception as e: | |
st.warning(f"تصاویر نمونه یافت نشدند. خطا: {str(e)}") | |
# دکمه ادامه | |
if st.button("ادامه", key=f"continue_{current_q['key']}"): | |
if answer is None and current_q["required"]: | |
st.warning("لطفاً یک گزینه را انتخاب کنید") | |
else: | |
# ذخیره پاسخ | |
st.session_state[current_q["key"]] = answer if answer is not None else "N/A" | |
# افزایش شماره مرحله | |
st.session_state.explanation_step += 1 | |
# رفرش صفحه برای نمایش سوال بعدی | |
st.rerun() | |
def demographic_form(): | |
"""فرم اطلاعات دموگرافیک""" | |
st.markdown("### 📝 اطلاعات دموگرافیک") | |
st.markdown(""" | |
<div> | |
<p>لطفاً اطلاعات زیر را صادقانه وارد نمایید.</p> | |
<p>لطفاً اگر با گوشی موبایل به سؤالات پاسخ میدهید، در جعبه مربوط به سن، عدد انگلیسی وارد بفرمایید.</p> | |
</div> | |
""", unsafe_allow_html=True) | |
with st.form("demographic_form"): | |
age = st.number_input("سن", min_value=18, max_value=100, value=None, placeholder="سن خود را وارد کنید") | |
gender = st.selectbox("جنسیت", ["", "مرد", "زن", "سایر"], index=0) | |
education = st.selectbox("تحصیلات", ["", "دیپلم", "لیسانس", "فوق لیسانس", "دکترا"], index=0) | |
city = st.selectbox("لطفاً استان محل سکونت خود را انتخاب بفرمایید.", | |
["", "آذربایجان شرقی", "آذربایجان غربی", "اردبیل", "اصفهان", "البرز", "ایلام", | |
"بوشهر", "تهران", "چهارمحال و بختیاری", "خراسان جنوبی", "خراسان رضوی", "خراسان شمالی", | |
"خوزستان", "زنجان", "سمنان", "سیستان و بلوچستان", "فارس", "قزوین", "قم", "کردستان", | |
"کرمان", "کرمانشاه", "کهگیلویه و بویراحمد", "گلستان", "گیلان", "لرستان", "مازندران", | |
"مرکزی", "هرمزگان", "همدان", "یزد"], index=0) | |
related_education_job = st.selectbox("رشته تحصیلی/شغل شما در کدامیک از دستههای زیر قرار دارد؟", | |
["", "مهندسی", "درمانی", "فرهنگی", "مدیریتی (مالی)", | |
"مدیریتی (بازاریابی)", "مدیریتی (سایر)", "روانشناسی", | |
"اقتصادی", "حقوقی", "هنری", "ورزشی", "زبان", "غیره"], index=0) | |
ride_frequency = st.selectbox("معمولاً در ماه چه تعداد بار از اپلیکیشنهای اسنپ و تپسی استفاده میکنید؟", | |
["", "هیچوقت", "کمتر از 5 بار", "5-10 بار", "بیش از 10 بار"], index=0) | |
submitted = st.form_submit_button("ادامه") | |
if submitted: | |
if not all([age, gender, education, city, related_education_job, ride_frequency]): | |
st.error("لطفاً تمام فیلدها را پر کنید") | |
else: | |
st.session_state.demographic_data = { | |
"age": age, | |
"gender": gender, | |
"education": education, | |
"city": city, | |
"ride_frequency": ride_frequency, | |
"related_education_job": related_education_job | |
} | |
st.session_state.current_page = "contact" | |
st.rerun() | |
def user_contact(): | |
"""راه ارتباطی ساده""" | |
st.markdown(""" | |
<div style="text-align: center; margin-bottom: 30px;"> | |
<h3>📩 راه ارتباطی شما (اختیاری)</h3> | |
<p>جهت قدردانی از شما بابت زمانی که به پر کردن این پرسشنامه اختصاص دادید، به دو نفر از عزیزان به قید قرعه جایزه نقدی 5 میلیون ریالی تقدیم خواهد شد.</p> | |
<p>در صورت تمایل به شرکت در قرعهکشی میتوانید آیدی تلگرام، شماره تماس یا ایمیل خود را وارد کنید.</p> | |
<p>این اطلاعات کاملاً محرمانه نزد محقق خواهد ماند و صرفاً جهت قرعهکشی استفاده خواهد شد.</p> | |
<p>درصورتی که تمایل ندارید این فیلد را پر کنید لطفاً کلیک بر دکمه ثبت پاسخ را فراموش نکنید.</p> | |
</div> | |
""", unsafe_allow_html=True) | |
contact_info = st.text_input( | |
"راه ارتباطی (اختیاری)", | |
placeholder="مثال: @username یا 09123456789 یا example@email.com", | |
key="user_contact_input" | |
) | |
if st.button("ثبت پاسخها", type="primary", key="submit_explanation"): | |
st.session_state.user_contact = contact_info | |
end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
start_time = datetime.strptime(st.session_state.start_time, "%Y-%m-%d %H:%M:%S") | |
completion_time = (datetime.now() - start_time).total_seconds() | |
save_data = { | |
"start_time": st.session_state.start_time, | |
"end_time": end_time, | |
"completion_time": completion_time, | |
"scenario_type": st.session_state.scenario_type, | |
"price": st.session_state.price, | |
"user_contact": st.session_state.get("user_contact", ""), | |
"price_accepted": st.session_state.get("price_accepted", 0), | |
"attention_check1": st.session_state.get("attention_check1", None), | |
"trust": st.session_state.trust, | |
"pricing_method": st.session_state.pricing_method, | |
"price_increase": st.session_state.price_increase, | |
"increase_amount": st.session_state.increase_amount, | |
"explanation_received": st.session_state.explanation_received, | |
"explanation_type": st.session_state.get("explanation_type", "N/A"), | |
**st.session_state.demographic_data, | |
**st.session_state.answers # اضافه کردن تمام پاسخهای لیکرت | |
} | |
if save_to_sheet(save_data): | |
st.session_state.current_page = "thank_you" | |
st.rerun() | |
else: | |
st.error("خطا در ذخیرهسازی دادهها. لطفاً دوباره تلاش کنید.") | |
def thank_you_page(): | |
"""صفحه تشکر""" | |
st.success(""" | |
✅ پاسخهای شما با موفقیت ثبت شد. | |
سپاسگزاریم که وقت ارزشمند خود را به این پژوهش اختصاص دادید. | |
در صورت وجود هرگونه سوال، ابهام یا پیشنهاد میتوانید با محقق تماس بگیرید: | |
✉ایمیل: maryam.ilka2000@gmail.com | |
""") | |
st.balloons() | |
# ========== مدیریت وضعیت و صفحهبندی ========== | |
def main(): | |
# تشخیص دستگاه | |
user_agent = st.query_params.get("user_agent", [""])[0] | |
st.session_state.is_desktop = "mobile" not in user_agent.lower() | |
if st.session_state.is_desktop: | |
# اطمینان از نمایش همان حالت موبایل برای همه دستگاهها | |
st.session_state.is_desktop = False | |
if 'answers' not in st.session_state: | |
st.session_state.answers = {} | |
if 'current_page' not in st.session_state: | |
st.session_state.current_page = "welcome" | |
# ایجاد 4 شرط مختلف | |
conditions = [ | |
{"price": 120000, "scenario_type": "input"}, | |
{"price": 120000, "scenario_type": "counterfactual"}, | |
{"price": 200000, "scenario_type": "input"}, | |
{"price": 200000, "scenario_type": "counterfactual"} | |
] | |
# انتخاب تصادفی یکی از شرایط | |
selected_condition = random.choice(conditions) | |
st.session_state.price = selected_condition["price"] | |
st.session_state.scenario_type = selected_condition["scenario_type"] | |
st.session_state.user_contact = None | |
st.session_state.demographic_data = None | |
st.session_state.price_accepted = 0 | |
st.session_state.attention_check1 = None | |
pages = { | |
"welcome": welcome_page, | |
"scenario_explanation": scenario_explanation, | |
"map_view": map_view, | |
"attention_check1": attention_check1, | |
"random_likert_questions": random_likert_questions, | |
"explanation_questions": explanation_questions, | |
"demographic": demographic_form, # دموگرافیک قبل از کانتکت | |
"contact": user_contact, # کانتکت در انتها | |
"thank_you": thank_you_page | |
} | |
pages[st.session_state.current_page]() | |
if __name__ == "__main__": | |
main() |