""" チュートリアルコンポーネント 初回ユーザー向けのガイド機能を提供する """ import streamlit as st import logging from typing import Dict, List, Optional logger = logging.getLogger(__name__) class TutorialManager: """チュートリアル管理クラス""" def __init__(self): """初期化""" self.tutorial_steps = { 1: { "title": "最初の一言を送ってみよう", "description": "画面下部の入力欄に「こんにちは」などの一言を入力して、麻理に話しかけてみましょう。", "icon": "💬", "target": "chat_input", "completed_key": "tutorial_step1_completed" }, 2: { "title": "本音を見てみよう(ポチ機能)", "description": "画面右下の犬アイコン「ポチ🐕」をクリックすると、麻理の本音が見えるようになります。", "icon": "🐕", "target": "dog_assistant", "completed_key": "tutorial_step2_completed" }, 3: { "title": "セーフティ機能を切り替えてみよう", "description": "左サイドバー上部の🔒ボタンをクリックすると、麻理の表現がより大胆になります。", "icon": "🔓", "target": "safety_button", "completed_key": "tutorial_step3_completed" }, 4: { "title": "手紙をリクエストしよう", "description": "「手紙を受け取る」タブから、麻理からの特別な手紙をリクエストできます。チュートリアル中は即座に短縮版の手紙が生成されます。", "icon": "✉️", "target": "letter_tab", "completed_key": "tutorial_step4_completed" }, 5: { "title": "麻理との関係性を育てよう", "description": "会話を重ねることで好感度が上がり、関係性のステージが進展します。", "icon": "💖", "target": "affection_display", "completed_key": "tutorial_step5_completed" }, 6: { "title": "風景が変わる会話をしてみよう", "description": "「カフェ」「神社」「美術館」などのキーワードを話すと、背景が動的に変わります。", "icon": "🎨", "target": "scene_change", "completed_key": "tutorial_step6_completed" } } def is_first_visit(self) -> bool: """初回訪問かどうかを判定""" return not st.session_state.get('tutorial_shown', False) def should_show_tutorial(self) -> bool: """チュートリアルを表示すべきかどうか""" # 初回訪問または明示的にチュートリアルが要求された場合 return (self.is_first_visit() or st.session_state.get('show_tutorial_requested', False)) def mark_tutorial_shown(self): """チュートリアル表示済みとしてマーク""" st.session_state.tutorial_shown = True st.session_state.show_tutorial_requested = False def request_tutorial(self): """チュートリアル表示を要求""" st.session_state.show_tutorial_requested = True def get_current_step(self) -> int: """現在のチュートリアルステップを取得""" for step_num in range(1, 7): if not st.session_state.get(self.tutorial_steps[step_num]['completed_key'], False): return step_num return 7 # 全ステップ完了 def complete_step(self, step_num: int): """ステップを完了としてマーク""" if step_num in self.tutorial_steps: st.session_state[self.tutorial_steps[step_num]['completed_key']] = True logger.info(f"チュートリアルステップ{step_num}が完了しました") def is_step_completed(self, step_num: int) -> bool: """ステップが完了しているかチェック""" if step_num in self.tutorial_steps: return st.session_state.get(self.tutorial_steps[step_num]['completed_key'], False) return False def render_welcome_dialog(self): """初回訪問時のウェルカムダイアログ""" if not self.is_first_visit(): return # ウェルカムダイアログのスタイル welcome_css = """ """ st.markdown(welcome_css, unsafe_allow_html=True) # ウェルカムメッセージ welcome_html = """
🐕 麻理チャットへようこそ!
感情豊かなアンドロイド「麻理」と対話しながら、
本音や関係性の変化を楽しめる新感覚のAIチャット体験です。

最初の数分で、麻理との距離が少しだけ縮まります。
""" st.markdown(welcome_html, unsafe_allow_html=True) # ボタンを2列で配置 col1, col2 = st.columns(2) with col1: if st.button("📘 チュートリアルを始める", type="primary", use_container_width=True, key="start_tutorial"): # 初期メッセージを即座に保護 if 'chat' in st.session_state and 'messages' in st.session_state.chat: messages = st.session_state.chat['messages'] if not any(msg.get('is_initial', False) for msg in messages): initial_message = {"role": "assistant", "content": "何の用?遊びに来たの?", "is_initial": True} st.session_state.chat['messages'].insert(0, initial_message) logger.info("チュートリアル開始ボタン押下時に初期メッセージを即座に復元") # チュートリアル開始フラグを設定 st.session_state.tutorial_start_requested = True st.session_state.tutorial_shown = True st.session_state.preserve_initial_message = True logger.info("チュートリアル開始 - 初期メッセージ保護フラグ設定") with col2: if st.button("⏭️ スキップして始める", type="secondary", use_container_width=True, key="skip_tutorial"): # 初期メッセージを即座に保護 if 'chat' in st.session_state and 'messages' in st.session_state.chat: messages = st.session_state.chat['messages'] if not any(msg.get('is_initial', False) for msg in messages): initial_message = {"role": "assistant", "content": "何の用?遊びに来たの?", "is_initial": True} st.session_state.chat['messages'].insert(0, initial_message) logger.info("チュートリアルスキップボタン押下時に初期メッセージを即座に復元") # チュートリアルをスキップして全ステップを完了扱いにする for step_num in range(1, 7): if step_num in self.tutorial_steps: st.session_state[self.tutorial_steps[step_num]['completed_key']] = True st.session_state.tutorial_shown = True st.session_state.tutorial_skip_requested = True st.session_state.preserve_initial_message = True logger.info("チュートリアルスキップ - 初期メッセージ保護フラグ設定") def render_tutorial_sidebar(self): """サイドバーのチュートリアル案内(簡素版)""" with st.sidebar: st.markdown("---") # チュートリアル進行状況 current_step = self.get_current_step() total_steps = len(self.tutorial_steps) if current_step <= total_steps: progress = (current_step - 1) / total_steps st.markdown("### 📘 チュートリアル進行") st.progress(progress) st.caption(f"ステップ {current_step - 1}/{total_steps} 完了") else: st.success("🎉 チュートリアル完了!") st.caption("麻理との会話を楽しんでください") # チュートリアル再表示ボタン if st.button("📘 チュートリアルを見る", use_container_width=True): self.request_tutorial() # st.rerun()を削除 - 状態変更により自動的に再描画される def render_chat_tutorial_guide(self): """チャットタブでのチュートリアル案内""" current_step = self.get_current_step() total_steps = len(self.tutorial_steps) # チュートリアル完了済みの場合は何も表示しない if current_step > total_steps: return # ステップ4が完了済みの場合(手紙タブに遷移済み)は表示しない if current_step == 4 and self.is_step_completed(4): return step_info = self.tutorial_steps[current_step] # ステップごとの案内スタイル guide_css = """ """ st.markdown(guide_css, unsafe_allow_html=True) # ステップごとの具体的な案内 action_text = self._get_step_action_text(current_step) guide_html = f"""
チュートリアル ステップ {current_step}/{total_steps}
{step_info['icon']} {step_info['title']}
{step_info['description']}
💡 {action_text}
""" st.markdown(guide_html, unsafe_allow_html=True) def _get_step_action_text(self, step_num: int) -> str: """ステップごとの具体的なアクション案内テキストを取得""" action_texts = { 1: "下のチャット入力欄に「こんにちは」と入力して送信してみてください。", 2: "画面右下に表示される犬のアイコン「ポチ🐕」をクリックしてみてください。", 3: "左サイドバーの一番上にある🔒ボタンをクリックして、セーフティ機能を切り替えてみてください。", 4: "画面上部の光っている「✉️ 手紙を受け取る」タブをクリックして、手紙をリクエストしてみてください。矢印が案内しています!", 5: "麻理ともっと会話して、左サイドバーの好感度の変化を確認してみてください。", 6: "「カフェに行きたい」「神社でお参りしたい」「美術館を見に行こう」などと話しかけて、背景の変化を楽しんでください。" } return action_texts.get(step_num, "次のステップに進んでください。") def render_step_highlight(self, step_num: int, target_element: str): """特定のステップのハイライト表示""" if self.get_current_step() != step_num: return step_info = self.tutorial_steps[step_num] highlight_css = f""" """ st.markdown(highlight_css, unsafe_allow_html=True) def render_tutorial_tab(self): """チュートリアル専用タブの内容""" st.markdown("# 📘 麻理チャット チュートリアル") st.markdown(""" **ようこそ、麻理チャットへ!** 感情豊かなアンドロイド「麻理」と対話しながら、本音や関係性の変化を楽しめる新感覚のAIチャット体験です。 このチュートリアルで、主要機能を順番に体験してみましょう。 """) # 進行状況表示 current_step = self.get_current_step() total_steps = len(self.tutorial_steps) col1, col2, col3 = st.columns([1, 2, 1]) with col2: progress = min((current_step - 1) / total_steps, 1.0) st.progress(progress) st.caption(f"進行状況: {min(current_step - 1, total_steps)}/{total_steps} ステップ完了") st.markdown("---") # 各ステップの表示 for step_num, step_info in self.tutorial_steps.items(): is_completed = self.is_step_completed(step_num) is_current = (current_step == step_num) # ステップのスタイル決定 if is_completed: status_icon = "✅" status_color = "#28a745" card_style = "background: rgba(40, 167, 69, 0.1); border-left: 4px solid #28a745;" elif is_current: status_icon = "👉" status_color = "#ff6b6b" card_style = "background: rgba(255, 107, 107, 0.1); border-left: 4px solid #ff6b6b;" else: status_icon = "⏳" status_color = "#6c757d" card_style = "background: rgba(108, 117, 125, 0.1); border-left: 4px solid #6c757d;" # ステップカード st.markdown(f"""

{status_icon} ステップ {step_num}: {step_info['icon']} {step_info['title']}

{step_info['description']}

""", unsafe_allow_html=True) # 現在のステップの場合、追加のガイダンス if is_current: if step_num == 1: st.info("💡 **ヒント**: 「麻理と話す」タブに移動して、画面下部の入力欄にメッセージを入力してみてください。") elif step_num == 2: st.info("💡 **ヒント**: 画面右下に表示される犬のアイコン「ポチ🐕」をクリックしてみてください。") elif step_num == 3: st.info("💡 **ヒント**: 左サイドバーの一番上にある🔒ボタンをクリックしてみてください。") elif step_num == 4: st.info("💡 **ヒント**: 画面上部の「手紙を受け取る」タブをクリックして、手紙をリクエストしてみてください。") elif step_num == 5: st.info("💡 **ヒント**: 左サイドバーの「ステータス」で好感度の変化を確認できます。") elif step_num == 6: st.info("💡 **ヒント**: 「カフェに行きたい」「神社でお参りしたい」などと話しかけてみてください。") # 完了時のメッセージ if current_step > total_steps: st.balloons() st.success(""" 🎉 **チュートリアル完了おめでとうございます!** これで麻理チャットの主要機能をすべて体験しました。 これからは自由に麻理との会話を楽しんでください。 何か分からないことがあれば、いつでもこのチュートリアルに戻ってきてくださいね。 """) def check_step_completion(self, step_num: int, condition_met: bool): """ステップ完了条件をチェック(順序制御付き)""" # 順序制御:現在のステップまたは次のステップのみ完了可能 current_step = self.get_current_step() # 現在のステップより先のステップは完了できない if step_num > current_step + 1: logger.debug(f"ステップ{step_num}は順序違反のためスキップ(現在ステップ: {current_step})") return # 既に完了済みのステップは再完了しない if self.is_step_completed(step_num): logger.debug(f"ステップ{step_num}は既に完了済み") return if condition_met: self.complete_step(step_num) logger.info(f"✅ チュートリアルステップ{step_num}完了!現在ステップ: {current_step}") # 完了通知(控えめに) step_info = self.tutorial_steps[step_num] # 次のステップの案内 next_step = step_num + 1 if next_step in self.tutorial_steps: next_info = self.tutorial_steps[next_step] st.success(f"✅ ステップ{step_num}完了!次は「{next_info['title']}」です。") else: # 全ステップ完了 st.balloons() st.success("🎉 チュートリアル完了!麻理との会話を存分にお楽しみください!") # ステップ4完了時に強調表示を解除するためのページ再読み込み # st.rerun()を削除 - 状態変更により自動的に再描画される def auto_check_completions(self): """自動的にステップ完了をチェック(順序制御強化版)""" current_step = self.get_current_step() # 現在のステップのみをチェック(先のステップは無視) if current_step == 1: # ステップ1: メッセージ送信 messages = st.session_state.get('chat', {}).get('messages', []) non_initial_messages = [msg for msg in messages if not msg.get('is_initial', False)] if len(non_initial_messages) > 0: # ユーザーが1回でもメッセージを送信した self.check_step_completion(1, True) elif current_step == 2: # ステップ2: ポチ機能使用 if st.session_state.get('show_all_hidden', False): self.check_step_completion(2, True) elif current_step == 3: # ステップ3: セーフティ機能使用 if st.session_state.get('chat', {}).get('ura_mode', False): self.check_step_completion(3, True) elif current_step == 4: # ステップ4: 手紙タブに到達(手紙タブでのみ完了判定) # auto_check_completionsでは判定しない(手紙タブで明示的に完了) pass elif current_step == 5: # ステップ5: 好感度変化(ステップ4完了後のみ) initial_affection = 30 current_affection = st.session_state.get('chat', {}).get('affection', initial_affection) if current_affection != initial_affection: self.check_step_completion(5, True) elif current_step == 6: # ステップ6: シーン変更(ステップ5完了後のみ) current_theme = st.session_state.get('chat', {}).get('scene_params', {}).get('theme', 'default') if current_theme != 'default': self.check_step_completion(6, True) def get_tutorial_status(self) -> Dict: """チュートリアルの状態情報を取得""" current_step = self.get_current_step() total_steps = len(self.tutorial_steps) completed_steps = sum(1 for i in range(1, total_steps + 1) if self.is_step_completed(i)) return { 'is_first_visit': self.is_first_visit(), 'current_step': current_step, 'total_steps': total_steps, 'completed_steps': completed_steps, 'progress_percentage': (completed_steps / total_steps) * 100, 'is_completed': current_step > total_steps }