Spaces:
Sleeping
Sleeping
| # app.py | |
| import streamlit as st | |
| import pandas as pd | |
| import logic | |
| import os | |
| from datetime import datetime, timedelta, timezone | |
| from wordcloud import WordCloud | |
| import matplotlib.pyplot as plt | |
| # [NEW] 3D μκ°νλ₯Ό μν λΌμ΄λΈλ¬λ¦¬ | |
| import plotly.express as px | |
| from sklearn.decomposition import PCA | |
| import numpy as np | |
| # ------------------------------------------------------------------------- | |
| # 1. νμ΄μ§ κΈ°λ³Έ μ€μ & μΈμ μν μ΄κΈ°ν | |
| # ------------------------------------------------------------------------- | |
| st.set_page_config(page_title="AI νμ μ¬λ£ μΆμ²", layout="wide") | |
| st.title("π³ AI μμ¬λ£ λ체 μΆμ² λμ보λ") | |
| # [NEW] ν¬νΈν΄λ¦¬μ€ μ€νμΌ μ μ© (Nanum Pen Script & Theme) | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap'); | |
| html, body, [class*="css"] { | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| font-size: 1.25rem; | |
| } | |
| /* μ λͺ© ν°νΈ ν¬κΈ° ν€μ°κΈ° */ | |
| h1 { font-size: 3.5rem !important; color: #1e293b !important; } | |
| h2 { font-size: 2.8rem !important; color: #334155 !important; } | |
| h3 { font-size: 2.2rem !important; color: #475569 !important; } | |
| /* λ°°κ²½ κ·ΈλΌλ°μ΄μ (ν¬νΈν΄λ¦¬μ€μ μ μ¬νκ²) */ | |
| .stApp { | |
| background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important; | |
| } | |
| /* μ€λͺ λ°μ€ μ€νμΌ */ | |
| div[data-testid="stMarkdownContainer"] > div { | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| } | |
| /* λ²νΌ μ€νμΌ */ | |
| .stButton > button { | |
| background: #3b82f6 !important; | |
| color: white !important; | |
| border-radius: 12px !important; | |
| border: none !important; | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| font-size: 1.4rem !important; | |
| padding: 0.5rem 1rem !important; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| transition: all 0.2s; | |
| } | |
| .stButton > button:hover { | |
| transform: scale(1.05); | |
| opacity: 0.9; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| } | |
| /* μ λ ₯ νλ μ€νμΌ */ | |
| .stTextInput > div > div > input, .stTextArea > div > div > textarea { | |
| background-color: rgba(255, 255, 255, 0.9) !important; | |
| border-radius: 10px !important; | |
| border: 1px solid #cbd5e1 !important; | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| font-size: 1.2rem !important; | |
| color: #1e293b !important; | |
| } | |
| /* Expander μ€νμΌ */ | |
| .streamlit-expanderHeader { | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| font-size: 1.3rem !important; | |
| background-color: rgba(255, 255, 255, 0.5) !important; | |
| border-radius: 8px !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| if 'voted_logs' not in st.session_state: st.session_state['voted_logs'] = set() | |
| if "stopword_input_field" not in st.session_state: st.session_state["stopword_input_field"] = "" | |
| if "board_nick_input" not in st.session_state: st.session_state["board_nick_input"] = "" | |
| if "board_msg_input" not in st.session_state: st.session_state["board_msg_input"] = "" | |
| if "feedback_input_field" not in st.session_state: st.session_state["feedback_input_field"] = "" | |
| # ------------------------------------------------------------------------- | |
| # 2. ν¬νΌ ν¨μ λ° λ€μ΄μΌλ‘κ·Έ | |
| # ------------------------------------------------------------------------- | |
| def format_saving(score, is_multi=False): | |
| prefix = "μ΄ " if is_multi else "" | |
| if score > 0: return f"π’ {prefix}+{score}λ¨κ³ (μ κ°)" | |
| elif score < 0: return f"π΄ {prefix}{score}λ¨κ³ (λΉμ)" | |
| else: return "βͺ λμΌ μμ€" | |
| def show_logic_dialog(): | |
| if os.path.exists("flowchart.png"): | |
| st.image("flowchart.png", use_container_width=True) | |
| try: | |
| with open("docs/logic_explanation.md", "r", encoding="utf-8") as f: | |
| markdown_text = f.read() | |
| st.markdown("---") | |
| st.markdown(markdown_text) | |
| except: | |
| st.error("μ€λͺ νμΌμ μ°Ύμ μ μμ΅λλ€.") | |
| def show_wordcloud_dialog(timeframe_text, text_data): | |
| st.subheader(f"{timeframe_text} λ§μ΄ κ²μλ νκ² μ¬λ£") | |
| if not text_data: | |
| st.info("λ°μ΄ν°κ° μΆ©λΆνμ§ μμ΅λλ€.") | |
| return | |
| font_path = "src/font.ttf" if os.path.exists("src/font.ttf") else None | |
| try: | |
| wordcloud = WordCloud(font_path=font_path, width=800, height=400, background_color='white', colormap='viridis', random_state=42).generate(text_data) | |
| fig, ax = plt.subplots(figsize=(10, 5)) | |
| ax.imshow(wordcloud, interpolation='bilinear'); ax.axis('off') | |
| st.pyplot(fig) | |
| if not font_path: st.caption("β οΈ νκΈ ν°νΈ νμΌμ΄ μμ΄ κΈμκ° κΉ¨μ§ μ μμ΅λλ€.") | |
| except Exception as e: st.error(f"μ€λ₯ λ°μ: {e}") | |
| # [NEW] 3D λ²‘ν° κ³΅κ° μκ°ν νμ | |
| def show_3d_space_dialog(): | |
| st.caption("AIκ° νμ΅ν μ¬λ£λ€μ κ΄κ³λ₯Ό 3μ°¨μ 곡κ°μμ νμΈν΄λ³΄μΈμ. (μμ 300κ° μ¬λ£)") | |
| try: | |
| # logic.pyμμ λ‘λλ Word2Vec λͺ¨λΈ κ°μ Έμ€κΈ° | |
| model = logic.w2v_model | |
| # λΉλμ μμ 300κ° λ¨μ΄ μΆμΆ | |
| words = model.wv.index_to_key[:300] | |
| vectors = np.array([model.wv[word] for word in words]) | |
| # PCAλ‘ 100μ°¨μ -> 3μ°¨μ μΆμ | |
| pca = PCA(n_components=3) | |
| projections = pca.fit_transform(vectors) | |
| # λ°μ΄ν°νλ μ μμ± | |
| df_vis = pd.DataFrame(projections, columns=['x', 'y', 'z']) | |
| df_vis['word'] = words | |
| # Plotly 3D μ°μ λ 그리기 | |
| fig = px.scatter_3d( | |
| df_vis, x='x', y='y', z='z', | |
| text='word', | |
| hover_name='word', | |
| color='z', # λμ΄μ λ°λΌ μμ λ³ν | |
| color_continuous_scale='Viridis' | |
| ) | |
| fig.update_traces( | |
| marker=dict(size=4, opacity=0.8), | |
| textposition='top center', | |
| textfont=dict(size=10, color='black') # ν μ€νΈ μ€νμΌ | |
| ) | |
| fig.update_layout( | |
| height=600, | |
| scene=dict( | |
| xaxis=dict(showticklabels=False, title=''), | |
| yaxis=dict(showticklabels=False, title=''), | |
| zaxis=dict(showticklabels=False, title='') | |
| ), | |
| margin=dict(l=0, r=0, b=0, t=0) | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| st.info("π‘ **ν:** λ§μ°μ€λ‘ λλκ·Ένμ¬ νμ νκ±°λ ν λ‘ νλ/μΆμν μ μμ΅λλ€. κ°κΉμ΄ μλ μ¬λ£λ€μ AIκ° 'λΉμ·ν μ±μ§'λ‘ μΈμν κ²μ λλ€.") | |
| except Exception as e: | |
| st.error(f"μκ°ν μμ± μ€ν¨: {e}") | |
| # [CALLBACK] ν¨μλ€ | |
| def handle_board_submission(): | |
| nick = st.session_state.get("board_nick_input", "") | |
| msg = st.session_state.get("board_msg_input", "") | |
| if nick and msg: | |
| if logic.save_board_message(nick, msg): | |
| st.toast("κ²μκΈμ΄ λ±λ‘λμμ΅λλ€!", icon="β ") | |
| st.session_state["board_nick_input"] = "" | |
| st.session_state["board_msg_input"] = "" | |
| else: st.toast("κ²μκΈ λ±λ‘μ μ€ν¨νμ΅λλ€.", icon="β") | |
| else: st.toast("λλ€μκ³Ό λ΄μ©μ λͺ¨λ μ λ ₯ν΄μ£ΌμΈμ.", icon="β οΈ") | |
| def handle_stopword_submission(): | |
| current_input = st.session_state.get("stopword_input_field", "") | |
| if current_input: | |
| is_success, msg = logic.save_stopwords_to_db(current_input) | |
| if is_success: | |
| st.toast(msg, icon="β ") | |
| st.session_state["stopword_input_field"] = "" | |
| else: st.toast(msg, icon="β") | |
| else: st.toast("λ¨μ΄λ₯Ό μ λ ₯ν΄μ£ΌμΈμ.", icon="β οΈ") | |
| def handle_feedback_submission(): | |
| content = st.session_state.get("feedback_input_field", "") | |
| if content: | |
| if logic.save_feedback_to_db(content): | |
| st.toast("μ견 κ°μ¬ν©λλ€!", icon="β ") | |
| st.balloons() | |
| st.session_state["feedback_input_field"] = "" | |
| else: st.toast("μ μ‘ μ€ν¨", icon="β") | |
| else: st.toast("λ΄μ©μ μ λ ₯ν΄μ£ΌμΈμ.", icon="β οΈ") | |
| # ------------------------------------------------------------------------- | |
| # 3. μ¬μ΄λλ° UI | |
| # ------------------------------------------------------------------------- | |
| with st.sidebar: | |
| st.header("ποΈ μ»¨νΈλ‘€ ν¨λ") | |
| selected_mode = st.radio("λͺ¨λ μ ν", ["π Ver.1 κΈ°μ‘΄ λ μνΌ DB κ²μ", "β¨ Ver.2 λλ§μ μ¬λ£ μ λ ₯ (컀μ€ν )"], index=0) | |
| st.divider() | |
| st.subheader("βοΈ κ°μ€μΉ μ€μ ") | |
| is_v1 = selected_mode == "π Ver.1 κΈ°μ‘΄ λ μνΌ DB κ²μ" | |
| w_w2v = st.slider("λ§Β·μ±μ§ (Word2Vec)", 0.0, 5.0, 5.0, 0.5) | |
| w_d2v = st.slider("λ¬Έλ§₯ (Doc2Vec)", 0.0, 5.0, 1.0, 0.5) | |
| w_method = st.slider("μ‘°λ¦¬λ² ν΅κ³ (Ver.1 μ μ©)", 0.0, 5.0, 1.0, 0.5, disabled=not is_v1) | |
| w_cat = st.slider("μΉ΄ν κ³ λ¦¬ ν΅κ³ (Ver.1 μ μ©)", 0.0, 5.0, 1.0, 0.5, disabled=not is_v1) | |
| if not is_v1: st.caption("π‘ 컀μ€ν λͺ¨λμμλ ν΅κ³ κ°μ€μΉκ° μ μ©λμ§ μμ΅λλ€.") | |
| excluded_ingredients = [] | |
| if not is_v1: | |
| st.divider() | |
| st.subheader("π« μ μΈν μ¬λ£ μ€μ ") | |
| all_ing_options = sorted(list(logic.all_ingredients_set)) | |
| excluded_ingredients = st.multiselect("μ μΈν μ¬λ£ μ ν", all_ing_options, placeholder="μ: λ 콩, μ€μ΄") | |
| st.divider() | |
| # [NEW] 3D μκ°ν λ²νΌ μΆκ° | |
| if st.button("π μ¬λ£ μ°μ£Ό(3D) νννκΈ°", use_container_width=True): | |
| show_3d_space_dialog() | |
| if st.button("π€ μ΄λ€ κ³Όμ μ κ±°μ³ μ¬λ£κ° μΆμ²λλμ?", use_container_width=True): | |
| show_logic_dialog() | |
| st.divider() | |
| st.subheader("π μΈμ¬μ΄νΈ λμ보λ (Beta)") | |
| kst = timezone(timedelta(hours=9)) | |
| today_date_string = datetime.now(kst).strftime("%Yλ %mμ %dμΌ") | |
| stopwords_list = logic.load_global_stopwords() | |
| tab_today, tab_all = st.tabs(["π μ€λ", "π λμ "]) | |
| wc_text_today = logic.get_wordcloud_text('today') | |
| wc_text_all = logic.get_wordcloud_text('all') | |
| today_count, today_dishes, today_targets = logic.get_usage_stats(timeframe='today') | |
| all_count, all_dishes, all_targets = logic.get_usage_stats(timeframe='all') | |
| with tab_today: | |
| st.caption(f"κΈ°μ€μΌ: {today_date_string} (KST)") | |
| col_m1, col_m2 = st.columns(2) | |
| col_m1.metric("μ€λ μ¬μ©λ", f"{today_count}건") | |
| col_m2.metric("λμ λΆμ©μ΄", f"{len(stopwords_list)}κ°") | |
| if today_count > 0: | |
| if st.button("βοΈ μ€λμ μλν΄λΌμ°λ", key="btn_wc_today", use_container_width=True): | |
| show_wordcloud_dialog("μ€λ", wc_text_today) | |
| st.caption("π₯ μ€λ λ§μ΄ λ체λ μ¬λ£") | |
| if not today_targets.empty: st.bar_chart(today_targets, color="#FF6B6B", height=200) | |
| else: st.info("μ€λμ λ°μ΄ν°κ° μμ΅λλ€.") | |
| with tab_all: | |
| st.caption("μλΉμ€ μμ μ΄ν μ 체 λ°μ΄ν°") | |
| col_a1, col_a2 = st.columns(2) | |
| col_a1.metric("μ΄ μ¬μ©λ", f"{all_count}건") | |
| col_a2.metric("λμ λΆμ©μ΄", f"{len(stopwords_list)}κ°") | |
| if all_count > 0: | |
| if st.button("βοΈ λμ μλν΄λΌμ°λ", key="btn_wc_all", use_container_width=True): | |
| show_wordcloud_dialog("λμ ", wc_text_all) | |
| st.caption("π₯ μλ λ§μ΄ λ체λ μ¬λ£") | |
| if not all_targets.empty: st.bar_chart(all_targets, color="#FF6B6B", height=200) | |
| else: st.info("λμ λ°μ΄ν°κ° μμ΅λλ€.") | |
| with st.expander("π μ κ³ λ λΆμ©μ΄ λͺ©λ‘ 보기"): | |
| if stopwords_list: st.dataframe(pd.DataFrame(stopwords_list, columns=["λΆμ©μ΄"]), use_container_width=True, hide_index=True) | |
| else: st.info("μ κ³ λ λΆμ©μ΄κ° μμ΅λλ€.") | |
| st.divider() | |
| with st.expander("π¬ μ΅λͺ κ²μν (Beta)", expanded=True): | |
| with st.form("board_form"): | |
| st.text_input("λλ€μ", placeholder="μ΅λͺ ", key="board_nick_input") | |
| st.text_area("λ΄μ©", placeholder="μμ λ‘κ² μ견μ λ¨κ²¨μ£ΌμΈμ", height=80, key="board_msg_input") | |
| st.form_submit_button("λ±λ‘", on_click=handle_board_submission) | |
| st.markdown("---") | |
| messages = logic.get_board_messages() | |
| if messages: | |
| for m in messages: | |
| st.markdown(f"**{m['nickname']}** <span style='color:grey; font-size:0.8em;'>({m['display_time']})</span>", unsafe_allow_html=True) | |
| st.text(m['content']) | |
| st.divider() | |
| else: st.caption("첫 λ²μ§Έ κΈμ λ¨κ²¨λ³΄μΈμ!") | |
| # ------------------------------------------------------------------------- | |
| # 4. λ©μΈ UI (κΈ°μ‘΄κ³Ό λμΌ) | |
| # ------------------------------------------------------------------------- | |
| col_main, _ = st.columns([0.9, 0.1]) | |
| with col_main: | |
| if selected_mode == "π Ver.1 κΈ°μ‘΄ λ μνΌ DB κ²μ": | |
| st.markdown("""<div style="background-color: #f0f8ff; padding: 15px; border-radius: 10px; margin-bottom: 20px;"><h4 style="margin:0; color:#0066cc;">[Ver.1] λ μνΌ λ°μ΄ν°λ² μ΄μ€μμ κ²μ</h4><p style="margin:5px 0 0 0; font-size:14px;">νμ΅λ 12λ§μ¬ κ°μ λ μνΌ μ€ νλλ₯Ό μ ννμ¬ λΆμν©λλ€. λͺ¨λ ν΅κ³ μ μκ° νμ©λ©λλ€.</p></div>""", unsafe_allow_html=True) | |
| search_keyword = st.text_input("π½οΈ μ리λͺ κ²μ (ν€μλ μ λ ₯ ν μν°)", placeholder="μ: λμ₯μ°κ°") | |
| final_dish_name = None | |
| if search_keyword: | |
| exact_match = logic.df[logic.df['μ리λͺ '] == search_keyword] | |
| exact_name = exact_match['μ리λͺ '].iloc[0] if not exact_match.empty else None | |
| candidates = logic.df[logic.df['μ리λͺ '].str.contains(search_keyword, na=False, case=False)] | |
| if exact_name: candidates = candidates[candidates['μ리λͺ '] != exact_name] | |
| candidate_names = sorted(candidates['μ리λͺ '].unique().tolist())[:30] | |
| options = [] | |
| if exact_name: options.append(exact_name) | |
| options.extend(candidate_names) | |
| if not options: st.warning(f"π '{search_keyword}'κ° ν¬ν¨λ μ리λͺ μ μ°Ύμ μ μμ΅λλ€.") | |
| else: | |
| index_to_select = 0 if exact_name else None | |
| label_msg = f"π '{search_keyword}' κ²μ κ²°κ³Ό ({len(options)}κ°)" | |
| if exact_name: label_msg += " - μ νν μ리λͺ μ΄ λ°κ²¬λμμ΅λλ€!" | |
| selected_option = st.selectbox(label_msg, options, index=index_to_select) | |
| final_dish_name = selected_option | |
| if final_dish_name: | |
| st.success(f"β μ νλ μ리: **{final_dish_name}**") | |
| cands = logic.df[logic.df['μ리λͺ '] == final_dish_name] | |
| cands = cands.head(10).reset_index(drop=True) | |
| if cands.empty: st.error("β λ μνΌ μ 보λ₯Ό λΆλ¬μ¬ μ μμ΅λλ€.") | |
| else: | |
| st.divider() | |
| options = {} | |
| for _, r in cands.iterrows(): | |
| preview = ', '.join(r['μ¬λ£ν ν°']) | |
| options[f"[{r['μ리방λ²λ³λͺ ']}] {r['μ리λͺ ']} (ID:{r['λ μνΌμΌλ ¨λ²νΈ']}) - {preview}"] = r['λ μνΌμΌλ ¨λ²νΈ'] | |
| selected_label = st.selectbox("π λΆμν λ μνΌλ₯Ό μ ννμΈμ", list(options.keys())) | |
| recipe_id = options[selected_label] | |
| c1, c2 = st.columns(2) | |
| with c1: target_str = st.text_input("π― λ°κΏ μ¬λ£", placeholder="λΌμ§κ³ κΈ°, μν") | |
| with c2: stop_str = st.text_input("π« μ κ±°ν 문ꡬ", placeholder="μ½κ°, μνμ©") | |
| if target_str: | |
| targets = [t.strip() for t in target_str.split(',') if t.strip()] | |
| stops = [s.strip() for s in stop_str.split(',') if s.strip()] | |
| current_recipe_row = logic.df[logic.df['λ μνΌμΌλ ¨λ²νΈ'] == recipe_id].iloc[0] | |
| recipe_ingredients = current_recipe_row['μ¬λ£ν ν°'] | |
| invalid_targets = [t for t in targets if t not in recipe_ingredients] | |
| if not targets: st.warning("νκ² μ¬λ£λ₯Ό μ λ ₯ν΄μ£ΌμΈμ.") | |
| elif invalid_targets: | |
| st.error(f"π¨ λ€μ μ¬λ£λ μ νν λ μνΌμ μμ΅λλ€: {', '.join(invalid_targets)}") | |
| st.info("π‘ ν: λ μνΌ λ―Έλ¦¬λ³΄κΈ°μ μλ μ¬λ£λͺ μ μ νν μ λ ₯ν΄μ£ΌμΈμ.") | |
| else: | |
| st.divider() | |
| has_result = False | |
| final_recs = [] | |
| if len(targets) == 1: | |
| st.subheader("πΉ λ¨μΌ μ¬λ£ λ체 μΆμ² (DB κΈ°λ°)") | |
| t = targets[0] | |
| res = logic.substitute_single(recipe_id, t, stops, w_w2v, w_d2v, w_method, w_cat, topn=5) | |
| st.markdown(f"**{t}** λ체 κ²°κ³Ό") | |
| if not res.empty: | |
| has_result = True | |
| final_recs = res['λ체μ¬λ£'].head(3).tolist() | |
| d_df = res[['λ체μ¬λ£', 'μ΅μ’ μ μ', 'saving_score']].copy() | |
| d_df['μμ μκ°λ³λ'] = d_df['saving_score'].apply(lambda x: format_saving(x)) | |
| d_df = d_df[['λ체μ¬λ£', 'μ΅μ’ μ μ', 'μμ μκ°λ³λ']] | |
| d_df.columns = ['μΆμ²μ¬λ£', 'μ ν©λ', 'μμ μκ°λ³λ'] | |
| st.dataframe(d_df.style.format("{:.1%}", subset=['μ ν©λ']).background_gradient(cmap='Greens', subset=['μ ν©λ']), use_container_width=True, hide_index=True) | |
| else: st.warning("κ²°κ³Ό μμ") | |
| elif len(targets) > 1: | |
| st.subheader("π§© μ΅μ μ μ¬λ£ μ‘°ν© (DB κΈ°λ° λ€μ€ λ체)") | |
| multi_res = logic.substitute_multi(recipe_id, targets, stops, w_w2v, w_d2v, w_method, w_cat) | |
| if multi_res: | |
| has_result = True | |
| final_recs = [", ".join(subs) for subs, score, saving in multi_res] | |
| m_df = pd.DataFrame([(f"{', '.join(subs)}", score, format_saving(saving, True)) for subs, score, saving in multi_res], columns=['μΆμ² μ‘°ν©', 'μ’ ν© μ μ', 'μμ μκ°λ³λ ν©κ³']) | |
| st.dataframe(m_df.style.format("{:.1%}", subset=['μ’ ν© μ μ']).background_gradient(cmap='Blues', subset=['μ’ ν© μ μ']), use_container_width=True, hide_index=True) | |
| else: st.info("μ‘°ν©μ μ°Ύμ μ μμ΅λλ€.") | |
| if has_result: | |
| current_state = f"DB_{final_dish_name}_{target_str}_{stop_str}_{w_w2v}_{w_d2v}_{w_method}_{w_cat}_{final_recs}" | |
| if 'last_log' not in st.session_state: st.session_state['last_log'] = "" | |
| if st.session_state['last_log'] != current_state: | |
| log_id = logic.save_log_to_db(final_dish_name, target_str, stops, w_w2v, w_d2v, w_method, w_cat, rec_list=final_recs, is_custom=False) | |
| st.session_state['current_log_id'] = log_id | |
| st.session_state['last_log'] = current_state | |
| if 'current_log_id' in st.session_state and st.session_state['current_log_id']: | |
| cl_id = st.session_state['current_log_id'] | |
| is_voted = cl_id in st.session_state['voted_logs'] | |
| st.write(""); b1, b2, _ = st.columns([0.2, 0.2, 0.6]) | |
| if is_voted: b1.success("β νκ° μλ£!"); b2.write("") | |
| else: | |
| b1.button("π λ§μ‘±ν΄μ", key="btn_sat_db", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id, "satisfy"), st.session_state['voted_logs'].add(cl_id), st.toast("κ°μ¬ν©λλ€!"))) | |
| b2.button("π μμ¬μμ", key="btn_dis_db", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id, "dissatisfy"), st.session_state['voted_logs'].add(cl_id), st.toast("μ견 κ°μ¬ν©λλ€."))) | |
| elif selected_mode == "β¨ Ver.2 λλ§μ μ¬λ£ μ λ ₯ (컀μ€ν )": | |
| st.markdown("""<div style="background-color: #fff5f0; padding: 15px; border-radius: 10px; margin-bottom: 20px;"><h4 style="margin:0; color:#cc5500;">[Ver.2] λλ§μ μ¬λ£ 리μ€νΈ μ λ ₯</h4><p style="margin:5px 0 0 0; font-size:14px;">λμ₯κ³ μ μ¬λ£λ€μ μ§μ μ λ ₯νμΈμ. λ¬Έλ§₯μ μ€μκ°μΌλ‘ λΆμνμ¬ μΆμ²ν©λλ€. (ν΅κ³ μ μ μ μΈ)</p></div>""", unsafe_allow_html=True) | |
| st.markdown("##### π·οΈ μ리λͺ μ λ ₯ (μ°Έκ³ μ©)") | |
| search_keyword_v2 = st.text_input("ν€μλ μ λ ₯ ν μν° (μ: λ³Άμλ°₯) - μ νμ¬ν", key="v2_search") | |
| custom_dish_name = search_keyword_v2 | |
| if search_keyword_v2: | |
| exact_match_v2 = logic.df[logic.df['μ리λͺ '] == search_keyword_v2] | |
| exact_name_v2 = exact_match_v2['μ리λͺ '].iloc[0] if not exact_match_v2.empty else None | |
| candidates_v2 = logic.df[logic.df['μ리λͺ '].str.contains(search_keyword_v2, na=False, case=False)] | |
| if exact_name_v2: candidates_v2 = candidates_v2[candidates_v2['μ리λͺ '] != exact_name_v2] | |
| candidate_names_v2 = sorted(candidates_v2['μ리λͺ '].unique().tolist())[:30] | |
| options_v2 = [] | |
| if exact_name_v2: options_v2.append(exact_name_v2) | |
| options_v2.append("(μ§μ μ λ ₯ν μ΄λ¦ μ¬μ©)") | |
| options_v2.extend(candidate_names_v2) | |
| if options_v2: | |
| idx_v2 = 0 if exact_name_v2 else 0 | |
| label_v2 = f"π‘ κ΄λ ¨ μ리λͺ λ°κ²¬ ({len(options_v2)-1}κ°)" | |
| if exact_name_v2: label_v2 += " - μ νν μ리λͺ λ°κ²¬!" | |
| sel_v2 = st.selectbox(label_v2, options_v2, index=idx_v2, key="v2_select") | |
| if sel_v2 != "(μ§μ μ λ ₯ν μ΄λ¦ μ¬μ©)": custom_dish_name = sel_v2 | |
| st.write("") | |
| context_str = st.text_area("π μ 체 μ¬λ£ 리μ€νΈ (μΌνλ‘ κ΅¬λΆ)", placeholder="μ: λ°₯, κ³λ, λν, κ°μ₯, μ°ΈκΈ°λ¦", height=100, key="v2_context") | |
| if context_str: | |
| context_ings_list = [ing.strip() for ing in context_str.split(',') if ing.strip()] | |
| if not context_ings_list: st.warning("μ¬λ£λ₯Ό ν κ° μ΄μ μ λ ₯ν΄μ£ΌμΈμ.") | |
| else: | |
| st.caption(f"μΈμλ μ¬λ£ ({len(context_ings_list)}κ°): {', '.join(context_ings_list)}") | |
| c1_c, c2_c = st.columns(2) | |
| with c1_c: target_str_c = st.text_input("π― λ°κΏ μ¬λ£ (μ 리μ€νΈ μ€)", placeholder="μ: κ³λ", key="v2_target") | |
| with c2_c: stop_str_c = st.text_input("π« μ κ±°ν 문ꡬ (μμ)", placeholder="μ: μ½κ°", key="v2_stop") | |
| if target_str_c: | |
| targets_c = [t.strip() for t in target_str_c.split(',') if t.strip()] | |
| stops_c = [s.strip() for s in stop_str_c.split(',') if s.strip()] | |
| invalid_targets = [t for t in targets_c if t not in context_ings_list] | |
| if invalid_targets: st.error(f"π¨ λ€μ μ¬λ£λ μ 체 리μ€νΈμ μμ΅λλ€: {', '.join(invalid_targets)}") | |
| elif not targets_c: st.warning("λ°κΏ μ¬λ£λ₯Ό μ λ ₯ν΄μ£ΌμΈμ.") | |
| else: | |
| st.divider() | |
| has_result_c = False | |
| final_recs_c = [] | |
| if len(targets_c) == 1: | |
| st.subheader("πΉ λ¨μΌ μ¬λ£ λ체 μΆμ² (컀μ€ν )") | |
| t_c = targets_c[0] | |
| res_c = logic.substitute_single_custom(t_c, context_ings_list, stops_c, w_w2v, w_d2v, excluded_ings=excluded_ingredients, topn=5) | |
| st.markdown(f"**{t_c}** λ체 κ²°κ³Ό") | |
| if not res_c.empty: | |
| has_result_c = True | |
| final_recs_c = res_c['λ체μ¬λ£'].head(3).tolist() | |
| d_df_c = res_c[['λ체μ¬λ£', 'μ΅μ’ μ μ', 'saving_score']].copy() | |
| d_df_c['μμ μκ°λ³λ'] = d_df_c['saving_score'].apply(lambda x: format_saving(x)) | |
| d_df_c = d_df_c[['λ체μ¬λ£', 'μ΅μ’ μ μ', 'μμ μκ°λ³λ']] | |
| d_df_c.columns = ['μΆμ²μ¬λ£', 'μ ν©λ', 'μμ μκ°λ³λ'] | |
| st.dataframe(d_df_c.style.format("{:.1%}", subset=['μ ν©λ']).background_gradient(cmap='Greens', subset=['μ ν©λ']), use_container_width=True, hide_index=True) | |
| else: st.warning("κ²°κ³Ό μμ") | |
| elif len(targets_c) > 1: | |
| st.subheader("π§© μ΅μ μ μ¬λ£ μ‘°ν© (컀μ€ν λ€μ€ λ체)") | |
| multi_res_c = logic.substitute_multi_custom(targets_c, context_ings_list, stops_c, w_w2v, w_d2v, excluded_ings=excluded_ingredients) | |
| if multi_res_c: | |
| has_result_c = True | |
| final_recs_c = [", ".join(subs) for subs, score, saving in multi_res_c] | |
| m_df_c = pd.DataFrame([(f"{', '.join(subs)}", score, format_saving(saving, True)) for subs, score, saving in multi_res_c], columns=['μΆμ² μ‘°ν©', 'μ’ ν© μ μ', 'μμ μκ°λ³λ ν©κ³']) | |
| st.dataframe(m_df_c.style.format("{:.1%}", subset=['μ’ ν© μ μ']).background_gradient(cmap='Blues', subset=['μ’ ν© μ μ']), use_container_width=True, hide_index=True) | |
| else: st.info("μ‘°ν©μ μ°Ύμ μ μμ΅λλ€.") | |
| if has_result_c: | |
| current_state_c = f"Custom_{custom_dish_name}_{target_str_c}_{stop_str_c}_{w_w2v}_{w_d2v}_{final_recs_c}" | |
| if 'last_log_c' not in st.session_state: st.session_state['last_log_c'] = "" | |
| if st.session_state['last_log_c'] != current_state_c: | |
| log_id_c = logic.save_log_to_db(custom_dish_name, target_str_c, stops_c, w_w2v, w_d2v, 0, 0, rec_list=final_recs_c, is_custom=True) | |
| st.session_state['current_log_id_c'] = log_id_c | |
| st.session_state['last_log_c'] = current_state_c | |
| if 'current_log_id_c' in st.session_state and st.session_state['current_log_id_c']: | |
| cl_id_c = st.session_state['current_log_id_c'] | |
| is_voted_c = cl_id_c in st.session_state['voted_logs'] | |
| st.write(""); b1_c, b2_c, _ = st.columns([0.2, 0.2, 0.6]) | |
| if is_voted_c: b1_c.success("β νκ° μλ£!"); b2_c.write("") | |
| else: | |
| b1_c.button("π λ§μ‘±ν΄μ", key="btn_sat_custom", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id_c, "satisfy"), st.session_state['voted_logs'].add(cl_id_c), st.toast("κ°μ¬ν©λλ€!"))) | |
| b2_c.button("π μμ¬μμ", key="btn_dis_custom", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id_c, "dissatisfy"), st.session_state['voted_logs'].add(cl_id_c), st.toast("μ견 κ°μ¬ν©λλ€."))) | |
| else: st.info("π μ 체 μ¬λ£ 리μ€νΈλ₯Ό λ¨Όμ μ λ ₯ν΄μ£ΌμΈμ.") | |
| # ------------------------------------------------------------------------- | |
| # 5. νλ¨ νΌλλ°± λ° λΆμ©μ΄ μ κ³ μμ | |
| # ------------------------------------------------------------------------- | |
| st.divider() | |
| col_feedback, col_stopword = st.columns(2) | |
| with col_feedback: | |
| st.subheader("π’ μλΉμ€ μ견 보λ΄κΈ°") | |
| with st.form("feedback_form"): | |
| text = st.text_area("κ°μ ν μ μ΄λ λ²κ·Έκ° μλ€λ©΄ μλ €μ£ΌμΈμ!", height=100, key="feedback_input_field") | |
| st.form_submit_button("μ견 보λ΄κΈ°", use_container_width=True, on_click=handle_feedback_submission) | |
| with col_stopword: | |
| st.subheader("π« λΆμ©μ΄(μ΄μν λ¨μ΄) μ κ³ νκΈ°") | |
| st.caption( | |
| "μΆμ² κ²°κ³Όμ μ΄μν λ¨μ΄κ° μλμ? μ κ³ ν΄μ£Όμλ©΄ λ€μλΆν° μ μΈλ©λλ€.", | |
| help="νμ¬ νμ΅ λ°μ΄ν°μ ν¬ν¨λ λΆμ©μ΄κ° λ무 λ§μ μΌμΌμ΄ μμμ μΌλ‘ μ²λ¦¬νκΈ° μ΄λ ΅μ΅λλ€. π₯ μ¬λ¬λΆμ μ κ³ κ° λͺ¨μ΄λ©΄ λ°μ΄ν°μ νμ§μ΄ λμμ§κ³ μΆμ² κ²°κ³Όλ λ μ νν΄μ§λλ€. μμ€ν κΈ°μ¬ λΆνλ립λλ€! π" | |
| ) | |
| st.info("π‘ Tip: 'κ°μ₯orμ§κ°μ₯' κ°μ κ²½μ° 'or'λ₯Ό μ κ³ νλ©΄ 'κ°μ₯μ§κ°μ₯'μΌλ‘ ν©μ³μ Έ μΆμ²μμ μ μΈλ©λλ€.") | |
| with st.form("stopword_form"): | |
| st.text_input("μ κ³ ν λ¨μ΄ μ λ ₯ (μΌνλ‘ κ΅¬λΆ)", placeholder="μ: λ©΄ν¬, ν©μμ΄μ , ν λ°", key="stopword_input_field") | |
| st.form_submit_button("μ κ³ νκΈ°", use_container_width=True, on_click=handle_stopword_submission) | |