# app.py # ─────────────────────────────────────────────────────────────────────────── # 챗봇 실험 메인 앱 (SQLite 제거 버전). # 흐름: intro → pre(사전설문) → chat(대화) → post(사후설문) → done # 저장: storage.py의 이중 안전망(구글시트 + 메모리). DB 파일이 없다. # 사이드바: (위) 연구 안내 설명 / (아래) 데이터 CSV 다운로드 백업 # ─────────────────────────────────────────────────────────────────────────── import uuid import time import streamlit as st import storage from prompts import SYSTEM_PROMPTS, TASK_INSTRUCTION from llm import stream_response st.set_page_config(page_title="진로 챗봇 대화 연구", page_icon="💬") # ===== 세션 상태 초기화 ===== # Streamlit은 매 상호작용마다 스크립트를 위→아래로 다시 실행한다. # 따라서 '지금 어느 단계인가'를 session_state에 저장해 둬야 한다. if "stage" not in st.session_state: st.session_state.stage = "intro" # 현재 단계 st.session_state.participant_id = str(uuid.uuid4()) # 익명 식별자(이름·이메일 안 받음) st.session_state.condition = None # 'A' 또는 'B' st.session_state.messages = [] # 화면 표시용 대화 기록 st.session_state.turn = 0 # 대화 턴 수 storage.init_state(st.session_state) # 누적 리스트 3개 준비 # ===== 사이드바: 위쪽 안내 + 아래쪽 다운로드 ===== def render_sidebar(): with st.sidebar: # ── (위) 연구 안내 설명 ── st.header("📋 연구 안내") st.markdown( "- **소요시간**: 약 10~15분\n" "- **절차**: 사전 설문 → 챗봇 대화(5턴+) → 사후 설문\n" "- **익명성**: 이름·이메일을 수집하지 않습니다.\n" "- **유의**: 챗봇은 AI이며, 전문 자문이 필요하면 전문가에게 별도 문의하세요." ) st.divider() # ── (아래) 데이터 다운로드 백업 ── # 안전망 2: HF Dataset 업로드가 실패해도 이 메모리 누적분으로 회수 가능. # 주의: session_state는 '현재 브라우저 세션'의 데이터만 담는다. # 전체 참여자 통합본은 HF Dataset에서 받는다(안전망 1). st.header("⬇️ 데이터 다운로드") st.caption("이 세션에 쌓인 기록(백업용). 전체 통합본은 HF Dataset 참조.") n_p = len(st.session_state["rows_participants"]) n_m = len(st.session_state["rows_messages"]) n_s = len(st.session_state["rows_surveys"]) st.download_button("참여자 CSV", storage.to_csv_bytes(st.session_state["rows_participants"]), "participants.csv", "text/csv", disabled=(n_p == 0)) st.download_button("대화 CSV", storage.to_csv_bytes(st.session_state["rows_messages"]), "messages.csv", "text/csv", disabled=(n_m == 0)) st.download_button("설문 CSV", storage.to_csv_bytes(st.session_state["rows_surveys"]), "surveys.csv", "text/csv", disabled=(n_s == 0)) st.caption(f"참여자 {n_p} · 대화 {n_m}줄 · 설문 {n_s}줄") render_sidebar() # ===== Stage 1: 인트로 + 동의 ===== if st.session_state.stage == "intro": st.title("AI 진로상담 챗봇 대화 연구") st.markdown( "졸업 후 진로에 대해 챗봇과 대화하는 연구입니다. " "사전 설문 → 대화 → 사후 설문 순으로 진행됩니다." ) if st.button("동의하고 시작하기", type="primary"): # 참여자별 랜덤 배정 (세션이 분리돼 있어 교대 배정이 불가하므로) import random cond = random.choice(["A", "B"]) st.session_state.condition = cond storage.add_participant(st.session_state, st.session_state.participant_id, cond) st.session_state.stage = "pre" st.rerun() # ===== Stage 2: 사전조사 ===== elif st.session_state.stage == "pre": st.title("사전 설문") with st.form("pre_survey"): year = st.selectbox("학년", ["1학년", "2학년", "3학년", "4학년", "졸업유예/휴학", "대학원생"]) major = st.selectbox("전공 계열", ["인문", "사회", "상경", "공학", "자연", "의약", "예체능", "기타"]) d_stage = st.selectbox("현재 진로 결정 단계", ["아직 탐색 중", "방향이 어느 정도 잡힘", "구체적 계획 수립 중", "거의 확정"]) anxiety = st.slider("최근 진로 불안 정도 (1: 전혀 없음 ~ 7: 매우 큼)", 1, 7, 4) ai_freq = st.selectbox("AI 챗봇 사용 빈도", ["거의 안 씀", "월 1~2회", "주 1~2회", "거의 매일"]) if st.form_submit_button("다음"): pid, cond = st.session_state.participant_id, st.session_state.condition for qid, val in [("year", year), ("major", major), ("decision_stage", d_stage), ("career_anxiety", anxiety), ("ai_usage", ai_freq)]: storage.add_survey(st.session_state, pid, cond, "pre", qid, val) st.session_state.stage = "chat" st.rerun() # ===== Stage 3: 대화 ===== elif st.session_state.stage == "chat": st.title("진로 챗봇과 대화하기") st.info(TASK_INSTRUCTION) st.caption(f"최소 5턴 이상 대화 후 아래 버튼을 누르세요. (현재 {st.session_state.turn}턴)") # 이전 메시지 다시 그리기(매 실행마다 화면을 새로 그리므로 필요) for m in st.session_state.messages: with st.chat_message(m["role"]): st.markdown(m["content"]) if prompt := st.chat_input("메시지를 입력하세요"): pid, cond = st.session_state.participant_id, st.session_state.condition st.session_state.turn += 1 # 1) 사용자 메시지: 화면 표시 + 저장 st.session_state.messages.append({"role": "user", "content": prompt}) storage.add_message(st.session_state, pid, cond, st.session_state.turn, "user", prompt) with st.chat_message("user"): st.markdown(prompt) # 2) 챗봇 응답: 스트리밍으로 한 청크씩 표시 with st.chat_message("assistant"): placeholder = st.empty() full, t0 = "", time.time() try: for delta in stream_response(SYSTEM_PROMPTS[cond], st.session_state.messages): full += delta placeholder.markdown(full + "▌") # 커서 효과 placeholder.markdown(full) except Exception as e: full = f"[오류가 발생했습니다: {e}]" placeholder.markdown(full) latency = int((time.time() - t0) * 1000) st.session_state.messages.append({"role": "assistant", "content": full}) storage.add_message(st.session_state, pid, cond, st.session_state.turn, "assistant", full, latency) # 5턴 이상이면 사후설문으로 넘어가는 버튼 노출 if st.session_state.turn >= 5: if st.button("대화 종료하고 사후 설문으로", type="primary"): st.session_state.stage = "post" st.rerun() # ===== Stage 4: 사후조사 ===== elif st.session_state.stage == "post": st.title("사후 설문") with st.form("post_survey"): usefulness = st.slider("받은 정보가 유용했다 (1~7)", 1, 7, 4) warmth = st.slider("챗봇이 따뜻했다 (1~7)", 1, 7, 4) competence = st.slider("챗봇이 유능했다 (1~7)", 1, 7, 4) trust = st.slider("챗봇을 신뢰한다 (1~7)", 1, 7, 4) clarity = st.slider("대화 후 진로 고민이 더 정리되었다 (1~7)", 1, 7, 4) recommend = st.slider("친구에게 추천할 의향 (1~7)", 1, 7, 4) free_text = st.text_area("자유 의견: 가장 도움이 된 점, 아쉬웠던 점 (선택)") if st.form_submit_button("제출"): pid, cond = st.session_state.participant_id, st.session_state.condition for qid, val in [("usefulness", usefulness), ("warmth", warmth), ("competence", competence), ("trust", trust), ("clarity", clarity), ("recommend", recommend), ("free_text", free_text)]: storage.add_survey(st.session_state, pid, cond, "post", qid, val) # 완료 표시(메모리): 해당 참여자 행의 completed를 1로 for r in st.session_state["rows_participants"]: if r["participant_id"] == pid: r["completed"] = 1 # 안전망 1: 이번 세션 누적분을 HF Dataset에 일괄 업로드 # (완료 시점에 한 번만. 실패해도 메모리 백업이 남으므로 흐름은 계속) storage.push_to_dataset(st.session_state) st.session_state.stage = "done" st.rerun() # ===== Stage 5: 완료 ===== elif st.session_state.stage == "done": st.title("참여해주셔서 감사합니다 🙏") st.markdown(f"참여자 ID: `{st.session_state.participant_id}`") st.caption("응답이 안전하게 기록되었습니다.")