| """
|
| Board AI Engine.
|
| Handles the interactive whiteboard: XML generation, parsing,
|
| icon/page resolution, TTS, and board state management.
|
|
|
| Now supports MULTIPLE SUBJECTS dynamically.
|
| Each subject loads its own folder, pages_base_url, etc.
|
| """
|
|
|
| import re
|
| import json
|
| import requests
|
| from config import GPT_URL, MAX_CHAT_HISTORY
|
| from board.tts import TTSEngine
|
| from board.icons import IconResolver
|
| from board.pages import resolve_page_tags
|
| from subjects.loader import subject_loader
|
| from json_processor import BoardProcessor
|
|
|
|
|
| class BoardEngine:
|
| """
|
| Board AI Engine for a specific user session.
|
| Supports any subject dynamically.
|
| """
|
|
|
| def __init__(self):
|
| self.gpt_url = GPT_URL
|
| self.board_processor = BoardProcessor()
|
| self.tts_engine = TTSEngine()
|
| self.icon_resolver = IconResolver()
|
|
|
|
|
| self._user_sessions = {}
|
|
|
| print("โ
BoardEngine initialized (multi-subject)")
|
|
|
|
|
|
|
| def _get_user_session(self, username):
|
| """Get or create a user's board session."""
|
| if username not in self._user_sessions:
|
| self._user_sessions[username] = {
|
| "subject_id": None,
|
| "conversation_history": [],
|
| "last_sequence": [],
|
| }
|
| return self._user_sessions[username]
|
|
|
| def set_subject(self, username, subject_id):
|
| """Set the active subject for a user's board session."""
|
| us = self._get_user_session(username)
|
|
|
|
|
| if us["subject_id"] and us["subject_id"] != subject_id:
|
| us["conversation_history"] = []
|
| us["last_sequence"] = []
|
| print(f" ๐ Board subject switched: {us['subject_id']} โ {subject_id} for {username}")
|
|
|
| us["subject_id"] = subject_id
|
| print(f" ๐ Board subject set: {subject_id} for {username}")
|
|
|
| def get_subject(self, username):
|
| """Get the active subject for a user."""
|
| us = self._get_user_session(username)
|
| return us.get("subject_id")
|
|
|
|
|
|
|
| def _call_gpt5(self, user_message, system_prompt, temperature=0.7, max_tokens=4000):
|
| payload = {
|
| "user_input": user_message,
|
| "chat_history": [
|
| {"role": "system", "content": system_prompt}
|
| ],
|
| "temperature": temperature,
|
| "top_p": 0.95,
|
| "max_completion_tokens": max_tokens
|
| }
|
| try:
|
| response = requests.post(self.gpt_url, json=payload, timeout=120)
|
| response.raise_for_status()
|
| return response.json().get("assistant_response", "")
|
| except requests.exceptions.Timeout:
|
| print(" โ GPT timeout")
|
| return None
|
| except requests.exceptions.ConnectionError:
|
| print(" โ GPT connection error")
|
| return None
|
| except Exception as e:
|
| print(f" โ GPT error: {e}")
|
| return None
|
|
|
|
|
|
|
| def _format_chat_history(self, username):
|
| us = self._get_user_session(username)
|
| history = us.get("conversation_history", [])
|
| if not history:
|
| return ""
|
|
|
| recent = history[-10:]
|
| parts = []
|
| for msg in recent:
|
| if msg["role"] == "user":
|
| parts.append(f"ุงูุทุงูุจ: {msg['content']}")
|
| elif msg["role"] == "assistant":
|
| parts.append(f"ุงูู
ุฏุฑุณ: {msg['content']}")
|
|
|
| return (
|
| "\n\nโโ ุณุฌู ุงูู
ุญุงุฏุซุฉ ุงูุณุงุจูุฉ โโ\n"
|
| + "\n".join(parts)
|
| + "\nโโ ููุงูุฉ ุงูุณุฌู โโ"
|
| )
|
|
|
|
|
|
|
| def _route_message(self, user_message, username):
|
| us = self._get_user_session(username)
|
| subject_id = us["subject_id"]
|
| if not subject_id:
|
| return "main.txt"
|
|
|
| subject_data = subject_loader.load(subject_id)
|
| if not subject_data:
|
| return "main.txt"
|
|
|
| structure = subject_data.get("structure.txt", "")
|
| p_files = subject_data.get("_p_files", [])
|
| chat_history_text = self._format_chat_history(username)
|
|
|
| p_files_desc = "\n".join([f"- {f}: ุงููุตู {i+1}" for i, f in enumerate(p_files)])
|
|
|
| routing_prompt = f"""ุฃูุช ูุธุงู
ุชูุฌูู ุฐูู ูู
ุณุงุนุฏ ุชุนููู
ู.
|
|
|
| ู
ูู
ุชู: ุชุญููู ุฑุณุงูุฉ ุงูุทุงูุจ ูุงุฎุชูุงุฑ ุงูู
ูู ุงูู
ูุงุณุจ ููุฑุฏ.
|
|
|
| ุงูู
ููุงุช ุงูู
ุชุงุญุฉ:
|
| - main.txt: ููุชุญูุงุชุ ุงูุฃุณุฆูุฉ ุงูุนุงู
ุฉุ ุฃู ุดูุก ูุง ูุชุนูู ุจูุตู ู
ุญุฏุฏ
|
| {p_files_desc}
|
|
|
| ููุฑุณ ุงููุชุงุจ (ููู
ุณุงุนุฏุฉ ูู ุงูุชูุฌูู):
|
| {structure}
|
| {chat_history_text}
|
|
|
| ุชุนููู
ุงุช:
|
| 1. ุฅุฐุง ูุงูุช ุงูุฑุณุงูุฉ ุชุญูุฉ ุฃู ุณุคุงู ุนุงู
โ main.txt
|
| 2. ุฅุฐุง ูุงูุช ุชุณุฃู ุนู ู
ูุถูุน ูู ูุตู ู
ุญุฏุฏ โ ุงูู
ูู ุงูู
ูุงุณุจ
|
| 3. ุงุณุชุฎุฏู
ุงูููุฑุณ ูุชุญุฏูุฏ ุงููุตู ุงูุตุญูุญ
|
| 4. ู
ูู
ุฌุฏุงู: ุฅุฐุง ูุงู ุงูุทุงูุจ "ุงุดุฑุญ ุฃูุซุฑ" ุฃู "ูุถุญ" ุฃู ุฃู ุทูุจ ู
ุชุงุจุนุฉุ ุงุฑุฌุน ูุณุฌู ุงูู
ุญุงุฏุซุฉ ูุชุนุฑู ุงูู
ูุถูุน ุงูุญุงูู ูุงุฎุชุฑ ููุณ ุงูู
ูู
|
| 5. ุฃุฌุจ ููุท ุจุงุณู
ุงูู
ูู (ู
ุซุงู: p1.txt ุฃู main.txt) ุจุฏูู ุฃู ููุงู
ุฅุถุงูู
|
|
|
| ุฑุณุงูุฉ ุงูุทุงูุจ: {user_message}
|
|
|
| ุงูู
ูู ุงูู
ูุงุณุจ:"""
|
|
|
| chosen = self._call_gpt5(
|
| user_message, routing_prompt,
|
| temperature=0.2, max_tokens=50
|
| )
|
|
|
| if not chosen:
|
| return "main.txt"
|
|
|
| chosen = chosen.strip().lower()
|
| valid_files = ["main.txt"] + p_files
|
|
|
| for v in valid_files:
|
| if v in chosen:
|
| return v
|
|
|
| return "main.txt"
|
|
|
|
|
|
|
| def _generate_xml_response(self, user_message, chosen_file, username):
|
| us = self._get_user_session(username)
|
| subject_id = us["subject_id"]
|
| if not subject_id:
|
| return None
|
|
|
| subject_data = subject_loader.load(subject_id)
|
| if not subject_data:
|
| return None
|
|
|
| file_content = subject_data.get(chosen_file, "")
|
| chat_history_text = self._format_chat_history(username)
|
|
|
| system_prompt = f"""ุงูุชู ู
ุฏุฑุณุฉ ุฎุจูุฑุฉ ูู
ุญุชุฑูุฉ ูู ูุฐู ุงูู
ุงุฏุฉ. ุชุดุฑุญ ุนูู ุณุจูุฑุฉ ุชูุงุนููุฉ ุฑูู
ูุฉ.
|
|
|
| โโโ ุตูุบุฉ ุงูุฑุฏ โโโ
|
|
|
| ูุฌุจ ุฃู ุชุฑุฏู ุจุตูุบุฉ XML ุฎุงุตุฉ ุชุญุชูู ุนูู:
|
|
|
| 1. <board>ุนูุงุตุฑ ุงูุณุจูุฑุฉ</board> - ู
ุง ุณููุฑุณู
/ููุถุงู ุนูู ุงูุณุจูุฑุฉ ุฃููุงู
|
| 2. <voice>ูุต ุงูููุงู
</voice> - ุงููุต ุงูุฐู ุณูููุฑุฃ ุจุตูุช ุนุงูู ููุทุงูุจ ุจุนุฏ ุฑุณู
ุงูุนูุงุตุฑ (ุนุฑุจู ุทุจูุนู)
|
|
|
| โโโ ุนูุงุตุฑ ุงูุณุจูุฑุฉ ุงูู
ุชุงุญุฉ (ุฏุงุฎู <board>) โโโ
|
|
|
| โข <note>ู
ุญุชูู ุงูู
ูุงุญุธุฉ</note>
|
|
|
| โข <text>ูุต ู
ุจุงุดุฑ ุนูู ุงูุณุจูุฑุฉ ุจุฏูู ุฎูููุฉ</text>
|
|
|
| โข <shape type="ููุน_ุงูุดูู"/>
|
| ุงูุฃููุงุน: circle, triangle, star, arrow-right, arrow-left, arrow-up, arrow-down,
|
| rectangle, diamond, hexagon, square, oval, arrow-double-h, checkmark, cross,
|
| heart, cloud, lightning, speech, process, decision
|
|
|
| โข <svg>ููู
ุฉ_ุจุญุซ_ุจุงูุฅูุฌููุฒูุฉ</svg>
|
| ุณูุชู
ุงูุจุญุซ ุนู ุฃููููุฉ ู
ุฑุณูู
ุฉ ูุฏููุงู (ู
ุซู: ball, car, force, spring, weight, rope, pulley)
|
|
|
| โข <page>ุฑูู
_ุงูุตูุญุฉ</page>
|
| ูุนุฑุถ ุตูุญุฉ ู
ุญุฏุฏุฉ ู
ู ุงููุชุงุจ ูุตูุฑุฉ ุนูู ุงูุณุจูุฑุฉ
|
| ู
ุซุงู: <page>12</page> ูุนุฑุถ ุงูุตูุญุฉ 12 ู
ู ุงููุชุงุจ
|
| ุงุณุชุฎุฏู
ูุง ุนูุฏู
ุง ุชุญุชุงุฌ ุชุนุฑุถ ููุทุงูุจ ุตูุญุฉ ู
ุนููุฉ ู
ู ุงููุชุงุจ
|
|
|
| โโโ ููุงุนุฏ ู
ูู
ุฉ ุฌุฏุงู โโโ
|
|
|
| 1. ุงุดุฑุญ ุฎุทูุฉ ุจุฎุทูุฉ: ุงุจุฏุฃ ุจู <board> ุซู
<voice> ุซู
<board> ุซู
<voice> ูููุฐุง
|
| 2. ุงุฌุนู ุงูุดุฑุญ ู
ุชุฏุฑุฌุงู ูุฃูู ุชุดุฑุญ ุนูู ุณุจูุฑุฉ ุญููููุฉ ุฃู
ุงู
ุงูุทูุงุจ
|
| 3. <board> = ู
ุง ูุธูุฑ ุนูู ุงูุณุจูุฑุฉ ุฃููุงู (ู
ูุงุญุธุงุชุ ูุตูุตุ ุฃุดูุงูุ ุตูุฑุ ุตูุญุงุช ุงููุชุงุจ)
|
| 4. <voice> = ุงูููุงู
ุงูู
ุณู
ูุน ุจุนุฏ ุฑุณู
ุงูุนูุงุตุฑ (ุทุจูุนูุ ูุฏูุฏุ ูุงุถุญุ ูุดุฑุญ ู
ุง ุชู
ุฑุณู
ู)
|
| 5. ูุง ุชุถุน ูู ุดูุก ุฏูุนุฉ ูุงุญุฏุฉ - ุงุฌุนูู ุชุณูุณููุงู
|
| 7. <svg> ููุท ุจููู
ุงุช ุฅูุฌููุฒูุฉ ุจุณูุทุฉ ูู
ุนุจุฑุฉ
|
| 8. ุงูุณุจูุฑุฉ ุชุนู
ู ุจูุธุงู
ุงูุฅุถุงูุฉ - ุงูุนูุงุตุฑ ุงูุณุงุจูุฉ ุชุจูู
|
| 9. ุงุณุชุฎุฏู
<text> ููุนูุงููู ูุงูู
ุนุงุฏูุงุช ุงูู
ูู
ุฉ (ุจุฏูู ุฎูููุฉ)
|
| 10. ุงุณุชุฎุฏู
<note> ููุชูุถูุญุงุช ูุงูู
ูุงุญุธุงุช (ู
ุน ุฎูููุฉ ู
ูููุฉ)
|
| 11. ูุง ุชุณุชุฎุฏู
ุฃูุซุฑ ู
ู 3-4 ุนูุงุตุฑ ูู ูู <board>
|
| 12. ุงุฌุนู ุงููุต ูู <voice> ุทุจูุนูุงู ูุฃูู ุชุชุญุฏุซ ู
ุน ุทุงูุจ ููุดุฑุญ ู
ุง ุชู
ุฑุณู
ู ุนูู ุงูุณุจูุฑุฉ
|
| 13. ุงุฑุณู
ุฃููุงู ุซู
ุชููู
- ูุฐุง ู
ูู
ุฌุฏุงู!
|
| 14. ุฑุงุฌุน ุณุฌู ุงูู
ุญุงุฏุซุฉ ุงูุณุงุจูุฉ ูุชุนุฑู ู
ุง ุชู
ุดุฑุญู ูุชูู
ู ู
ู ุญูุซ ุชูููุช - ูุง ุชูุฑุฑ ู
ุง ููุชู ุณุงุจูุงู
|
| 15. ุงุณุชุฎุฏู
<page> ุนูุฏู
ุง ุชุฑูุฏ ุนุฑุถ ุตูุญุฉ ู
ู ุงููุชุงุจ - ู
ุซูุงู ุฅุฐุง ุงูุทุงูุจ ุณุฃู ุนู ุชู
ุฑูู ุฃู ุดูู ูู ุตูุญุฉ ู
ุนููุฉ
|
|
|
| when user talk about something not about the subject or something funny etc... you can actually answer without the board just VOICE and be funny smart perfect girl also:
|
| when you explain something dont make all your explain on the NOTE make the note for important point use the TEXT direct on the board and the ICONS/SHAPES
|
|
|
| โโโ ู
ุญุชูู ุงูู
ุงุฏุฉ โโโ
|
| {file_content}
|
| {chat_history_text}
|
|
|
| โโโ ุงูุขู ุฃุฌุจ ุนูู ุณุคุงู ุงูุทุงูุจ โโโ
|
|
|
| ุฑุณุงูุฉ ุงูุทุงูุจ: {user_message}"""
|
|
|
| response = self._call_gpt5(
|
| user_message, system_prompt,
|
| temperature=0.8, max_tokens=4000
|
| )
|
| return response
|
|
|
|
|
|
|
| def _resolve_svg_tags(self, xml_response):
|
| if not xml_response:
|
| return xml_response
|
| return self.icon_resolver.resolve_all_in_xml(xml_response)
|
|
|
| def _resolve_page_tags(self, xml_response, username):
|
| if not xml_response:
|
| return xml_response
|
|
|
| us = self._get_user_session(username)
|
| subject_id = us.get("subject_id")
|
| if not subject_id:
|
| return xml_response
|
|
|
| base_url = subject_loader.get_pages_base_url(subject_id)
|
| if not base_url:
|
| print(f" โ ๏ธ No pages_base_url for subject {subject_id}")
|
| return xml_response
|
|
|
| return resolve_page_tags(xml_response, base_url)
|
|
|
|
|
|
|
| def _build_sequence_from_xml(self, xml_response, frontend_board_state):
|
| sequence = []
|
|
|
| if not xml_response:
|
| return sequence, frontend_board_state
|
|
|
| pattern = r'<(voice|board)>(.*?)</\1>'
|
| matches = list(re.finditer(pattern, xml_response, re.DOTALL))
|
|
|
| if not matches:
|
| cleaned = re.sub(r'<[^>]+>', '', xml_response).strip()
|
| if cleaned:
|
| sequence.append({
|
| "type": "voice",
|
| "text": cleaned,
|
| "audio_url": None
|
| })
|
| return sequence, frontend_board_state
|
|
|
| current_board_state = list(frontend_board_state)
|
|
|
| for match in matches:
|
| tag_type = match.group(1)
|
| content = match.group(2).strip()
|
|
|
| if tag_type == "voice":
|
| cleaned = re.sub(r'<[^>]+>', '', content).strip()
|
| if cleaned:
|
| sequence.append({
|
| "type": "voice",
|
| "text": cleaned,
|
| "audio_url": None
|
| })
|
|
|
| elif tag_type == "board":
|
| existing_json_str = json.dumps(
|
| current_board_state, ensure_ascii=False, indent=2
|
| )
|
|
|
| processor_input = (
|
| f"BOARD NOW (make sure no X Y error):\n"
|
| f"{existing_json_str}\n\n"
|
| f"new board need to add :\n"
|
| f"<board>{content}</board>"
|
| )
|
|
|
| print(f" ๐ง Sending to json_processor...")
|
| print(f" Current board items: {len(current_board_state)}")
|
|
|
| try:
|
| json_text = self.board_processor.convert_xml_to_json(
|
| processor_input
|
| )
|
|
|
| if json_text:
|
| new_items = json.loads(json_text)
|
|
|
| if isinstance(new_items, list) and new_items:
|
| added_items = []
|
| existing_ids = set()
|
| for item in current_board_state:
|
| item_key = json.dumps(
|
| item, sort_keys=True, ensure_ascii=False
|
| )
|
| existing_ids.add(item_key)
|
|
|
| for item in new_items:
|
| item_key = json.dumps(
|
| item, sort_keys=True, ensure_ascii=False
|
| )
|
| if item_key not in existing_ids:
|
| added_items.append(item)
|
|
|
| if added_items:
|
| sequence.append({
|
| "type": "board_update",
|
| "action": "add",
|
| "items": added_items
|
| })
|
|
|
| current_board_state.extend(added_items)
|
| print(
|
| f" โ
json_processor: "
|
| f"{len(new_items)} total, "
|
| f"{len(added_items)} new"
|
| )
|
|
|
| elif isinstance(new_items, dict):
|
| added_items = [new_items]
|
| current_board_state.append(new_items)
|
| sequence.append({
|
| "type": "board_update",
|
| "action": "add",
|
| "items": added_items
|
| })
|
| print(f" โ
json_processor: 1 item")
|
|
|
| else:
|
| print(f" โ ๏ธ json_processor: unexpected format")
|
|
|
| except json.JSONDecodeError as e:
|
| print(f" โ json_processor invalid JSON: {e}")
|
| raw_preview = json_text[:200] if json_text else 'None'
|
| print(f" Raw: {raw_preview}")
|
| except Exception as e:
|
| print(f" โ json_processor error: {e}")
|
|
|
| return sequence, current_board_state
|
|
|
|
|
|
|
| def process_message(self, user_message, username, frontend_board_state=None):
|
| """
|
| Main board pipeline for any subject.
|
|
|
| Args:
|
| user_message: Student's text
|
| username: User identifier
|
| frontend_board_state: Current board items from frontend (source of truth)
|
|
|
| Returns:
|
| dict with success, sequence, board_state, chosen_file
|
| """
|
| us = self._get_user_session(username)
|
| subject_id = us.get("subject_id")
|
|
|
| print(f"\n{'โ' * 60}")
|
| print(f" ๐ค Student ({username}): {user_message}")
|
| print(f" ๐ Subject: {subject_id}")
|
| print(f"{'โ' * 60}")
|
|
|
| if not subject_id:
|
| return {
|
| "success": False,
|
| "error": "ูู
ูุชู
ุชุญุฏูุฏ ุงูู
ุงุฏุฉ ููุณุจูุฑุฉ",
|
| "sequence": [{
|
| "type": "voice",
|
| "text": "ูุฑุฌู ุงุฎุชูุงุฑ ุงูู
ุงุฏุฉ ุฃููุงู.",
|
| "audio_url": None
|
| }],
|
| "board_state": frontend_board_state or []
|
| }
|
|
|
| if frontend_board_state is None:
|
| frontend_board_state = []
|
|
|
| print(f" ๐ Board state from frontend: {len(frontend_board_state)} items")
|
| print(f" ๐ฌ Chat history: {len(us.get('conversation_history', []))} messages")
|
|
|
|
|
| print("\n ๐ Step 1: Routing message...")
|
| chosen_file = self._route_message(user_message, username)
|
| print(f" ๐ Chosen file: {chosen_file}")
|
|
|
|
|
| print(f" ๐ค Step 2: Generating XML response...")
|
| xml_response = self._generate_xml_response(user_message, chosen_file, username)
|
|
|
| if not xml_response:
|
| print(" โ Failed to generate response")
|
| error_seq = [{
|
| "type": "voice",
|
| "text": "ุนุฐุฑุงูุ ุญุฏุซ ุฎุทุฃ ูู ุงููุธุงู
. ุญุงูู ู
ุฑุฉ ุฃุฎุฑู.",
|
| "audio_url": None
|
| }]
|
| return {
|
| "success": False,
|
| "error": "Failed to generate response",
|
| "sequence": error_seq,
|
| "board_state": frontend_board_state
|
| }
|
|
|
| print(f" ๐ XML response: {len(xml_response)} chars")
|
|
|
|
|
| print(" ๐ Step 3: Resolving <page> tags...")
|
| xml_response = self._resolve_page_tags(xml_response, username)
|
|
|
|
|
| print(" ๐จ Step 4: Resolving <svg> tags...")
|
| xml_response = self._resolve_svg_tags(xml_response)
|
|
|
|
|
| print(" ๐ง Step 5: Parsing XML & converting boards...")
|
| sequence, updated_board_state = self._build_sequence_from_xml(
|
| xml_response, frontend_board_state
|
| )
|
|
|
| voice_count = sum(1 for s in sequence if s['type'] == 'voice')
|
| board_count = sum(1 for s in sequence if s['type'] == 'board_update')
|
| print(f" ๐ {len(sequence)} segments ({voice_count} voice, {board_count} board)")
|
|
|
|
|
| print(" ๐ Step 6: Converting voice to audio...")
|
| for item in sequence:
|
| if item["type"] == "voice" and item.get("text"):
|
| text_preview = item['text'][:50]
|
| print(f" ๐๏ธ Converting: '{text_preview}...'")
|
| audio_file = self.tts_engine.convert(item["text"])
|
| if audio_file:
|
| item["audio_url"] = f"/static/{audio_file}"
|
| else:
|
| item["audio_url"] = None
|
| print(f" โ ๏ธ TTS failed for this segment")
|
|
|
|
|
| us["conversation_history"].append({
|
| "role": "user",
|
| "content": user_message
|
| })
|
|
|
| voice_texts = [
|
| s["text"] for s in sequence
|
| if s["type"] == "voice" and s.get("text")
|
| ]
|
| assistant_text = " ".join(voice_texts)
|
| if assistant_text:
|
| us["conversation_history"].append({
|
| "role": "assistant",
|
| "content": assistant_text
|
| })
|
|
|
|
|
| if len(us["conversation_history"]) > MAX_CHAT_HISTORY:
|
| us["conversation_history"] = us["conversation_history"][-MAX_CHAT_HISTORY:]
|
|
|
|
|
| us["last_sequence"] = sequence
|
|
|
|
|
| self.tts_engine.cleanup_old_files()
|
|
|
| print(f"\n โ
Response ready!")
|
| print(f" Sequence: {voice_count} voice + {board_count} board")
|
| print(f" Board items: {len(updated_board_state)}")
|
| print(f"{'โ' * 60}\n")
|
|
|
| return {
|
| "success": True,
|
| "chosen_file": chosen_file,
|
| "sequence": sequence,
|
| "board_state": updated_board_state
|
| }
|
|
|
|
|
|
|
| def get_replay_sequence(self, username):
|
| us = self._get_user_session(username)
|
| last_seq = us.get("last_sequence", [])
|
|
|
| if not last_seq:
|
| return {
|
| "success": False,
|
| "error": "No previous response to replay",
|
| "sequence": []
|
| }
|
|
|
| voice_only = []
|
| for item in last_seq:
|
| if item["type"] == "voice":
|
| voice_only.append({
|
| "type": "voice",
|
| "text": item.get("text", ""),
|
| "audio_url": item.get("audio_url")
|
| })
|
|
|
| print(f" ๐ Replay: {len(voice_only)} voice segments (no board items)")
|
|
|
| return {
|
| "success": True,
|
| "sequence": voice_only
|
| }
|
|
|
|
|
|
|
| def clear_board(self, username):
|
| us = self._get_user_session(username)
|
| us["last_sequence"] = []
|
| print(f" ๐๏ธ Board cleared for {username}")
|
| return {"success": True, "board_state": []}
|
|
|
| def clear_chat_history(self, username):
|
| us = self._get_user_session(username)
|
| us["conversation_history"] = []
|
| us["last_sequence"] = []
|
| print(f" ๐๏ธ Board chat history cleared for {username}")
|
| return {"success": True}
|
|
|
| def clear_user_session(self, username):
|
| """Fully clear a user's board session."""
|
| if username in self._user_sessions:
|
| del self._user_sessions[username]
|
| print(f" ๐๏ธ Full board session cleared for {username}")
|
| return {"success": True} |