import os import re import streamlit as st import streamlit.components.v1 as components from cert_study_app.config import DEFAULT_USER, ensure_runtime_dirs from cert_study_app.db import SessionLocal from cert_study_app.services.quiz_service import QuizService from cert_study_app.services.learning_lab_service import ( active_tracks, certification_for_track, certifications_for_track, normalize_track_id, ) from cert_study_app.services.learning_progress_service import ( load_completed_items, load_wrong_notes, preferred_track, save_preferred_track, ) DEFAULT_VISUAL_MODEL = os.getenv("OLLAMA_VISUAL_MODEL", "qwen3-vl:8b-instruct-q4_K_M") DEFAULT_MAIN_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:14b") DEFAULT_FAST_MODEL = os.getenv("OLLAMA_FAST_MODEL", "qwen3.5:9b") DEFAULT_DEEP_MODEL = os.getenv("OLLAMA_DEEP_MODEL", "").strip() DEFAULT_EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "BAAI/bge-m3") EMBEDDING_MODEL_OPTIONS = [ "BAAI/bge-m3", "intfloat/multilingual-e5-large", "sentence-transformers/paraphrase-multilingual-mpnet-base-v2", "sentence-transformers/all-MiniLM-L6-v2", ] def go_to(page: str): st.session_state.page = page st.rerun() def get_service(): db = SessionLocal() return db, QuizService(db) def init_state(): st.session_state.setdefault("question_id", None) st.session_state.setdefault("selected", None) st.session_state.setdefault("last_result", None) st.session_state.setdefault("exam_source", None) st.session_state.setdefault("page", "홈") st.session_state.setdefault("weak_type", None) st.session_state.setdefault("similar_type", None) st.session_state.setdefault("review_question_id", None) st.session_state.setdefault("quiz_order_mode", "순서대로") st.session_state.setdefault("lab_track", normalize_track_id(preferred_track())) st.session_state.setdefault("lab_lesson_index", 0) st.session_state.setdefault("lab_quiz_index", 0) st.session_state.setdefault("lab_practice_index", 0) if "lab_completed_lessons" not in st.session_state: lessons, quizzes, practices = load_completed_items() st.session_state.lab_completed_lessons = lessons st.session_state.lab_completed_quizzes = quizzes st.session_state.lab_completed_practices = practices if "lab_wrong_notes" not in st.session_state: st.session_state.lab_wrong_notes = load_wrong_notes() st.session_state.setdefault("quiz_skill_category", "전체") st.session_state.setdefault("quiz_skill_subcategory", "전체") st.session_state.setdefault("lab_lesson_just_completed", None) def inject_pwa_assets(): components.html( """ """, height=0, width=0, ) def apply_mobile_styles(): st.markdown( """ """, unsafe_allow_html=True, ) def slugify(value: str) -> str: slug = re.sub(r"[^0-9A-Za-z가-힣._-]+", "_", value.strip()) return slug.strip("._-") or "exam" def get_exams(): db, service = get_service() try: return service.list_exams() finally: db.close() def render_exam_selector(exams): sources = [exam["source"] for exam in exams] labels = ["전체 문제"] + [ f"{exam['name']} ({exam['count']}문항)" for exam in exams ] current = st.session_state.exam_source index = sources.index(current) + 1 if current in sources else 0 selected_label = st.selectbox("시험", labels, index=index) selected_source = None if selected_label == "전체 문제" else sources[labels.index(selected_label) - 1] if selected_source != st.session_state.exam_source: st.session_state.exam_source = selected_source st.session_state.question_id = None st.session_state.selected = None st.session_state.last_result = None selected_exam = next((exam for exam in exams if exam["source"] == selected_source), None) return selected_exam, selected_source def render_top_bar(): title_col, menu_col = st.columns([0.74, 0.26], vertical_alignment="center") title_col.title("Cert Study Lab") with menu_col.popover("메뉴", use_container_width=True): st.caption("학습 모드") menu_items = [ ("📖 개념 공부", "개념공부"), ("🖥 실습", "실습"), ("📋 시험 준비", "시험준비"), ("오답노트", "오답노트"), ("학습 현황", "대시보드"), ] for label, page in menu_items: if st.button(label, use_container_width=True, key=f"menu_study_{page}"): go_to(page) st.divider() st.caption("관리") admin_items = [ ("콘텐츠 관리", "콘텐츠 관리"), ("PDF 업로드", "PDF 업로드"), ("처리 현황", "처리 현황"), ("시험 현황", "시험 현황"), ("AI 색인", "AI 색인"), ] for label, page in admin_items: if st.button(label, use_container_width=True, key=f"menu_admin_{page}"): go_to(page) def render_back_home(): if st.button("처음으로", use_container_width=True): go_to("홈") def track_for_question_source(source): normalized = (source or "").strip().lower() if normalized.startswith("az-104") or "azure" in normalized: return "azure" if "linux" in normalized or "lfcs" in normalized: return "linux" return normalize_track_id(st.session_state.get("lab_track", "linux")) def selected_lab_track() -> str: tracks = active_tracks() labels = [] for track in tracks: certification_names = " / ".join( certification["name"] for certification in certifications_for_track(track["id"]) ) labels.append(f"{track['name']} · {certification_names or '미정'}") ids = [track["id"] for track in tracks] current = normalize_track_id(st.session_state.get("lab_track", "linux")) index = ids.index(current) if current in ids else 0 selected_label = st.selectbox("Track", labels, index=index) track_id = ids[labels.index(selected_label)] if track_id != st.session_state.get("lab_track"): save_preferred_track(track_id) st.session_state.lab_track = track_id return track_id