mari-chat-3 / components_dog_assistant.py
sirochild's picture
Upload 2 files
8fb43ea verified
raw
history blame
20 kB
"""
ポチ(犬)アシスタントコンポーネント
画面右下に固定配置され、本音表示機能を提供する
"""
import streamlit as st
import logging
import time
logger = logging.getLogger(__name__)
class DogAssistant:
"""ポチ(犬)アシスタントクラス"""
def __init__(self):
"""初期化"""
self.default_message = "ポチは麻理の本音を察知したようだ・・・"
self.active_message = "ワンワン!本音が見えてるワン!"
def render_dog_component(self, tutorial_manager=None):
"""画面右下に固定配置される犬のコンポーネントを描画"""
try:
# 現在の状態を取得
current_show_all_hidden = st.session_state.get('show_all_hidden', False)
# 犬のコンポーネントのCSS(レスポンシブ対応)
dog_css = """
<style>
.dog-assistant-container {
position: fixed;
bottom: 120px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: flex-start;
pointer-events: none;
transform: translateX(-50px);
}
.dog-speech-bubble {
background-color: rgba(255, 255, 255, 0.95);
color: #333;
padding: 10px 15px;
border-radius: 20px;
font-size: 13px;
margin-bottom: 10px;
position: relative;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
backdrop-filter: blur(10px);
border: 1px solid rgba(0,0,0,0.1);
max-width: 200px;
word-wrap: break-word;
animation: bubbleFloat 3s ease-in-out infinite;
pointer-events: auto;
}
.dog-speech-bubble::after {
content: '';
position: absolute;
bottom: -8px;
left: 20px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid rgba(255, 255, 255, 0.95);
}
.dog-button {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%);
border: none;
border-radius: 50%;
width: 70px;
height: 70px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(255, 154, 158, 0.4);
display: flex;
align-items: center;
justify-content: center;
font-size: 35px;
pointer-events: auto;
animation: dogBounce 2s ease-in-out infinite;
}
.dog-button:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(255, 154, 158, 0.6);
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 50%, #ff9ff3 100%);
}
.dog-button:active {
transform: scale(0.95);
}
.dog-button.active {
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
animation: dogActive 1s ease-in-out infinite;
}
@keyframes bubbleFloat {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
}
@keyframes dogBounce {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-3px); }
}
@keyframes dogActive {
0%, 100% { transform: scale(1) rotate(0deg); }
25% { transform: scale(1.05) rotate(-2deg); }
75% { transform: scale(1.05) rotate(2deg); }
}
/* レスポンシブ対応 */
@media (max-width: 768px) {
.dog-assistant-container {
bottom: 110px;
right: 15px;
transform: translateX(-40px);
}
.dog-speech-bubble {
max-width: 150px;
font-size: 12px;
padding: 8px 12px;
}
.dog-button {
width: 60px;
height: 60px;
font-size: 30px;
}
}
@media (max-width: 480px) {
.dog-assistant-container {
bottom: 100px;
right: 10px;
transform: translateX(-30px);
}
.dog-speech-bubble {
max-width: 120px;
font-size: 11px;
padding: 6px 10px;
}
.dog-button {
width: 50px;
height: 50px;
font-size: 25px;
}
}
/* 画面が非常に小さい場合は吹き出しを非表示 */
@media (max-width: 320px) {
.dog-speech-bubble {
display: none;
}
}
</style>
"""
# 現在の状態を使用(関数開始時に取得済み)
is_active = current_show_all_hidden
bubble_text = self.active_message if is_active else self.default_message
button_class = "dog-button active" if is_active else "dog-button"
# JavaScriptでクリックイベントを処理
dog_js = f"""
<script>
function toggleDogMode() {{
// Streamlitのセッション状態を更新するためのトリガー
const event = new CustomEvent('dogButtonClick', {{
detail: {{ active: {str(is_active).lower()} }}
}});
window.dispatchEvent(event);
// ボタンの状態を即座に更新
const button = document.querySelector('.dog-button');
const bubble = document.querySelector('.dog-speech-bubble');
if (button && bubble) {{
if ({str(is_active).lower()}) {{
button.classList.remove('active');
bubble.textContent = '{self.default_message}';
}} else {{
button.classList.add('active');
bubble.textContent = '{self.active_message}';
}}
}}
}}
// ページ読み込み時にイベントリスナーを設定
document.addEventListener('DOMContentLoaded', function() {{
const button = document.querySelector('.dog-button');
if (button) {{
button.addEventListener('click', toggleDogMode);
}}
}});
// Streamlitの再描画後にもイベントリスナーを再設定
setTimeout(function() {{
const button = document.querySelector('.dog-button');
if (button) {{
button.addEventListener('click', toggleDogMode);
}}
}}, 100);
</script>
"""
# HTMLコンポーネント
dog_html = f"""
<div class="dog-assistant-container">
<div class="dog-speech-bubble">
{bubble_text}
</div>
<button class="{button_class}" title="ポチが麻理の本音を察知します" onclick="toggleDogMode()">
🐕
</button>
</div>
"""
# HTMLコンポーネント(ボタン以外)を表示
dog_display_html = f"""
<div class="dog-assistant-container">
<div class="dog-speech-bubble">
{bubble_text}
</div>
<div style="width: 70px; height: 70px; display: flex; align-items: center; justify-content: center;">
<!-- Streamlitボタンがここに配置される -->
</div>
</div>
"""
st.markdown(dog_css + dog_display_html, unsafe_allow_html=True)
# Streamlitボタンを固定位置に配置
button_css = """
<style>
.dog-button-overlay {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1001;
pointer-events: auto;
}
.dog-button-overlay .stButton > button {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
border: none;
border-radius: 50%;
width: 70px;
height: 70px;
font-size: 35px;
color: white;
box-shadow: 0 4px 15px rgba(255, 154, 158, 0.4);
transition: all 0.3s ease;
animation: dogBounce 2s ease-in-out infinite;
}
.dog-button-overlay .stButton > button:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(255, 154, 158, 0.6);
}
@keyframes dogBounce {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-3px); }
}
@media (max-width: 768px) {
.dog-button-overlay {
bottom: 15px;
right: 15px;
}
.dog-button-overlay .stButton > button {
width: 60px;
height: 60px;
font-size: 30px;
}
}
@media (max-width: 480px) {
.dog-button-overlay {
bottom: 10px;
right: 10px;
}
.dog-button-overlay .stButton > button {
width: 50px;
height: 50px;
font-size: 25px;
}
}
</style>
"""
st.markdown(button_css, unsafe_allow_html=True)
st.markdown('<div class="dog-button-overlay">', unsafe_allow_html=True)
# ボタンクリック処理(キーを固定して状態変更の問題を解決)
button_key = "dog_fixed_button" # 固定キー
button_help = "本音を隠す" if is_active else "本音を見る"
# ボタンの状態を視覚的に反映するためのスタイル
button_style = """
<style>
div[data-testid="stButton"] > button[kind="primary"] {
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%) !important;
animation: dogActive 1s ease-in-out infinite !important;
}
</style>
""" if is_active else ""
if button_style:
st.markdown(button_style, unsafe_allow_html=True)
if st.button("🐕", key=button_key, help=button_help, type="primary" if is_active else "secondary"):
self.handle_dog_button_click(tutorial_manager)
logger.info("右下の犬のボタンがクリックされました")
st.markdown('</div>', unsafe_allow_html=True)
logger.debug(f"犬のコンポーネントを描画しました (active: {is_active})")
except Exception as e:
logger.error(f"犬のコンポーネント描画エラー: {e}")
def handle_dog_button_click(self, tutorial_manager=None):
"""犬のボタンクリック処理(1回クリック対応版)"""
try:
# 重複クリック防止(短時間での連続クリックを防ぐ)
current_time = time.time()
last_click_time = st.session_state.get('dog_button_last_click', 0)
if current_time - last_click_time < 0.5: # 0.5秒以内の連続クリックを防ぐ
logger.debug("犬のボタン連続クリックを防止しました")
return
st.session_state.dog_button_last_click = current_time
# 本音表示機能のトリガー
if 'show_all_hidden' not in st.session_state:
st.session_state.show_all_hidden = False
# 現在の状態を確実に取得
current_state = st.session_state.get('show_all_hidden', False)
new_state = not current_state
# 状態を即座に更新(複数のフラグを同時に設定)
st.session_state.show_all_hidden = new_state
st.session_state.show_all_hidden_changed = True # チャット履歴の強制再表示フラグ
st.session_state.dog_button_clicked = True # UI更新フラグ
st.session_state.force_chat_rerender = True # 強制再描画フラグ
logger.info(f"🐕 ポチボタンクリック: {current_state}{new_state}")
# 全メッセージのフリップ状態を即座に更新
if 'message_flip_states' not in st.session_state:
st.session_state.message_flip_states = {}
# 現在のメッセージに対してフリップ状態を設定
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):
logger.warning("犬のボタン押下時に初期メッセージが見つかりません - 復元します")
initial_message = {"role": "assistant", "content": "何の用?遊びに来たの?", "is_initial": True}
st.session_state.chat['messages'].insert(0, initial_message)
logger.info("犬のボタン押下時に初期メッセージを復元しました")
# 全てのアシスタントメッセージのフリップ状態を更新
for i, message in enumerate(st.session_state.chat['messages']):
if message['role'] == 'assistant':
message_id = f"msg_{i}"
st.session_state.message_flip_states[message_id] = new_state
logger.debug(f"メッセージ {message_id} のフリップ状態を {new_state} に設定")
else:
logger.warning("犬のボタン押下時にチャットセッションが存在しません - 初期化します")
# チャットセッションが存在しない場合は初期化
initial_message = {"role": "assistant", "content": "何の用?遊びに来たの?", "is_initial": True}
if 'chat' not in st.session_state:
st.session_state.chat = {
"messages": [initial_message],
"affection": 30,
"scene_params": {"theme": "default"},
"limiter_state": {},
"scene_change_pending": None,
"ura_mode": False
}
else:
st.session_state.chat['messages'] = [initial_message]
logger.info("犬のボタン押下時にチャットセッションを初期化しました")
# チュートリアルステップ2を完了(tutorial_managerが渡された場合)
if tutorial_manager:
tutorial_manager.check_step_completion(2, True)
# 通知メッセージ(一度だけ表示)
if new_state:
st.success("🐕 ポチが麻理の本音を察知しました!")
else:
st.info("🐕 ポチが通常モードに戻りました。")
logger.info(f"犬のボタン状態変更完了: {current_state}{new_state}")
# 状態変更を即座に反映するため再描画を実行
st.rerun()
except Exception as e:
logger.error(f"犬のボタンクリック処理エラー: {e}")
import traceback
logger.error(f"犬のボタンクリック処理エラーの詳細: {traceback.format_exc()}")
def render_with_streamlit_button(self):
"""Streamlitのボタンを使用した代替実装(フォールバック用)"""
try:
# 固定位置のCSS
fallback_css = """
<style>
.dog-fallback-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
background: rgba(255, 255, 255, 0.9);
border-radius: 15px;
padding: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
backdrop-filter: blur(10px);
}
@media (max-width: 768px) {
.dog-fallback-container {
bottom: 15px;
right: 15px;
padding: 8px;
}
}
</style>
"""
st.markdown(fallback_css, unsafe_allow_html=True)
# コンテナの開始
st.markdown('<div class="dog-fallback-container">', unsafe_allow_html=True)
# 状態表示
is_active = st.session_state.get('show_all_hidden', False)
status_text = "本音モード中" if is_active else "通常モード"
st.caption(f"🐕 {status_text}")
# ボタン
button_text = "🔄 戻す" if is_active else "🐕 本音を見る"
if st.button(button_text, key="dog_assistant_btn"):
# チュートリアルマネージャーを取得(可能な場合)
tutorial_manager = None
try:
# セッション状態からチュートリアルマネージャーを取得する試み
# (完全ではないが、フォールバック用)
pass
except:
pass
self.handle_dog_button_click(tutorial_manager)
# コンテナの終了
st.markdown('</div>', unsafe_allow_html=True)
except Exception as e:
logger.error(f"犬のフォールバック描画エラー: {e}")
def get_current_state(self):
"""現在の犬の状態を取得"""
return {
'is_active': st.session_state.get('show_all_hidden', False),
'message': self.active_message if st.session_state.get('show_all_hidden', False) else self.default_message
}