Spaces:
Running
Running
| """ | |
| 세계관 설명 페이지 모듈 | |
| """ | |
| import streamlit as st | |
| from datetime import datetime | |
| from config.constants import SUGGESTED_WORLD_QUESTIONS | |
| from modules.world_generator import ( | |
| master_answer_question, | |
| generate_world_expansion | |
| ) | |
| def world_description_page(): | |
| """세계관 설명 및 질문 페이지 구현""" | |
| st.header("2️⃣ 세계관 설명") | |
| # 마스터 메시지 표시 | |
| st.markdown(f"<div class='master-text'>{st.session_state.master_message}</div>", unsafe_allow_html=True) | |
| # 세계관 설명 표시 - 단락 구분 개선 | |
| world_desc_paragraphs = st.session_state.world_description.split("\n\n") | |
| formatted_desc = "" | |
| for para in world_desc_paragraphs: | |
| formatted_desc += f"<p>{para}</p>\n" | |
| st.markdown(f"<div class='story-text'>{formatted_desc}</div>", unsafe_allow_html=True) | |
| # "다른 세계 탐험하기" 버튼 추가 | |
| if st.button("🌍 다른 세계 탐험하기", key="explore_other_world", use_container_width=True): | |
| # 세션 상태 초기화 (일부만) | |
| for key in ['theme', 'world_description', 'world_generated', 'world_accepted', | |
| 'question_answers', 'question_count', 'current_location']: | |
| if key in st.session_state: | |
| del st.session_state[key] | |
| # 테마 선택 화면으로 돌아가기 | |
| st.session_state.stage = 'theme_selection' | |
| st.session_state.master_message = "새로운 세계를 탐험해보세요!" | |
| st.rerun() | |
| # 탭 기반 UI로 변경 | |
| tabs = st.tabs(["세계관 확장", "질문하기", "탐험 시작"]) | |
| # 세계관 확장 탭 | |
| with tabs[0]: | |
| world_expansion_tab() | |
| # 질문하기 탭 | |
| with tabs[1]: | |
| world_question_tab() | |
| # 탐험 시작 탭 | |
| with tabs[2]: | |
| exploration_start_tab() | |
| def world_expansion_tab(): | |
| """세계관 확장 탭 내용""" | |
| st.subheader("세계관 이어서 작성") | |
| # 설명 추가 | |
| st.markdown(""" | |
| <div style='background-color: #1e2636; padding: 15px; border-radius: 5px; margin-bottom: 15px;'> | |
| <p>세계관을 더 풍부하게 만들어보세요. AI 마스터에게 특정 부분을 확장해달라고 요청하거나, 직접 내용을 추가할 수 있습니다.</p> | |
| <p>추가된 내용은 기존 세계관과 자연스럽게 통합되어 더 깊이 있는 세계를 만들어갑니다.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 직접 입력 옵션 추가 | |
| expand_method = st.radio( | |
| "확장 방법 선택:", | |
| ["AI 마스터에게 맡기기", "직접 작성하기"], | |
| horizontal=True | |
| ) | |
| # AI 확장 선택 시 | |
| if expand_method == "AI 마스터에게 맡기기": | |
| handle_ai_expansion() | |
| # 직접 작성 선택 시 | |
| else: | |
| handle_manual_expansion() | |
| def handle_ai_expansion(): | |
| """AI가 세계관을 확장하는 기능 처리""" | |
| # 확장할 주제 선택 (더 구체적인 세계관 생성 유도) | |
| expansion_topics = { | |
| "역사와 전설": "세계의 역사적 사건, 신화, 전설적 영웅 등에 대한 이야기를 확장합니다.", | |
| "마법/기술 체계": "세계의 마법 시스템이나 기술 체계의 작동 방식과 한계를 자세히 설명합니다.", | |
| "종족과 문화": "세계에 존재하는 다양한 종족들과 그들의 문화, 관습, 생활 방식을 확장합니다.", | |
| "정치 체계와 세력": "권력 구조, 주요 세력 간의 관계, 정치적 갈등 등을 더 자세히 설명합니다.", | |
| "지리와 환경": "세계의 지리적 특성, 주요 지역, 기후, 자연환경에 대해 확장합니다.", | |
| "현재 갈등과 위기": "세계에서 진행 중인 갈등, 위기, 중요한 문제에 대해 자세히 설명합니다." | |
| } | |
| topic_options = list(expansion_topics.keys()) | |
| topic_descriptions = list(expansion_topics.values()) | |
| # 설명과 함께 확장 주제 선택 | |
| expansion_topic_idx = st.selectbox( | |
| "확장할 세계관 요소를 선택하세요:", | |
| range(len(topic_options)), | |
| format_func=lambda i: topic_options[i] | |
| ) | |
| expansion_topic = topic_options[expansion_topic_idx] | |
| # 선택한 주제에 대한 설명 표시 | |
| st.markdown(f""" | |
| <div style='background-color: #1e2636; padding: 10px; border-radius: 5px; margin: 10px 0;'> | |
| <p>{topic_descriptions[expansion_topic_idx]}</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 확장 버튼 누르기 전과 후의 상태 관리 | |
| if 'continuation_generated' not in st.session_state: | |
| st.session_state.continuation_generated = False | |
| if not st.session_state.continuation_generated: | |
| if st.button("세계관 확장하기", key="expand_world"): | |
| with st.spinner("이어질 내용을 생성 중..."): | |
| try: | |
| # 확장 내용 생성 | |
| st.session_state.continuation_text = generate_world_expansion( | |
| st.session_state.world_description, | |
| st.session_state.theme, | |
| expansion_topic | |
| ) | |
| st.session_state.continuation_generated = True | |
| except Exception as e: | |
| st.error(f"내용 생성 중 오류 발생: {e}") | |
| # 오류 발생 시 백업 응답 | |
| st.session_state.continuation_text = "이 세계는 더 많은 비밀과 모험으로 가득 차 있습니다. 숨겨진 장소와 만날 수 있는 흥미로운 캐릭터들이 여러분을 기다리고 있습니다." | |
| st.session_state.continuation_generated = True | |
| st.rerun() | |
| # 생성된 내용이 있으면 표시 | |
| if st.session_state.continuation_generated: | |
| # 생성된 내용과 어떻게 반영되는지 시각적으로 표시 | |
| st.subheader("확장된 세계관 내용:") | |
| st.info("다음 내용이 세계관에 추가됩니다. '이 내용으로 적용하기'를 클릭하면 세계관에 반영됩니다.") | |
| # 단락 나누기 - 가독성 개선 | |
| continuation_paragraphs = st.session_state.continuation_text.split("\n\n") | |
| formatted_continuation = "" | |
| for para in continuation_paragraphs: | |
| formatted_continuation += f"<p>{para}</p>\n" | |
| st.markdown(f"<div class='story-text' style='border-left: 4px solid #4CAF50;'>{formatted_continuation}</div>", unsafe_allow_html=True) | |
| # 적용 버튼과 다시 생성 버튼 병렬 배치 | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("이 내용으로 적용하기", key="apply_expansion"): | |
| # 세계 설명에 추가 | |
| st.session_state.world_description += "\n\n## " + expansion_topic + "\n" + st.session_state.continuation_text | |
| # 상태 초기화 | |
| st.session_state.continuation_generated = False | |
| if "continuation_text" in st.session_state: | |
| del st.session_state.continuation_text | |
| st.session_state.master_message = "세계관이 더욱 풍부해졌습니다! 이 세계에 대해 더 궁금한 점이 있으신가요?" | |
| st.success("세계관이 성공적으로 확장되었습니다!") | |
| st.rerun() | |
| with col2: | |
| if st.button("다시 생성하기", key="regenerate_expansion"): | |
| # 내용 다시 생성하도록 상태 초기화 | |
| st.session_state.continuation_generated = False | |
| if "continuation_text" in st.session_state: | |
| del st.session_state.continuation_text | |
| st.rerun() | |
| def handle_manual_expansion(): | |
| """사용자가 직접 세계관을 확장하는 기능 처리""" | |
| st.write("세계관에 추가하고 싶은 내용을 직접 작성해보세요:") | |
| user_continuation = st.text_area("세계관 추가 내용:", height=200) | |
| # 사용성 개선: 무한 추가 방지를 위한 확인 메시지 | |
| if user_continuation and st.button("내용 추가하기", key="add_user_content"): | |
| # 미리보기 표시 | |
| st.subheader("추가될 내용:") | |
| st.info("다음 내용이 세계관에 추가됩니다. 내용이 올바른지 확인하세요.") | |
| # 단락 나누기 - 가독성 개선 | |
| user_paragraphs = user_continuation.split("\n\n") | |
| formatted_user_content = "" | |
| for para in user_paragraphs: | |
| formatted_user_content += f"<p>{para}</p>\n" | |
| st.markdown(f"<div class='story-text' style='border-left: 4px solid #4CAF50;'>{formatted_user_content}</div>", unsafe_allow_html=True) | |
| # 확인 후 추가 | |
| confirm = st.checkbox("위 내용을 세계관에 추가하시겠습니까?", key="confirm_add_content") | |
| if confirm and st.button("확인 후 추가하기", key="confirm_add_user_content"): | |
| # 작성한 내용 추가 | |
| st.session_state.world_description += "\n\n## 직접 추가한 세계관 내용\n" + user_continuation | |
| st.session_state.master_message = "직접 작성하신 내용이 세계관에 추가되었습니다! 이 세계가 더욱 풍부해졌습니다." | |
| st.success("세계관에 내용이 성공적으로 추가되었습니다!") | |
| st.rerun() | |
| def world_question_tab(): | |
| """세계관 질문 탭 내용""" | |
| st.subheader("세계관에 대한 질문") | |
| # 설명 추가 | |
| st.markdown(""" | |
| <div style='background-color: #1e2636; padding: 15px; border-radius: 5px; margin-bottom: 15px;'> | |
| <p>세계에 대해 궁금한 점을 마스터에게 질문해보세요. 세계의 역사, 문화, 종족, 마법/기술 체계 등에 대한 질문을 할 수 있습니다.</p> | |
| <p>마스터의 답변은 세계관에 추가되어 더 풍부한 배경을 만들어갑니다.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 질문 처리 상태 관리 | |
| if 'question_processing' not in st.session_state: | |
| st.session_state.question_processing = False | |
| if 'selected_suggested_question' not in st.session_state: | |
| st.session_state.selected_suggested_question = None | |
| if 'world_questions_history' not in st.session_state: | |
| st.session_state.world_questions_history = [] | |
| # 제안된 질문 표시 | |
| st.write("제안된 질문:") | |
| question_cols = st.columns(2) | |
| for i, q in enumerate(SUGGESTED_WORLD_QUESTIONS): | |
| with question_cols[i % 2]: | |
| # 토글 버튼으로 질문 선택 | |
| is_selected = st.checkbox(q, key=f"toggle_q_{i}", value=(st.session_state.selected_suggested_question == q)) | |
| if is_selected: | |
| st.session_state.selected_suggested_question = q | |
| elif st.session_state.selected_suggested_question == q: | |
| st.session_state.selected_suggested_question = None | |
| # 선택된 질문이 있으면 질문하기 버튼 표시 | |
| if st.session_state.selected_suggested_question: | |
| st.markdown("<div style='margin-top: 15px;'></div>", unsafe_allow_html=True) | |
| st.success(f"'{st.session_state.selected_suggested_question}' 질문이 선택되었습니다.") | |
| # 직접 질문 입력 섹션 | |
| st.markdown("<div style='margin-top: 20px; padding-top: 10px; border-top: 1px solid #3d4c63;'></div>", unsafe_allow_html=True) | |
| st.write("### 직접 질문 입력") | |
| # 기본값 설정 (선택된 질문이 있으면 해당 질문 표시) | |
| default_question = st.session_state.get('custom_question_value', st.session_state.get('selected_suggested_question', '')) | |
| # 폼 사용으로 무한 생성 방지 | |
| with st.form(key="world_question_form"): | |
| custom_question = st.text_input("질문 내용:", value=default_question, key="custom_world_question") | |
| submit_question = st.form_submit_button("질문하기", use_container_width=True, disabled=st.session_state.question_processing) | |
| # 질문이 제출되었을 때 | |
| if submit_question and (custom_question or st.session_state.selected_suggested_question): | |
| process_world_question(custom_question or st.session_state.selected_suggested_question) | |
| # 이전 질문 및 답변 표시 | |
| if st.session_state.world_questions_history: | |
| st.markdown("<div style='margin-top: 30px; padding-top: 10px; border-top: 1px solid #3d4c63;'></div>", unsafe_allow_html=True) | |
| st.write("### 이전 질문 및 답변") | |
| for i, qa in enumerate(reversed(st.session_state.world_questions_history)): | |
| with st.expander(f"Q: {qa['question']} ({qa['timestamp']})"): | |
| st.markdown(qa['answer']) | |
| def process_world_question(question): | |
| """세계관 질문 처리 함수""" | |
| # 이미 처리 중이 아닐 때만 실행 | |
| if not st.session_state.question_processing: | |
| st.session_state.question_processing = True | |
| # 응답 표시할 플레이스홀더 생성 | |
| response_placeholder = st.empty() | |
| response_placeholder.info("마스터가 답변을 작성 중입니다... 잠시만 기다려주세요.") | |
| # 질문 처리 및 답변 생성 | |
| try: | |
| answer = master_answer_question( | |
| question, | |
| st.session_state.world_description, | |
| st.session_state.theme | |
| ) | |
| # 질문과 답변을 세션 상태에 저장 | |
| qa_pair = { | |
| "question": question, | |
| "answer": answer, | |
| "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| st.session_state.world_questions_history.append(qa_pair) | |
| # 세계관에 질문과 답변 추가 | |
| st.session_state.world_description += f"\n\n## 질문: {question}\n{answer}" | |
| # 단락 구분 적용 | |
| answer_paragraphs = answer.split("\n\n") | |
| formatted_answer = "" | |
| for para in answer_paragraphs: | |
| formatted_answer += f"<p>{para}</p>\n" | |
| # 응답 표시 | |
| response_placeholder.markdown(f""" | |
| <div style='background-color: #2d3748; padding: 15px; border-radius: 5px; margin: 10px 0; border-left: 4px solid #6b8afd;'> | |
| <div style='font-weight: bold; margin-bottom: 5px;'>질문: {question}</div> | |
| <div>{formatted_answer}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 상태 초기화 | |
| st.session_state.master_message = "질문에 답변했습니다. 더 궁금한 점이 있으신가요?" | |
| except Exception as e: | |
| st.error(f"응답 생성 중 오류가 발생했습니다: {e}") | |
| response_placeholder.error("질문 처리 중 오류가 발생했습니다. 다시 시도해주세요.") | |
| finally: | |
| # 처리 완료 상태로 변경 | |
| st.session_state.question_processing = False | |
| st.session_state.selected_suggested_question = None | |
| st.session_state.custom_question_value = '' | |
| def exploration_start_tab(): | |
| """탐험 시작 탭 내용""" | |
| st.subheader("탐험 시작하기") | |
| # 설명 추가 | |
| st.markdown(""" | |
| <div style='background-color: #1e2636; padding: 15px; border-radius: 5px; margin-bottom: 15px;'> | |
| <p>모험을 시작할 지역을 선택하고 캐릭터 생성으로 진행하세요.</p> | |
| <p>선택한 지역은 캐릭터가 모험을 시작하는 첫 장소가 됩니다.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 시작 지점 선택 | |
| if 'available_locations' in st.session_state and st.session_state.available_locations: | |
| st.write("#### 시작 지점 선택") | |
| st.write("모험을 시작할 위치를 선택하세요:") | |
| # 사용성 개선: 선택된 위치를 표시 | |
| selected_location = st.session_state.get('current_location', '') | |
| # 시작 지점 그리드 표시 | |
| location_cols = st.columns(3) | |
| for i, location in enumerate(st.session_state.available_locations): | |
| with location_cols[i % 3]: | |
| # 현재 선택된 위치인 경우 다른 스타일로 표시 | |
| if location == selected_location: | |
| st.markdown(f""" | |
| <div style='background-color: #4CAF50; color: white; padding: 10px; | |
| border-radius: 5px; text-align: center; margin-bottom: 10px;'> | |
| ✓ {location} (선택됨) | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 선택 취소 버튼 | |
| if st.button("선택 취소", key=f"unselect_loc_{i}"): | |
| st.session_state.current_location = "" | |
| st.rerun() | |
| else: | |
| if st.button(location, key=f"start_loc_{i}", use_container_width=True): | |
| st.session_state.current_location = location | |
| st.session_state.master_message = f"{location}에서 모험을 시작합니다. 이제 캐릭터를 생성할 차례입니다." | |
| st.rerun() | |
| # 캐릭터 생성으로 이동 버튼 | |
| st.write("#### 캐릭터 생성") | |
| st.write("세계를 충분히 탐색했다면, 이제 당신의 캐릭터를 만들어 모험을 시작할 수 있습니다.") | |
| # 선택된 시작 위치 없으면 경고 | |
| if not st.session_state.get('current_location'): | |
| st.warning("캐릭터 생성으로 진행하기 전에 시작 지점을 선택해주세요!") | |
| proceed_button = st.button("캐릭터 생성으로 진행", key="to_character_creation", | |
| use_container_width=True, disabled=True) | |
| else: | |
| proceed_button = st.button("캐릭터 생성으로 진행", key="to_character_creation", | |
| use_container_width=True) | |
| if proceed_button: | |
| st.session_state.stage = 'character_creation' | |
| st.session_state.master_message = "이제 이 세계에서 모험을 떠날 당신의 캐릭터를 만들어 볼까요?" | |
| st.rerun() |