import logging import time import streamlit as st from db import ( debate_ip_column_available, ensure_db_login, fetch_debate_status, fetch_live_messages, fetch_opinion_change, fetch_pending_teacher_accounts, fetch_topic_data, init_db, opinion_changes_available, submit_opinion, update_topic, using_service_role_key, ) from config import APP_CSS, MAX_STUDENT_NAME_LEN from utils import anonymize_ip, get_client_ip, get_kst_now_str, log_audit from validators import validate_opinion_content, validate_student_name from views.home import render_home_page from views.lobby import render_lobby_page from components.admin_panel import render_admin_page from components.sidebar import render_sidebar logger = logging.getLogger("talk_trace_ai") if not logger.handlers: logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s" ) supabase = init_db() if not using_service_role_key(): ensure_db_login(supabase) st.set_page_config(page_title="λ§μμ·¨(Talk-Trace) AI", layout="wide") st.markdown(APP_CSS, unsafe_allow_html=True) if 'reset_key' not in st.session_state: st.session_state['reset_key'] = 0 if 'ai_hint_text' not in st.session_state: st.session_state['ai_hint_text'] = "" if 'ai_report_text' not in st.session_state: st.session_state['ai_report_text'] = "" if 'page' not in st.session_state: st.session_state['page'] = "home" if 'current_room' not in st.session_state: st.session_state['current_room'] = "" if 'joined' not in st.session_state: st.session_state['joined'] = False if 'teacher_auth' not in st.session_state: st.session_state['teacher_auth'] = False if 'admin_auth' not in st.session_state: st.session_state['admin_auth'] = False if 'teacher_id' not in st.session_state: st.session_state['teacher_id'] = "" if 'is_working' not in st.session_state: st.session_state['is_working'] = False if 'ai_hint_manual_mode' not in st.session_state: st.session_state['ai_hint_manual_mode'] = False if '_last_debate_status' not in st.session_state: st.session_state['_last_debate_status'] = None if st.session_state['page'] != "home": col_home_btn, _ = st.columns([1, 7]) with col_home_btn: if st.button("π ν", use_container_width=True): st.session_state['page'] = "home" st.session_state['joined'] = False st.session_state['teacher_auth'] = False st.rerun() if st.session_state['page'] == "home": render_home_page() # plotly / google.generativeai μν¬νΈλ ν νλ©΄μμ λΆνμ β st.stop() μ΄νμ λ‘λ from components.chat_board import render_chat_board from components.teacher_dashboard import render_teacher_dashboard from components.opinion_change import render_pre_opinion_form, render_post_opinion_section sidebar_ctx = render_sidebar(supabase) user_role = sidebar_ctx['user_role'] room_name = sidebar_ctx['room_name'] teacher_auth = sidebar_ctx['teacher_auth'] admin_auth = sidebar_ctx['admin_auth'] student_name = sidebar_ctx['student_name'] student_number = sidebar_ctx['student_number'] # admin 첫 μ μ μ pending μ¬λΆμ λ°λΌ 첫 νλ©΄ κ²°μ (μ΅μ΄ 1ν) if admin_auth and not st.session_state.get('_admin_redirected'): _pending = fetch_pending_teacher_accounts(supabase) st.session_state['page'] = "admin_approval" if _pending else "lobby" st.session_state['joined'] = False st.session_state['_admin_redirected'] = True st.rerun() if st.session_state['page'] == "admin_approval": render_admin_page(supabase, user_role, teacher_auth, admin_auth) if not st.session_state['joined']: render_lobby_page(supabase, user_role, teacher_auth, room_name, student_number) # === Joined room view === topic_data = fetch_topic_data(supabase, room_name) current_topic = topic_data.get('title', "μμ μ£Όμ λ‘ λνν΄ λ΄ μλ€.") current_mode = topic_data.get('mode', "βοΈ μ°¬λ° ν λ‘ ") act_type = "ν λ‘ " if "ν λ‘ " in current_mode else "ν μ" if admin_auth: col_title, col_btn1, col_btn2 = st.columns([4, 1, 1]) with col_title: st.title(f"ποΈ λ§μμ·¨(Talk-Trace) AI [{room_name}]") with col_btn1: if st.button("π ID μμ² μλ½", use_container_width=True): st.session_state['page'] = "admin_approval" st.rerun() with col_btn2: if st.button("πͺ λ§μμ·¨ AI λκΈ°μ€", use_container_width=True): st.session_state['page'] = "lobby" st.rerun() else: st.title(f"ποΈ λ§μμ·¨(Talk-Trace) AI [{room_name}]") st.info(f"**νμ¬ μ£Όμ :** {current_topic} ({current_mode})") if user_role == "κ΅μ¬" and teacher_auth: with st.expander("βοΈ μ£Όμ μμ ", expanded=False): _edit_title = st.text_input("μ μ£Όμ ", value=current_topic, max_chars=120, key="edit_topic_title") _edit_mode = st.radio( "μ§ν λ°©μ", ["βοΈ μ°¬λ° ν λ‘ ", "π‘ μμ ν μ"], index=0 if "ν λ‘ " in current_mode else 1, horizontal=True, key="edit_topic_mode", ) if st.button("β μ£Όμ μ μ₯", type="primary", use_container_width=True, key="edit_topic_save"): if not _edit_title.strip(): st.warning("μ£Όμ λ₯Ό μ λ ₯ν΄ μ£ΌμΈμ.") else: if update_topic(supabase, room_name, _edit_title.strip(), _edit_mode) is not None: st.toast("β μ£Όμ κ° μμ λμμ΅λλ€.", icon="βοΈ") st.rerun() @st.fragment(run_every=5) def _poll_debate_status(room_name): """νμ νλ©΄μμ 5μ΄λ§λ€ ν λ‘ μνλ₯Ό νμΈνκ³ λ³κ²½ μ μ 체 rerun.""" current = fetch_debate_status(supabase, room_name) if current != st.session_state.get("_last_debate_status"): st.session_state["_last_debate_status"] = current st.rerun(scope="app") @st.fragment def _render_opinion_input(supabase, room_name, user_role, student_name, student_number, current_mode): st.subheader("π£οΈ λ΄ μ견 μμ±") col_input, col_stt = st.columns([4, 1]) with col_input: user_input = st.text_area( "μ견μ μ λ ₯νμΈμ", key=f"input_{st.session_state['reset_key']}", height=80, label_visibility="collapsed", ) opts = ["π΅ μ°¬μ±", "π΄ λ°λ"] if current_mode == "βοΈ μ°¬λ° ν λ‘ " else ["π‘ μμ΄λμ΄", "β 보좩", "β μ§λ¬Έ"] sentiment = st.radio("μ견 μ±κ²©", opts, horizontal=True) with col_stt: st.components.v1.html( """
λκΈ° μ€...
""", height=120, ) _elapsed = time.time() - st.session_state.get('last_submit_ts', 0) _submit_cooldown = _elapsed < 3 _submit_disabled = _submit_cooldown or st.session_state.get('is_working', False) if st.button("μ견 μ μΆ", use_container_width=True, type="primary", disabled=_submit_disabled): if st.session_state.get('is_working', False): st.stop() st.session_state['is_working'] = True input_ok, safe_input, input_error_code, input_error_message = validate_opinion_content(user_input, max_len=700) student_ok, safe_student_name, student_error_code, student_error_message = validate_student_name(student_name, max_len=MAX_STUDENT_NAME_LEN) student_number_ok, safe_student_number, _, student_number_error_message = validate_student_name(student_number, max_len=20) if not student_number_ok and user_role == "νμ": st.session_state['is_working'] = False st.error(f"β {student_number_error_message}") st.stop() if user_role == "νμ" and (not safe_student_name or safe_student_name == "μ΅λͺ "): safe_student_name = safe_student_number or "νλ²λ―Έμ λ ₯" if input_ok and safe_input: now = get_kst_now_str() author_role_for_submit = "κ΅μ¬" if user_role == "κ΅μ¬" else "νμ" client_ip = get_client_ip() insert_payload = { "room_name": room_name, "timestamp": now, "student_name": safe_student_name, "content": safe_input, "sentiment": sentiment, "author_role": author_role_for_submit, } if debate_ip_column_available() and client_ip: anonymized_ip = anonymize_ip(client_ip) if anonymized_ip: insert_payload["ip_address"] = anonymized_ip try: res = submit_opinion(supabase, insert_payload) if res is None: st.session_state['is_working'] = False st.stop() st.session_state['last_submit_ts'] = time.time() st.session_state['is_working'] = False fetch_live_messages.clear() log_audit( "opinion_submitted", room_name=room_name, actor_name=safe_student_name, role=author_role_for_submit, sentiment=sentiment, client_ip=client_ip if client_ip else "N/A", ) st.session_state['reset_key'] += 1 st.rerun(scope="app") except Exception as e: st.session_state['is_working'] = False st.error(f"μ μ₯ μ€ν¨: {e}") else: st.session_state['is_working'] = False st.warning(f"{input_error_message}") if _submit_cooldown: st.caption("β³ μ μΆ μ±κ³΅ ν 3μ΄κ° μ견μ μ μΆν μ μμ΅λλ€.") time.sleep(0.5) st.rerun() if user_role == "νμ" and opinion_changes_available(): _poll_debate_status(room_name) debate_status = fetch_debate_status(supabase, room_name) # νμμκ² ν λ‘ μ§ν μνλ₯Ό νμ λͺ μμ μΌλ‘ νμ if debate_status == "ended": st.warning(f"π΄ **{act_type} μ’ λ£** β μλμμ {act_type} ν μκ° λ³νλ₯Ό κΈ°λ‘ν΄μ£ΌμΈμ.") else: st.success(f"π’ **{act_type} μ§ν μ€** β μμ λ‘κ² μ견μ λλ 보μΈμ.") row = fetch_opinion_change(supabase, room_name, student_name) has_pre_opinion = bool((row or {}).get("pre_opinion")) if debate_status == "ended": if has_pre_opinion: render_post_opinion_section(supabase, room_name, student_name, act_type, current_topic) else: st.warning(f"{'ν λ‘ ' if act_type == 'ν λ‘ ' else 'ν μ'}μ΄ μ’ λ£λμμ΅λλ€. ν λ‘ μ μκ°μ 미리 κΈ°λ‘νμ§ μμ μ°Έμ¬ν μ μμ΅λλ€.") else: if not has_pre_opinion: render_pre_opinion_form(supabase, room_name, student_name, current_topic, act_type) st.caption("π‘ μμμ ν λ‘ μ μκ°μ μ μΆνλ©΄ μ견 μμ±μ΄ νμ±νλ©λλ€.") else: _render_opinion_input(supabase, room_name, user_role, student_name, student_number, current_mode) else: _render_opinion_input(supabase, room_name, user_role, student_name, student_number, current_mode) st.divider() render_chat_board(supabase, room_name, user_role, teacher_auth, student_name, current_mode, act_type) if user_role == "κ΅μ¬" and teacher_auth: render_teacher_dashboard(supabase, room_name, user_role, student_name, current_topic, current_mode, act_type)