ButterM40 commited on
Commit
c2db707
·
1 Parent(s): 8955d02

Deploy actual React-style frontend with FastAPI backend

Browse files
app.py CHANGED
@@ -1,9 +1,17 @@
1
- import gradio as gr
 
 
 
 
 
2
  import os
3
  import sys
4
- import asyncio
 
 
 
 
5
  import logging
6
- from typing import List, Tuple, Optional
7
 
8
  # Setup logging
9
  logging.basicConfig(level=logging.INFO)
@@ -13,238 +21,130 @@ logger = logging.getLogger(__name__)
13
  backend_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'backend')
14
  sys.path.insert(0, backend_path)
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  # Global character manager
17
  character_manager = None
18
- models_loaded = False
19
 
20
- def initialize_models():
21
- """Initialize the character manager"""
22
- global character_manager, models_loaded
23
-
24
- if models_loaded:
25
- return "✅ Models already loaded!"
26
-
27
  try:
28
- from backend.models.character_manager import CharacterManager
29
-
30
  character_manager = CharacterManager()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- # Initialize synchronously
33
- loop = asyncio.new_event_loop()
34
- asyncio.set_event_loop(loop)
35
- loop.run_until_complete(character_manager.initialize())
 
36
 
37
- models_loaded = True
38
- logger.info(" Character models initialized successfully!")
39
- return " Models loaded successfully!"
 
 
40
 
41
  except Exception as e:
42
- logger.error(f" Failed to initialize models: {e}")
43
- return f"❌ Failed to load models: {str(e)}"
44
-
45
- def get_character_info():
46
- """Get character information for display"""
47
- return {
48
- "moses": {
49
- "name": "Moses",
50
- "description": "📚 Wise biblical figure offering guidance and wisdom",
51
- "avatar": "👨‍🏫"
52
- },
53
- "samsung_employee": {
54
- "name": "Samsung Employee",
55
- "description": "💼 Professional tech support specialist",
56
- "avatar": "👨‍💼"
57
- },
58
- "jinx": {
59
- "name": "Jinx",
60
- "description": "🎭 Chaotic and energetic character from Arcane",
61
- "avatar": "🔮"
62
  }
63
- }
64
 
65
- def chat_with_character(message: str, character_id: str, history: List[Tuple[str, str]]) -> Tuple[List[Tuple[str, str]], str]:
66
- """Generate character response and update chat history"""
67
- global character_manager, models_loaded
68
-
69
- # Initialize if needed
70
- if not models_loaded:
71
- init_result = initialize_models()
72
- if "Failed" in init_result:
73
- return history + [(message, init_result)], ""
74
-
75
- if not message.strip():
76
- return history, ""
77
-
78
  try:
79
- if character_manager is None:
80
- return history + [(message, "❌ Character manager not initialized")], ""
81
-
82
- # Generate response
83
- response = character_manager.generate_response(
84
- character_id=character_id,
85
- user_input=message,
86
- max_length=512
87
- )
88
 
89
- # Update history
90
- new_history = history + [(message, response)]
91
- return new_history, ""
 
 
 
 
 
 
 
92
 
93
  except Exception as e:
94
- logger.error(f"Error generating response: {e}")
95
- error_response = f"❌ Error: {str(e)}"
96
- return history + [(message, error_response)], ""
97
-
98
- def get_character_display_html(character_id: str) -> str:
99
- """Generate HTML for character display"""
100
- char_info = get_character_info()
101
- if character_id not in char_info:
102
- return "<div>Character not found</div>"
103
-
104
- info = char_info[character_id]
105
- return f"""
106
- <div style="text-align: center; padding: 20px; background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
107
- color: white; border-radius: 15px; margin: 10px;">
108
- <div style="font-size: 4rem; margin-bottom: 10px;">{info['avatar']}</div>
109
- <h2 style="margin: 10px 0; color: white;">{info['name']}</h2>
110
- <p style="margin: 0; opacity: 0.9; font-size: 1.1rem;">{info['description']}</p>
111
- </div>
112
- """
113
 
114
- def create_interface():
115
- """Create the main Gradio interface"""
116
-
117
- # Custom CSS
118
- custom_css = """
119
- .gradio-container {
120
- max-width: 1200px !important;
121
- }
122
- .character-display {
123
- min-height: 200px;
124
- }
125
- .chat-container {
126
- height: 500px;
127
  }
128
- """
129
-
130
- with gr.Blocks(
131
- title="🎭 Roleplay Chat Box",
132
- theme=gr.themes.Soft(primary_hue="purple"),
133
- css=custom_css
134
- ) as demo:
135
-
136
- gr.Markdown("# 🎭 Roleplay Chat Box")
137
- gr.Markdown("### Chat with AI characters, each with unique personalities!")
138
-
139
- with gr.Row():
140
- # Character selection column
141
- with gr.Column(scale=1):
142
- gr.Markdown("## 👥 Choose Character")
143
-
144
- character_dropdown = gr.Dropdown(
145
- choices=[
146
- ("👨‍🏫 Moses", "moses"),
147
- ("👨‍💼 Samsung Employee", "samsung_employee"),
148
- ("🔮 Jinx", "jinx")
149
- ],
150
- value="moses",
151
- label="Select Character",
152
- interactive=True
153
- )
154
-
155
- # Character info display
156
- character_display = gr.HTML(
157
- value=get_character_display_html("moses"),
158
- elem_classes=["character-display"]
159
- )
160
-
161
- # Update character display when selection changes
162
- character_dropdown.change(
163
- fn=get_character_display_html,
164
- inputs=[character_dropdown],
165
- outputs=[character_display]
166
- )
167
-
168
- # Chat column
169
- with gr.Column(scale=2):
170
- gr.Markdown("## 💬 Chat")
171
-
172
- chatbot = gr.Chatbot(
173
- height=500,
174
- show_label=False,
175
- elem_classes=["chat-container"]
176
- )
177
-
178
- with gr.Row():
179
- msg_input = gr.Textbox(
180
- placeholder="Type your message here...",
181
- show_label=False,
182
- scale=4,
183
- lines=2
184
- )
185
-
186
- with gr.Column(scale=1):
187
- send_btn = gr.Button("Send 📨", variant="primary")
188
- clear_btn = gr.Button("Clear 🗑️")
189
-
190
- # Status section
191
- with gr.Row():
192
- status_display = gr.Textbox(
193
- value="Click 'Initialize Models' to start chatting!",
194
- label="Status",
195
- interactive=False
196
- )
197
- init_btn = gr.Button("Initialize Models 🚀", variant="secondary")
198
-
199
- # Event handlers
200
- def send_message(message, character_id, history):
201
- return chat_with_character(message, character_id, history)
202
-
203
- def clear_chat():
204
- return [], "Chat cleared!"
205
-
206
- def init_models_handler():
207
- return initialize_models()
208
-
209
- # Button clicks
210
- send_btn.click(
211
- fn=send_message,
212
- inputs=[msg_input, character_dropdown, chatbot],
213
- outputs=[chatbot, msg_input]
214
- )
215
-
216
- msg_input.submit(
217
- fn=send_message,
218
- inputs=[msg_input, character_dropdown, chatbot],
219
- outputs=[chatbot, msg_input]
220
- )
221
-
222
- clear_btn.click(
223
- fn=clear_chat,
224
- outputs=[chatbot, status_display]
225
- )
226
-
227
- init_btn.click(
228
- fn=init_models_handler,
229
- outputs=[status_display]
230
  )
231
-
232
- # Example interactions
233
- gr.Markdown("""
234
- ### 💡 Example Conversations
235
- - **Moses**: "What is the meaning of wisdom?"
236
- - **Samsung Employee**: "Tell me about the latest Samsung phones"
237
- - **Jinx**: "I need help with a creative project!"
238
- """)
239
-
240
- return demo
241
 
 
 
 
 
 
 
242
  if __name__ == "__main__":
243
- # Create and launch the interface
244
- demo = create_interface()
245
- demo.launch(
246
- server_name="0.0.0.0",
247
- server_port=7860,
248
- show_error=True,
249
- share=False
250
- )
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hugging Face Spaces deployment for Roleplay Chat Box
4
+ Serves the React-style frontend with FastAPI backend
5
+ """
6
+
7
  import os
8
  import sys
9
+ import uvicorn
10
+ from fastapi import FastAPI, Request, HTTPException
11
+ from fastapi.staticfiles import StaticFiles
12
+ from fastapi.responses import FileResponse, HTMLResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
  import logging
 
15
 
16
  # Setup logging
17
  logging.basicConfig(level=logging.INFO)
 
21
  backend_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'backend')
22
  sys.path.insert(0, backend_path)
23
 
24
+ # Import your existing REST server components
25
+ try:
26
+ from backend.models.character_manager import CharacterManager
27
+ from backend.config import settings
28
+ logger.info("✅ Backend modules imported successfully")
29
+ except ImportError as e:
30
+ logger.error(f"❌ Failed to import backend: {e}")
31
+ # Create a minimal fallback
32
+ class CharacterManager:
33
+ def __init__(self):
34
+ self.initialized = False
35
+ async def initialize(self):
36
+ self.initialized = True
37
+ async def get_response(self, character, message, history):
38
+ return f"[{character.upper()}]: I received your message: {message}"
39
+
40
+ # Create FastAPI app
41
+ app = FastAPI(title="Roleplay Chat Box", description="AI Roleplay Chat with Multiple Characters")
42
+
43
+ # Add CORS middleware
44
+ app.add_middleware(
45
+ CORSMiddleware,
46
+ allow_origins=["*"],
47
+ allow_credentials=True,
48
+ allow_methods=["*"],
49
+ allow_headers=["*"],
50
+ )
51
+
52
  # Global character manager
53
  character_manager = None
 
54
 
55
+ @app.on_event("startup")
56
+ async def startup_event():
57
+ """Initialize the character manager on startup"""
58
+ global character_manager
 
 
 
59
  try:
 
 
60
  character_manager = CharacterManager()
61
+ await character_manager.initialize()
62
+ logger.info("✅ Character manager initialized")
63
+ except Exception as e:
64
+ logger.error(f"❌ Failed to initialize character manager: {e}")
65
+ character_manager = CharacterManager() # Fallback
66
+
67
+ # API Routes (matching your existing REST API)
68
+ @app.post("/api/chat")
69
+ async def chat_endpoint(request: Request):
70
+ """Chat endpoint matching your original REST API"""
71
+ try:
72
+ data = await request.json()
73
+ character_id = data.get('character', 'moses')
74
+ message = data.get('message', '')
75
+ history = data.get('history', [])
76
 
77
+ if not character_manager:
78
+ raise HTTPException(status_code=500, detail="Character manager not initialized")
79
+
80
+ # Get response from character
81
+ response = await character_manager.get_response(character_id, message, history)
82
 
83
+ return {
84
+ "success": True,
85
+ "response": response,
86
+ "character": character_id
87
+ }
88
 
89
  except Exception as e:
90
+ logger.error(f"Chat error: {e}")
91
+ return {
92
+ "success": False,
93
+ "error": str(e),
94
+ "response": "I apologize, but I'm having trouble responding right now. Please try again."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  }
 
96
 
97
+ @app.post("/api/switch-character")
98
+ async def switch_character_endpoint(request: Request):
99
+ """Switch character endpoint"""
 
 
 
 
 
 
 
 
 
 
100
  try:
101
+ data = await request.json()
102
+ character_id = data.get('character', 'moses')
 
 
 
 
 
 
 
103
 
104
+ # Validate character
105
+ valid_characters = ['moses', 'samsung_employee', 'jinx']
106
+ if character_id not in valid_characters:
107
+ raise HTTPException(status_code=400, detail="Invalid character")
108
+
109
+ return {
110
+ "success": True,
111
+ "character": character_id,
112
+ "message": f"Switched to {character_id}"
113
+ }
114
 
115
  except Exception as e:
116
+ return {"success": False, "error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ @app.get("/api/voice-status")
119
+ async def voice_status():
120
+ """Voice status endpoint"""
121
+ return {
122
+ "available": False, # Disabled for Spaces deployment
123
+ "enabled": False
 
 
 
 
 
 
 
124
  }
125
+
126
+ # Static file serving
127
+ app.mount("/static", StaticFiles(directory="frontend/static"), name="static")
128
+
129
+ @app.get("/")
130
+ async def serve_index():
131
+ """Serve the main React-style frontend"""
132
+ try:
133
+ with open("frontend/index.html", "r", encoding="utf-8") as f:
134
+ content = f.read()
135
+ return HTMLResponse(content=content)
136
+ except FileNotFoundError:
137
+ return HTMLResponse(
138
+ content="<h1>Frontend not found</h1><p>Please ensure frontend files are properly deployed.</p>",
139
+ status_code=404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  )
 
 
 
 
 
 
 
 
 
 
141
 
142
+ @app.get("/health")
143
+ async def health_check():
144
+ """Health check endpoint"""
145
+ return {"status": "healthy", "characters_loaded": character_manager is not None}
146
+
147
+ # Run the server
148
  if __name__ == "__main__":
149
+ port = int(os.environ.get("PORT", 7860)) # Hugging Face Spaces uses port 7860
150
+ uvicorn.run(app, host="0.0.0.0", port=port)
 
 
 
 
 
 
frontend/index.html ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Roleplay Chat</title>
7
+ <link rel="stylesheet" href="static/css/style.css">
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
10
+ </head>
11
+ <body>
12
+ <div class="app-container">
13
+ <!-- Sidebar for character selection -->
14
+ <div class="sidebar">
15
+ <div class="sidebar-header">
16
+ <h2><i class="fas fa-masks-theater"></i> Characters</h2>
17
+ <button class="new-chat-btn" onclick="startNewChat()">
18
+ <i class="fas fa-plus"></i> New Chat
19
+ </button>
20
+ </div>
21
+
22
+ <div class="characters-list">
23
+ <div class="character-card" data-character="moses">
24
+ <div class="character-avatar">
25
+ <img src="static/avatars/moses.svg" alt="Moses" onerror="this.src='static/avatars/default.svg'">
26
+ </div>
27
+ <div class="character-info">
28
+ <h3>Moses</h3>
29
+ <p>Biblical Prophet</p>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="character-card" data-character="samsung_employee">
34
+ <div class="character-avatar">
35
+ <img src="static/avatars/samsung.svg" alt="Samsung Employee" onerror="this.src='static/avatars/default.svg'">
36
+ </div>
37
+ <div class="character-info">
38
+ <h3>Samsung Employee</h3>
39
+ <p>Tech Expert</p>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="character-card" data-character="jinx">
44
+ <div class="character-avatar">
45
+ <img src="static/avatars/jinx.svg" alt="Jinx" onerror="this.src='static/avatars/default.svg'">
46
+ </div>
47
+ <div class="character-info">
48
+ <h3>Jinx</h3>
49
+ <p>Chaotic Genius</p>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="sidebar-footer">
55
+ <button class="resources-btn" onclick="showResources()">
56
+ <i class="fas fa-book"></i> Resources
57
+ </button>
58
+ <button class="settings-btn" onclick="showSettings()">
59
+ <i class="fas fa-cog"></i> Settings
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- Main chat area -->
65
+ <div class="main-content">
66
+ <div class="chat-header">
67
+ <div class="current-character">
68
+ <div class="character-avatar-small">
69
+ <img id="current-avatar" src="static/avatars/moses.svg" alt="Current Character">
70
+ </div>
71
+ <div class="character-details">
72
+ <h2 id="current-character-name">Moses</h2>
73
+ <p id="current-character-desc">Biblical Prophet and Lawgiver</p>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="chat-controls">
78
+ <button class="voice-toggle" onclick="toggleVoice()" title="Toggle Voice Output">
79
+ <i id="voice-icon" class="fas fa-volume-up"></i>
80
+ </button>
81
+ <button class="clear-chat" onclick="clearChat()" title="Clear Chat">
82
+ <i class="fas fa-trash"></i>
83
+ </button>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="chat-messages" id="chat-messages">
88
+ <div class="welcome-message">
89
+ <div class="message-avatar">
90
+ <img src="static/avatars/moses.svg" alt="Moses">
91
+ </div>
92
+ <div class="message-content">
93
+ <p>Peace be with you, my child. I am Moses, the lawgiver and prophet of the Most High. How may I guide you in righteousness?</p>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <div class="chat-input-container">
99
+ <div class="typing-indicator" id="typing-indicator" style="display: none;">
100
+ <div class="typing-dots">
101
+ <span></span>
102
+ <span></span>
103
+ <span></span>
104
+ </div>
105
+ <span id="typing-character">Moses</span> is typing...
106
+ </div>
107
+
108
+ <div class="chat-input-wrapper">
109
+ <textarea
110
+ id="message-input"
111
+ placeholder="Message Moses..."
112
+ rows="1"
113
+ onkeydown="handleKeyPress(event)"
114
+ oninput="autoResize(this)"
115
+ ></textarea>
116
+ <button class="send-button" onclick="sendMessage()" id="send-btn">
117
+ <i class="fas fa-paper-plane"></i>
118
+ </button>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Resources Modal -->
125
+ <div class="modal" id="resources-modal">
126
+ <div class="modal-content">
127
+ <div class="modal-header">
128
+ <h2>Resources & Architecture</h2>
129
+ <button class="close-modal" onclick="closeModal('resources-modal')">
130
+ <i class="fas fa-times"></i>
131
+ </button>
132
+ </div>
133
+ <div class="modal-body">
134
+ <div class="resource-section">
135
+ <h3>Models Used</h3>
136
+ <div class="resource-links">
137
+ <a href="https://huggingface.co/Polarium/qwen2-yoda-lora" target="_blank" class="resource-link">
138
+ <i class="fab fa-huggingface"></i>
139
+ Qwen2 Base Model (Polarium/qwen2-yoda-lora)
140
+ </a>
141
+ <a href="https://huggingface.co/microsoft/VibeVoice-1.5B" target="_blank" class="resource-link">
142
+ <i class="fab fa-microsoft"></i>
143
+ VibeVoice-1.5B (Text-to-Speech)
144
+ </a>
145
+ </div>
146
+ </div>
147
+
148
+ <div class="resource-section">
149
+ <h3>System Architecture</h3>
150
+ <div class="architecture-diagram">
151
+ <img src="static/diagrams/architecture.png" alt="System Architecture" onerror="this.style.display='none'">
152
+ </div>
153
+ <div class="architecture-text">
154
+ <h4>Components:</h4>
155
+ <ul>
156
+ <li><strong>Frontend:</strong> HTML/CSS/JS with WebSocket communication</li>
157
+ <li><strong>Backend:</strong> FastAPI with character management</li>
158
+ <li><strong>Base Model:</strong> Qwen2-7B-Instruct for conversation</li>
159
+ <li><strong>LoRA Adapters:</strong> Character-specific fine-tuning</li>
160
+ <li><strong>Voice Synthesis:</strong> VibeVoice for audio output</li>
161
+ </ul>
162
+ </div>
163
+ </div>
164
+
165
+ <div class="resource-section">
166
+ <h3>LoRA Implementation</h3>
167
+ <p>Low-Rank Adaptation (LoRA) allows efficient character switching by:</p>
168
+ <ul>
169
+ <li>Keeping base model frozen</li>
170
+ <li>Training small adapter matrices for each character</li>
171
+ <li>Switching adapters at inference time</li>
172
+ <li>Reducing memory usage by ~90%</li>
173
+ </ul>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Settings Modal -->
180
+ <div class="modal" id="settings-modal">
181
+ <div class="modal-content">
182
+ <div class="modal-header">
183
+ <h2>Settings</h2>
184
+ <button class="close-modal" onclick="closeModal('settings-modal')">
185
+ <i class="fas fa-times"></i>
186
+ </button>
187
+ </div>
188
+ <div class="modal-body">
189
+ <div class="setting-group">
190
+ <label>Voice Output</label>
191
+ <input type="checkbox" id="voice-enabled" checked onchange="updateVoiceSetting()">
192
+ </div>
193
+ <div class="setting-group">
194
+ <label>Response Speed</label>
195
+ <select id="response-speed">
196
+ <option value="0.5">Slow</option>
197
+ <option value="0.7" selected>Normal</option>
198
+ <option value="0.9">Fast</option>
199
+ </select>
200
+ </div>
201
+ <div class="setting-group">
202
+ <label>Theme</label>
203
+ <select id="theme-select" onchange="changeTheme()">
204
+ <option value="dark">Dark</option>
205
+ <option value="light">Light</option>
206
+ </select>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <script src="static/js/rest_app.js"></script>
213
+ </body>
214
+ </html>
frontend/static/avatars/default.png ADDED
frontend/static/avatars/default.svg ADDED
frontend/static/avatars/jinx.png ADDED
frontend/static/avatars/jinx.svg ADDED
frontend/static/avatars/moses.png ADDED
frontend/static/avatars/moses.svg ADDED
frontend/static/avatars/samsung.png ADDED
frontend/static/avatars/samsung.svg ADDED
frontend/static/avatars/user.png ADDED
frontend/static/avatars/user.svg ADDED
frontend/static/css/style.css ADDED
@@ -0,0 +1,700 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-primary: #1a1a1a;
3
+ --bg-secondary: #2d2d2d;
4
+ --bg-tertiary: #3a3a3a;
5
+ --text-primary: #ffffff;
6
+ --text-secondary: #b3b3b3;
7
+ --accent-primary: #4a90e2;
8
+ --accent-secondary: #357abd;
9
+ --border-color: #404040;
10
+ --success-color: #28a745;
11
+ --warning-color: #ffc107;
12
+ --danger-color: #dc3545;
13
+ --message-user: #4a90e2;
14
+ --message-assistant: #2d2d2d;
15
+ --sidebar-width: 280px;
16
+ }
17
+
18
+ [data-theme="light"] {
19
+ --bg-primary: #ffffff;
20
+ --bg-secondary: #f8f9fa;
21
+ --bg-tertiary: #e9ecef;
22
+ --text-primary: #212529;
23
+ --text-secondary: #6c757d;
24
+ --accent-primary: #4a90e2;
25
+ --accent-secondary: #357abd;
26
+ --border-color: #dee2e6;
27
+ --message-user: #4a90e2;
28
+ --message-assistant: #f8f9fa;
29
+ }
30
+
31
+ * {
32
+ margin: 0;
33
+ padding: 0;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ body {
38
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
39
+ background-color: var(--bg-primary);
40
+ color: var(--text-primary);
41
+ height: 100vh;
42
+ overflow: hidden;
43
+ }
44
+
45
+ .app-container {
46
+ display: flex;
47
+ height: 100vh;
48
+ }
49
+
50
+
51
+
52
+ /* Notification Styles */
53
+ .notification {
54
+ position: fixed;
55
+ top: 20px;
56
+ right: 20px;
57
+ background: linear-gradient(135deg, #4a90e2, #357abd);
58
+ color: white;
59
+ padding: 1rem 1.5rem;
60
+ border-radius: 12px;
61
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
62
+ z-index: 1000;
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 0.75rem;
66
+ max-width: 400px;
67
+ animation: slideIn 0.3s ease-out;
68
+ }
69
+
70
+ .notification.success {
71
+ background: linear-gradient(135deg, #28a745, #20c997);
72
+ }
73
+
74
+ .notification.error {
75
+ background: linear-gradient(135deg, #dc3545, #c82333);
76
+ }
77
+
78
+ .notification.info {
79
+ background: linear-gradient(135deg, #17a2b8, #138496);
80
+ }
81
+
82
+ .notification button {
83
+ background: none;
84
+ border: none;
85
+ color: white;
86
+ font-size: 1.2rem;
87
+ cursor: pointer;
88
+ margin-left: auto;
89
+ padding: 0;
90
+ width: 20px;
91
+ height: 20px;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ }
96
+
97
+ @keyframes slideIn {
98
+ from {
99
+ transform: translateX(100%);
100
+ opacity: 0;
101
+ }
102
+ to {
103
+ transform: translateX(0);
104
+ opacity: 1;
105
+ }
106
+ }
107
+
108
+ /* Sidebar Styles */
109
+ .sidebar {
110
+ width: var(--sidebar-width);
111
+ background-color: var(--bg-secondary);
112
+ border-right: 1px solid var(--border-color);
113
+ display: flex;
114
+ flex-direction: column;
115
+ overflow: hidden;
116
+ }
117
+
118
+ .sidebar-header {
119
+ padding: 20px;
120
+ border-bottom: 1px solid var(--border-color);
121
+ }
122
+
123
+ .sidebar-header h2 {
124
+ font-size: 18px;
125
+ font-weight: 600;
126
+ margin-bottom: 16px;
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 8px;
130
+ }
131
+
132
+ .new-chat-btn {
133
+ width: 100%;
134
+ padding: 12px;
135
+ background-color: var(--accent-primary);
136
+ color: white;
137
+ border: none;
138
+ border-radius: 8px;
139
+ cursor: pointer;
140
+ font-size: 14px;
141
+ font-weight: 500;
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ gap: 8px;
146
+ transition: background-color 0.2s;
147
+ }
148
+
149
+ .new-chat-btn:hover {
150
+ background-color: var(--accent-secondary);
151
+ }
152
+
153
+ .characters-list {
154
+ flex: 1;
155
+ padding: 20px;
156
+ overflow-y: auto;
157
+ }
158
+
159
+ .character-card {
160
+ display: flex;
161
+ align-items: center;
162
+ padding: 16px;
163
+ margin-bottom: 12px;
164
+ background-color: var(--bg-tertiary);
165
+ border-radius: 12px;
166
+ cursor: pointer;
167
+ transition: all 0.2s;
168
+ border: 2px solid transparent;
169
+ }
170
+
171
+ .character-card:hover {
172
+ background-color: var(--bg-primary);
173
+ transform: translateY(-2px);
174
+ }
175
+
176
+ .character-card.active {
177
+ border-color: var(--accent-primary);
178
+ background-color: var(--accent-primary);
179
+ color: white;
180
+ }
181
+
182
+ .character-avatar {
183
+ width: 48px;
184
+ height: 48px;
185
+ border-radius: 50%;
186
+ overflow: hidden;
187
+ margin-right: 12px;
188
+ flex-shrink: 0;
189
+ }
190
+
191
+ .character-avatar img {
192
+ width: 100%;
193
+ height: 100%;
194
+ object-fit: cover;
195
+ }
196
+
197
+ .character-info h3 {
198
+ font-size: 16px;
199
+ font-weight: 600;
200
+ margin-bottom: 4px;
201
+ }
202
+
203
+ .character-info p {
204
+ font-size: 13px;
205
+ color: var(--text-secondary);
206
+ }
207
+
208
+ .character-card.active .character-info p {
209
+ color: rgba(255, 255, 255, 0.8);
210
+ }
211
+
212
+ .sidebar-footer {
213
+ padding: 20px;
214
+ border-top: 1px solid var(--border-color);
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: 12px;
218
+ }
219
+
220
+ .resources-btn, .settings-btn {
221
+ padding: 12px;
222
+ background-color: transparent;
223
+ color: var(--text-secondary);
224
+ border: 1px solid var(--border-color);
225
+ border-radius: 8px;
226
+ cursor: pointer;
227
+ font-size: 14px;
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 8px;
231
+ transition: all 0.2s;
232
+ }
233
+
234
+ .resources-btn:hover, .settings-btn:hover {
235
+ background-color: var(--bg-tertiary);
236
+ color: var(--text-primary);
237
+ }
238
+
239
+ /* Main Content Styles */
240
+ .main-content {
241
+ flex: 1;
242
+ display: flex;
243
+ flex-direction: column;
244
+ background-color: var(--bg-primary);
245
+ }
246
+
247
+ .chat-header {
248
+ padding: 20px;
249
+ border-bottom: 1px solid var(--border-color);
250
+ display: flex;
251
+ justify-content: space-between;
252
+ align-items: center;
253
+ background-color: var(--bg-secondary);
254
+ }
255
+
256
+ .current-character {
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 12px;
260
+ }
261
+
262
+ .character-avatar-small {
263
+ width: 40px;
264
+ height: 40px;
265
+ border-radius: 50%;
266
+ overflow: hidden;
267
+ }
268
+
269
+ .character-avatar-small img {
270
+ width: 100%;
271
+ height: 100%;
272
+ object-fit: cover;
273
+ }
274
+
275
+ .character-details h2 {
276
+ font-size: 20px;
277
+ font-weight: 600;
278
+ margin-bottom: 2px;
279
+ }
280
+
281
+ .character-details p {
282
+ font-size: 14px;
283
+ color: var(--text-secondary);
284
+ }
285
+
286
+ .chat-controls {
287
+ display: flex;
288
+ gap: 12px;
289
+ }
290
+
291
+ .voice-toggle, .clear-chat {
292
+ width: 40px;
293
+ height: 40px;
294
+ border: none;
295
+ border-radius: 8px;
296
+ background-color: var(--bg-tertiary);
297
+ color: var(--text-primary);
298
+ cursor: pointer;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ transition: all 0.2s;
303
+ }
304
+
305
+ .voice-toggle:hover, .clear-chat:hover {
306
+ background-color: var(--bg-primary);
307
+ }
308
+
309
+ .voice-toggle.active {
310
+ background-color: var(--success-color);
311
+ color: white;
312
+ }
313
+
314
+ /* Chat Messages */
315
+ .chat-messages {
316
+ flex: 1;
317
+ overflow-y: auto;
318
+ padding: 20px;
319
+ display: flex;
320
+ flex-direction: column;
321
+ gap: 20px;
322
+ }
323
+
324
+ .message {
325
+ display: flex;
326
+ gap: 12px;
327
+ max-width: 80%;
328
+ animation: fadeIn 0.3s ease-out;
329
+ }
330
+
331
+ .message.user {
332
+ align-self: flex-end;
333
+ flex-direction: row-reverse;
334
+ }
335
+
336
+ .message-avatar {
337
+ width: 32px;
338
+ height: 32px;
339
+ border-radius: 50%;
340
+ overflow: hidden;
341
+ flex-shrink: 0;
342
+ }
343
+
344
+ .message-avatar img {
345
+ width: 100%;
346
+ height: 100%;
347
+ object-fit: cover;
348
+ }
349
+
350
+ .message-content {
351
+ background-color: var(--message-assistant);
352
+ padding: 16px;
353
+ border-radius: 16px;
354
+ position: relative;
355
+ }
356
+
357
+ .message.user .message-content {
358
+ background-color: var(--message-user);
359
+ color: white;
360
+ }
361
+
362
+ .message-content p {
363
+ margin: 0;
364
+ line-height: 1.5;
365
+ }
366
+
367
+ .welcome-message {
368
+ display: flex;
369
+ gap: 12px;
370
+ margin-bottom: 20px;
371
+ opacity: 0.7;
372
+ }
373
+
374
+ .welcome-message .message-content {
375
+ background-color: var(--bg-secondary);
376
+ border: 1px solid var(--border-color);
377
+ }
378
+
379
+ /* Chat Input */
380
+ .chat-input-container {
381
+ padding: 20px;
382
+ border-top: 1px solid var(--border-color);
383
+ background-color: var(--bg-secondary);
384
+ }
385
+
386
+ .typing-indicator {
387
+ display: flex;
388
+ align-items: center;
389
+ gap: 8px;
390
+ margin-bottom: 12px;
391
+ font-size: 14px;
392
+ color: var(--text-secondary);
393
+ }
394
+
395
+ .typing-dots {
396
+ display: flex;
397
+ gap: 4px;
398
+ }
399
+
400
+ .typing-dots span {
401
+ width: 6px;
402
+ height: 6px;
403
+ background-color: var(--accent-primary);
404
+ border-radius: 50%;
405
+ animation: typingDot 1.4s infinite ease-in-out;
406
+ }
407
+
408
+ .typing-dots span:nth-child(2) {
409
+ animation-delay: 0.16s;
410
+ }
411
+
412
+ .typing-dots span:nth-child(3) {
413
+ animation-delay: 0.32s;
414
+ }
415
+
416
+ .chat-input-wrapper {
417
+ display: flex;
418
+ gap: 12px;
419
+ align-items: flex-end;
420
+ }
421
+
422
+ #message-input {
423
+ flex: 1;
424
+ min-height: 48px;
425
+ max-height: 120px;
426
+ padding: 12px 16px;
427
+ border: 1px solid var(--border-color);
428
+ border-radius: 24px;
429
+ background-color: var(--bg-primary);
430
+ color: var(--text-primary);
431
+ font-family: inherit;
432
+ font-size: 14px;
433
+ resize: none;
434
+ outline: none;
435
+ transition: border-color 0.2s;
436
+ }
437
+
438
+ #message-input:focus {
439
+ border-color: var(--accent-primary);
440
+ }
441
+
442
+ #message-input::placeholder {
443
+ color: var(--text-secondary);
444
+ }
445
+
446
+ .send-button {
447
+ width: 48px;
448
+ height: 48px;
449
+ border: none;
450
+ border-radius: 50%;
451
+ background-color: var(--accent-primary);
452
+ color: white;
453
+ cursor: pointer;
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: center;
457
+ transition: all 0.2s;
458
+ }
459
+
460
+ .send-button:hover {
461
+ background-color: var(--accent-secondary);
462
+ transform: scale(1.05);
463
+ }
464
+
465
+ .send-button:disabled {
466
+ opacity: 0.5;
467
+ cursor: not-allowed;
468
+ transform: none;
469
+ }
470
+
471
+ /* Modal Styles */
472
+ .modal {
473
+ display: none;
474
+ position: fixed;
475
+ top: 0;
476
+ left: 0;
477
+ width: 100%;
478
+ height: 100%;
479
+ background-color: rgba(0, 0, 0, 0.5);
480
+ z-index: 1000;
481
+ backdrop-filter: blur(4px);
482
+ }
483
+
484
+ .modal.active {
485
+ display: flex;
486
+ align-items: center;
487
+ justify-content: center;
488
+ }
489
+
490
+ .modal-content {
491
+ background-color: var(--bg-secondary);
492
+ border-radius: 16px;
493
+ width: 90%;
494
+ max-width: 600px;
495
+ max-height: 80vh;
496
+ overflow: hidden;
497
+ animation: modalSlideIn 0.3s ease-out;
498
+ }
499
+
500
+ .modal-header {
501
+ display: flex;
502
+ justify-content: space-between;
503
+ align-items: center;
504
+ padding: 24px;
505
+ border-bottom: 1px solid var(--border-color);
506
+ }
507
+
508
+ .modal-header h2 {
509
+ font-size: 20px;
510
+ font-weight: 600;
511
+ }
512
+
513
+ .close-modal {
514
+ width: 32px;
515
+ height: 32px;
516
+ border: none;
517
+ background: none;
518
+ color: var(--text-secondary);
519
+ cursor: pointer;
520
+ border-radius: 8px;
521
+ display: flex;
522
+ align-items: center;
523
+ justify-content: center;
524
+ transition: all 0.2s;
525
+ }
526
+
527
+ .close-modal:hover {
528
+ background-color: var(--bg-tertiary);
529
+ color: var(--text-primary);
530
+ }
531
+
532
+ .modal-body {
533
+ padding: 24px;
534
+ overflow-y: auto;
535
+ max-height: 60vh;
536
+ }
537
+
538
+ .resource-section {
539
+ margin-bottom: 32px;
540
+ }
541
+
542
+ .resource-section h3 {
543
+ font-size: 18px;
544
+ font-weight: 600;
545
+ margin-bottom: 16px;
546
+ color: var(--accent-primary);
547
+ }
548
+
549
+ .resource-links {
550
+ display: flex;
551
+ flex-direction: column;
552
+ gap: 12px;
553
+ }
554
+
555
+ .resource-link {
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 12px;
559
+ padding: 16px;
560
+ background-color: var(--bg-tertiary);
561
+ border-radius: 12px;
562
+ text-decoration: none;
563
+ color: var(--text-primary);
564
+ transition: all 0.2s;
565
+ }
566
+
567
+ .resource-link:hover {
568
+ background-color: var(--accent-primary);
569
+ color: white;
570
+ transform: translateY(-2px);
571
+ }
572
+
573
+ .architecture-diagram img {
574
+ width: 100%;
575
+ border-radius: 8px;
576
+ margin: 16px 0;
577
+ }
578
+
579
+ .architecture-text ul {
580
+ margin-left: 20px;
581
+ margin-top: 12px;
582
+ }
583
+
584
+ .architecture-text li {
585
+ margin-bottom: 8px;
586
+ line-height: 1.5;
587
+ }
588
+
589
+ .setting-group {
590
+ display: flex;
591
+ justify-content: space-between;
592
+ align-items: center;
593
+ padding: 16px 0;
594
+ border-bottom: 1px solid var(--border-color);
595
+ }
596
+
597
+ .setting-group:last-child {
598
+ border-bottom: none;
599
+ }
600
+
601
+ .setting-group label {
602
+ font-weight: 500;
603
+ }
604
+
605
+ .setting-group input, .setting-group select {
606
+ padding: 8px 12px;
607
+ border: 1px solid var(--border-color);
608
+ border-radius: 6px;
609
+ background-color: var(--bg-primary);
610
+ color: var(--text-primary);
611
+ }
612
+
613
+ /* Animations */
614
+ @keyframes fadeIn {
615
+ from {
616
+ opacity: 0;
617
+ transform: translateY(20px);
618
+ }
619
+ to {
620
+ opacity: 1;
621
+ transform: translateY(0);
622
+ }
623
+ }
624
+
625
+ @keyframes typingDot {
626
+ 0%, 80%, 100% {
627
+ transform: scale(0);
628
+ opacity: 0.5;
629
+ }
630
+ 40% {
631
+ transform: scale(1);
632
+ opacity: 1;
633
+ }
634
+ }
635
+
636
+ @keyframes modalSlideIn {
637
+ from {
638
+ opacity: 0;
639
+ transform: scale(0.9) translateY(-20px);
640
+ }
641
+ to {
642
+ opacity: 1;
643
+ transform: scale(1) translateY(0);
644
+ }
645
+ }
646
+
647
+ /* Scrollbar Styles */
648
+ .chat-messages::-webkit-scrollbar,
649
+ .modal-body::-webkit-scrollbar {
650
+ width: 6px;
651
+ }
652
+
653
+ .chat-messages::-webkit-scrollbar-track,
654
+ .modal-body::-webkit-scrollbar-track {
655
+ background: var(--bg-secondary);
656
+ }
657
+
658
+ .chat-messages::-webkit-scrollbar-thumb,
659
+ .modal-body::-webkit-scrollbar-thumb {
660
+ background: var(--border-color);
661
+ border-radius: 3px;
662
+ }
663
+
664
+ .chat-messages::-webkit-scrollbar-thumb:hover,
665
+ .modal-body::-webkit-scrollbar-thumb:hover {
666
+ background: var(--text-secondary);
667
+ }
668
+
669
+ /* Responsive Design */
670
+ @media (max-width: 768px) {
671
+ .app-container {
672
+ flex-direction: column;
673
+ }
674
+
675
+ .sidebar {
676
+ width: 100%;
677
+ height: auto;
678
+ position: relative;
679
+ }
680
+
681
+ .characters-list {
682
+ display: flex;
683
+ overflow-x: auto;
684
+ padding: 12px 20px;
685
+ }
686
+
687
+ .character-card {
688
+ min-width: 200px;
689
+ margin-right: 12px;
690
+ margin-bottom: 0;
691
+ }
692
+
693
+ .main-content {
694
+ height: calc(100vh - 200px);
695
+ }
696
+
697
+ .message {
698
+ max-width: 95%;
699
+ }
700
+ }
frontend/static/diagrams/architecture.png ADDED
frontend/static/js/app.js ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // WebSocket connection
2
+ let ws = null;
3
+ let clientId = generateClientId();
4
+ let currentCharacter = 'moses';
5
+ let conversationHistory = [];
6
+ let voiceEnabled = true;
7
+
8
+ // Initialize the application
9
+ document.addEventListener('DOMContentLoaded', function() {
10
+ initializeWebSocket();
11
+ setupCharacterSelection();
12
+ setupEventListeners();
13
+ loadSettings();
14
+ });
15
+
16
+ function generateClientId() {
17
+ return 'client_' + Math.random().toString(36).substr(2, 9);
18
+ }
19
+
20
+ function initializeWebSocket() {
21
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
22
+ const wsUrl = `${wsProtocol}//${window.location.host}/ws/${clientId}`;
23
+
24
+ ws = new WebSocket(wsUrl);
25
+
26
+ ws.onopen = function() {
27
+ console.log('WebSocket connected');
28
+ updateConnectionStatus(true);
29
+ };
30
+
31
+ ws.onmessage = function(event) {
32
+ const data = JSON.parse(event.data);
33
+ handleWebSocketMessage(data);
34
+ };
35
+
36
+ ws.onclose = function() {
37
+ console.log('WebSocket disconnected');
38
+ updateConnectionStatus(false);
39
+
40
+ // Attempt to reconnect after 3 seconds
41
+ setTimeout(initializeWebSocket, 3000);
42
+ };
43
+
44
+ ws.onerror = function(error) {
45
+ console.error('WebSocket error:', error);
46
+ updateConnectionStatus(false);
47
+ };
48
+ }
49
+
50
+ function updateConnectionStatus(connected) {
51
+ // You could add a connection indicator in the UI
52
+ const sendBtn = document.getElementById('send-btn');
53
+ sendBtn.disabled = !connected;
54
+ }
55
+
56
+ function handleWebSocketMessage(data) {
57
+ switch(data.type) {
58
+ case 'character_switched':
59
+ console.log(`Switched to character: ${data.character_id}`);
60
+ break;
61
+
62
+ case 'chat_response':
63
+ hideTypingIndicator();
64
+ displayMessage(data.response, 'assistant', data.character_id);
65
+
66
+ // Play audio if available
67
+ if (data.audio && voiceEnabled) {
68
+ playAudio(data.audio);
69
+ }
70
+ break;
71
+ }
72
+ }
73
+
74
+ function setupCharacterSelection() {
75
+ const characterCards = document.querySelectorAll('.character-card');
76
+
77
+ characterCards.forEach(card => {
78
+ card.addEventListener('click', function() {
79
+ const characterId = this.dataset.character;
80
+ switchCharacter(characterId);
81
+
82
+ // Show enhancement notification
83
+ showEnhancementNotification(characterId);
84
+ });
85
+ });
86
+
87
+ // Set initial character as active
88
+ setActiveCharacter(currentCharacter);
89
+ }
90
+
91
+ function showEnhancementNotification(character) {
92
+ const notifications = {
93
+ 'moses': 'Moses enhanced with 70 examples of divine wisdom and biblical knowledge',
94
+ 'samsung_employee': 'Samsung Employee enhanced with 60 examples of technical expertise',
95
+ 'jinx': 'Jinx enhanced with 60 examples of chaotic personality and emotional depth'
96
+ };
97
+
98
+ const message = notifications[character] || 'Character enhanced with 5x training data';
99
+ showNotification(message, 'enhancement');
100
+ }
101
+
102
+ function showNotification(message, type = 'info') {
103
+ // Remove existing notifications
104
+ document.querySelectorAll('.notification').forEach(n => n.remove());
105
+
106
+ const notification = document.createElement('div');
107
+ notification.className = `notification ${type}`;
108
+ notification.innerHTML = `
109
+ <i class="fas fa-star"></i>
110
+ <span>${message}</span>
111
+ <button onclick="this.parentElement.remove()">×</button>
112
+ `;
113
+
114
+ document.body.appendChild(notification);
115
+
116
+ // Auto remove after 5 seconds
117
+ setTimeout(() => {
118
+ notification.remove();
119
+ }, 5000);
120
+ }
121
+
122
+ function setupEventListeners() {
123
+ const messageInput = document.getElementById('message-input');
124
+ const sendBtn = document.getElementById('send-btn');
125
+
126
+ // Send message on button click
127
+ sendBtn.addEventListener('click', sendMessage);
128
+
129
+ // Auto-resize textarea
130
+ messageInput.addEventListener('input', function() {
131
+ autoResize(this);
132
+ });
133
+ }
134
+
135
+ function handleKeyPress(event) {
136
+ if (event.key === 'Enter' && !event.shiftKey) {
137
+ event.preventDefault();
138
+ sendMessage();
139
+ }
140
+ }
141
+
142
+ function autoResize(textarea) {
143
+ textarea.style.height = 'auto';
144
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
145
+ }
146
+
147
+ function switchCharacter(characterId) {
148
+ if (currentCharacter === characterId) return;
149
+
150
+ currentCharacter = characterId;
151
+ setActiveCharacter(characterId);
152
+ updateChatHeader(characterId);
153
+
154
+ // Send switch message via WebSocket
155
+ if (ws && ws.readyState === WebSocket.OPEN) {
156
+ ws.send(JSON.stringify({
157
+ type: 'switch_character',
158
+ character_id: characterId
159
+ }));
160
+ }
161
+
162
+ // Clear current chat and show welcome message
163
+ clearChat();
164
+ showWelcomeMessage(characterId);
165
+ }
166
+
167
+ function setActiveCharacter(characterId) {
168
+ const characterCards = document.querySelectorAll('.character-card');
169
+
170
+ characterCards.forEach(card => {
171
+ card.classList.remove('active');
172
+ if (card.dataset.character === characterId) {
173
+ card.classList.add('active');
174
+ }
175
+ });
176
+ }
177
+
178
+ function updateChatHeader(characterId) {
179
+ const characters = {
180
+ 'moses': {
181
+ name: 'Moses',
182
+ description: 'Biblical Prophet and Lawgiver',
183
+ avatar: 'static/avatars/moses.svg'
184
+ },
185
+ 'samsung_employee': {
186
+ name: 'Samsung Employee',
187
+ description: 'Tech Expert and Product Specialist',
188
+ avatar: 'static/avatars/samsung.svg'
189
+ },
190
+ 'jinx': {
191
+ name: 'Jinx',
192
+ description: 'Chaotic Genius from Arcane',
193
+ avatar: 'static/avatars/jinx.svg'
194
+ }
195
+ };
196
+
197
+ const character = characters[characterId];
198
+ if (character) {
199
+ document.getElementById('current-character-name').textContent = character.name;
200
+ document.getElementById('current-character-desc').textContent = character.description;
201
+ document.getElementById('current-avatar').src = character.avatar;
202
+ document.getElementById('message-input').placeholder = `Message ${character.name}...`;
203
+ document.getElementById('typing-character').textContent = character.name;
204
+ }
205
+ }
206
+
207
+ function sendMessage() {
208
+ const messageInput = document.getElementById('message-input');
209
+ const message = messageInput.value.trim();
210
+
211
+ if (!message || !ws || ws.readyState !== WebSocket.OPEN) return;
212
+
213
+ // Display user message
214
+ displayMessage(message, 'user');
215
+
216
+ // Add to conversation history
217
+ conversationHistory.push({
218
+ role: 'user',
219
+ content: message
220
+ });
221
+
222
+ // Show typing indicator
223
+ showTypingIndicator();
224
+
225
+ // Send via WebSocket
226
+ ws.send(JSON.stringify({
227
+ type: 'chat_message',
228
+ text: message,
229
+ history: conversationHistory.slice(-10), // Keep last 10 messages
230
+ include_audio: voiceEnabled,
231
+ timestamp: Date.now()
232
+ }));
233
+
234
+ // Clear input
235
+ messageInput.value = '';
236
+ autoResize(messageInput);
237
+ }
238
+
239
+ function displayMessage(content, sender, characterId = null) {
240
+ const chatMessages = document.getElementById('chat-messages');
241
+ const messageDiv = document.createElement('div');
242
+ messageDiv.className = `message ${sender}`;
243
+
244
+ let avatarSrc = '';
245
+ if (sender === 'user') {
246
+ avatarSrc = 'static/avatars/user.svg';
247
+ } else {
248
+ const avatars = {
249
+ 'moses': 'static/avatars/moses.svg',
250
+ 'samsung_employee': 'static/avatars/samsung.svg',
251
+ 'jinx': 'static/avatars/jinx.svg'
252
+ };
253
+ avatarSrc = avatars[characterId || currentCharacter];
254
+ }
255
+
256
+ messageDiv.innerHTML = `
257
+ <div class="message-avatar">
258
+ <img src="${avatarSrc}" alt="${sender}" onerror="this.src='static/avatars/default.svg'">
259
+ </div>
260
+ <div class="message-content">
261
+ <p>${escapeHtml(content)}</p>
262
+ </div>
263
+ `;
264
+
265
+ chatMessages.appendChild(messageDiv);
266
+ scrollToBottom();
267
+
268
+ // Add to conversation history for assistant messages
269
+ if (sender === 'assistant') {
270
+ conversationHistory.push({
271
+ role: 'assistant',
272
+ content: content
273
+ });
274
+ }
275
+ }
276
+
277
+ function showWelcomeMessage(characterId) {
278
+ const welcomeMessages = {
279
+ 'moses': "Peace be with you, my child. I am Moses, prophet and lawgiver. My character-focused training allows me to embody the wisdom of the Almighty. How may I guide you in righteousness?",
280
+ 'samsung_employee': "Hello! I'm your Samsung technology expert, trained with character-focused learning to provide authentic product knowledge and enthusiasm. What amazing Galaxy features can I share with you today?",
281
+ 'jinx': "*spins around excitedly* Hey there! I'm Jinx - the real me, not some AI pretending! My character-focused training means pure chaotic genius with no boring assistant stuff. Ready for some explosive fun?"
282
+ };
283
+
284
+ const message = welcomeMessages[characterId];
285
+ if (message) {
286
+ displayMessage(message, 'assistant', characterId);
287
+ }
288
+ }
289
+
290
+ function showTypingIndicator() {
291
+ document.getElementById('typing-indicator').style.display = 'flex';
292
+ scrollToBottom();
293
+ }
294
+
295
+ function hideTypingIndicator() {
296
+ document.getElementById('typing-indicator').style.display = 'none';
297
+ }
298
+
299
+ function scrollToBottom() {
300
+ const chatMessages = document.getElementById('chat-messages');
301
+ chatMessages.scrollTop = chatMessages.scrollHeight;
302
+ }
303
+
304
+ function escapeHtml(text) {
305
+ const div = document.createElement('div');
306
+ div.textContent = text;
307
+ return div.innerHTML;
308
+ }
309
+
310
+ function playAudio(audioData) {
311
+ if (!voiceEnabled || !audioData) return;
312
+
313
+ try {
314
+ const audio = new Audio(audioData);
315
+ audio.play().catch(error => {
316
+ console.error('Error playing audio:', error);
317
+ });
318
+ } catch (error) {
319
+ console.error('Error creating audio:', error);
320
+ }
321
+ }
322
+
323
+ function toggleVoice() {
324
+ voiceEnabled = !voiceEnabled;
325
+ const voiceIcon = document.getElementById('voice-icon');
326
+ const voiceToggle = document.querySelector('.voice-toggle');
327
+
328
+ if (voiceEnabled) {
329
+ voiceIcon.className = 'fas fa-volume-up';
330
+ voiceToggle.classList.add('active');
331
+ } else {
332
+ voiceIcon.className = 'fas fa-volume-mute';
333
+ voiceToggle.classList.remove('active');
334
+ }
335
+
336
+ saveSettings();
337
+ }
338
+
339
+ function clearChat() {
340
+ const chatMessages = document.getElementById('chat-messages');
341
+ chatMessages.innerHTML = '';
342
+ conversationHistory = [];
343
+ }
344
+
345
+ function startNewChat() {
346
+ clearChat();
347
+ showWelcomeMessage(currentCharacter);
348
+ }
349
+
350
+ function showResources() {
351
+ const modal = document.getElementById('resources-modal');
352
+ modal.classList.add('active');
353
+ }
354
+
355
+ function showSettings() {
356
+ const modal = document.getElementById('settings-modal');
357
+ modal.classList.add('active');
358
+ }
359
+
360
+ function closeModal(modalId) {
361
+ const modal = document.getElementById(modalId);
362
+ modal.classList.remove('active');
363
+ }
364
+
365
+ function updateVoiceSetting() {
366
+ const voiceCheckbox = document.getElementById('voice-enabled');
367
+ voiceEnabled = voiceCheckbox.checked;
368
+
369
+ const voiceToggle = document.querySelector('.voice-toggle');
370
+ const voiceIcon = document.getElementById('voice-icon');
371
+
372
+ if (voiceEnabled) {
373
+ voiceToggle.classList.add('active');
374
+ voiceIcon.className = 'fas fa-volume-up';
375
+ } else {
376
+ voiceToggle.classList.remove('active');
377
+ voiceIcon.className = 'fas fa-volume-mute';
378
+ }
379
+
380
+ saveSettings();
381
+ }
382
+
383
+ function changeTheme() {
384
+ const themeSelect = document.getElementById('theme-select');
385
+ const theme = themeSelect.value;
386
+
387
+ document.documentElement.setAttribute('data-theme', theme);
388
+ saveSettings();
389
+ }
390
+
391
+ function saveSettings() {
392
+ const settings = {
393
+ voiceEnabled: voiceEnabled,
394
+ theme: document.documentElement.getAttribute('data-theme') || 'dark',
395
+ responseSpeed: document.getElementById('response-speed')?.value || '0.7'
396
+ };
397
+
398
+ localStorage.setItem('roleplayChatSettings', JSON.stringify(settings));
399
+ }
400
+
401
+ function loadSettings() {
402
+ try {
403
+ const settings = JSON.parse(localStorage.getItem('roleplayChatSettings')) || {};
404
+
405
+ // Load voice setting
406
+ if (settings.voiceEnabled !== undefined) {
407
+ voiceEnabled = settings.voiceEnabled;
408
+ document.getElementById('voice-enabled').checked = voiceEnabled;
409
+ updateVoiceSetting();
410
+ }
411
+
412
+ // Load theme
413
+ if (settings.theme) {
414
+ document.documentElement.setAttribute('data-theme', settings.theme);
415
+ document.getElementById('theme-select').value = settings.theme;
416
+ }
417
+
418
+ // Load response speed
419
+ if (settings.responseSpeed) {
420
+ document.getElementById('response-speed').value = settings.responseSpeed;
421
+ }
422
+ } catch (error) {
423
+ console.error('Error loading settings:', error);
424
+ }
425
+ }
426
+
427
+ // Close modals when clicking outside
428
+ window.addEventListener('click', function(event) {
429
+ if (event.target.classList.contains('modal')) {
430
+ event.target.classList.remove('active');
431
+ }
432
+ });
433
+
434
+ // Handle escape key to close modals
435
+ document.addEventListener('keydown', function(event) {
436
+ if (event.key === 'Escape') {
437
+ const activeModal = document.querySelector('.modal.active');
438
+ if (activeModal) {
439
+ activeModal.classList.remove('active');
440
+ }
441
+ }
442
+ });
frontend/static/js/rest_app.js ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // REST API-based Chat Application
2
+ let currentCharacter = 'moses';
3
+ let conversationHistory = [];
4
+ let voiceEnabled = false; // Disabled by default
5
+ let voiceAvailable = false;
6
+ let currentAudio = null;
7
+
8
+ // Initialize the application
9
+ document.addEventListener('DOMContentLoaded', function() {
10
+ setupCharacterSelection();
11
+ setupEventListeners();
12
+ loadSettings();
13
+ checkVoiceStatus();
14
+ showWelcomeMessage(currentCharacter);
15
+ updateConnectionStatus(true); // Always connected in REST mode
16
+ });
17
+
18
+ function setupCharacterSelection() {
19
+ const characterCards = document.querySelectorAll('.character-card');
20
+
21
+ characterCards.forEach(card => {
22
+ card.addEventListener('click', function() {
23
+ const characterId = this.dataset.character;
24
+ switchCharacter(characterId);
25
+ });
26
+ });
27
+ }
28
+
29
+ function setupEventListeners() {
30
+ const messageInput = document.getElementById('message-input');
31
+ const sendButton = document.getElementById('send-btn');
32
+
33
+ messageInput.addEventListener('keydown', handleKeyPress);
34
+ sendButton.addEventListener('click', sendMessage);
35
+
36
+ // Auto-resize textarea
37
+ messageInput.addEventListener('input', function() {
38
+ autoResize(this);
39
+ });
40
+ }
41
+
42
+ function handleKeyPress(event) {
43
+ if (event.key === 'Enter' && !event.shiftKey) {
44
+ event.preventDefault();
45
+ sendMessage();
46
+ }
47
+ }
48
+
49
+ function autoResize(textarea) {
50
+ textarea.style.height = 'auto';
51
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
52
+ }
53
+
54
+ async function sendMessage() {
55
+ const messageInput = document.getElementById('message-input');
56
+ const message = messageInput.value.trim();
57
+
58
+ if (!message) return;
59
+
60
+ // Clear input and show user message
61
+ messageInput.value = '';
62
+ autoResize(messageInput);
63
+
64
+ displayMessage(message, 'user');
65
+ showTypingIndicator();
66
+
67
+ try {
68
+ // Send REST API request
69
+ const requestData = {
70
+ text: message,
71
+ timestamp: Date.now(),
72
+ conversation_history: conversationHistory.slice(-4), // Last 4 messages for context
73
+ include_voice: voiceEnabled && voiceAvailable
74
+ };
75
+
76
+ console.log('Sending request:', {
77
+ character: currentCharacter,
78
+ includeVoice: requestData.include_voice,
79
+ voiceEnabled: voiceEnabled,
80
+ voiceAvailable: voiceAvailable
81
+ });
82
+
83
+ const response = await fetch(`/api/chat/${currentCharacter}`, {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json'
87
+ },
88
+ body: JSON.stringify(requestData)
89
+ });
90
+
91
+ hideTypingIndicator();
92
+
93
+ if (response.ok) {
94
+ const data = await response.json();
95
+ console.log('Response data:', {
96
+ hasVoiceData: !!data.voice_data,
97
+ voiceEnabled: voiceEnabled,
98
+ voiceAvailable: voiceAvailable,
99
+ voiceDataLength: data.voice_data ? data.voice_data.length : 0
100
+ });
101
+
102
+ displayMessage(data.response, 'assistant', data.character_id);
103
+
104
+ // Play voice if available
105
+ if (data.voice_data && voiceEnabled) {
106
+ console.log('Attempting to play voice...');
107
+ playVoice(data.voice_data);
108
+ } else if (!data.voice_data) {
109
+ console.log('No voice data in response');
110
+ } else if (!voiceEnabled) {
111
+ console.log('Voice is disabled');
112
+ }
113
+
114
+ // Update conversation history
115
+ conversationHistory.push(
116
+ { role: 'user', content: message },
117
+ { role: 'assistant', content: data.response }
118
+ );
119
+
120
+ // Limit history size
121
+ if (conversationHistory.length > 20) {
122
+ conversationHistory = conversationHistory.slice(-20);
123
+ }
124
+ } else {
125
+ const error = await response.text();
126
+ displayMessage(`Sorry, I encountered an error: ${error}`, 'error');
127
+ }
128
+
129
+ } catch (error) {
130
+ hideTypingIndicator();
131
+ console.error('Error sending message:', error);
132
+ displayMessage('Sorry, I could not connect to the server. Please try again.', 'error');
133
+ }
134
+ }
135
+
136
+ function displayMessage(content, type, characterId = null) {
137
+ const chatMessages = document.getElementById('chat-messages');
138
+ const messageDiv = document.createElement('div');
139
+ messageDiv.className = `message ${type}`;
140
+
141
+ if (type === 'user') {
142
+ messageDiv.innerHTML = `
143
+ <div class="message-avatar">
144
+ <img src="/static/avatars/user.svg" alt="User" onerror="this.src='/static/avatars/default.svg'">
145
+ </div>
146
+ <div class="message-content">
147
+ <div class="message-text">${escapeHtml(content)}</div>
148
+ <div class="message-time">${new Date().toLocaleTimeString()}</div>
149
+ </div>
150
+ `;
151
+ } else if (type === 'assistant') {
152
+ const char = characterId || currentCharacter;
153
+ const charName = getCharacterName(char);
154
+
155
+ messageDiv.innerHTML = `
156
+ <div class="message-avatar">
157
+ <img src="/static/avatars/${char}.svg" alt="${charName}" onerror="this.src='/static/avatars/default.svg'">
158
+ </div>
159
+ <div class="message-content">
160
+ <div class="message-text">${escapeHtml(content)}</div>
161
+ <div class="message-time">${new Date().toLocaleTimeString()}</div>
162
+ </div>
163
+ `;
164
+ } else if (type === 'error') {
165
+ messageDiv.className = 'message error';
166
+ messageDiv.innerHTML = `
167
+ <div class="message-content">
168
+ <div class="message-text error-text">❌ ${escapeHtml(content)}</div>
169
+ <div class="message-time">${new Date().toLocaleTimeString()}</div>
170
+ </div>
171
+ `;
172
+ }
173
+
174
+ chatMessages.appendChild(messageDiv);
175
+ chatMessages.scrollTop = chatMessages.scrollHeight;
176
+ }
177
+
178
+ function escapeHtml(text) {
179
+ const div = document.createElement('div');
180
+ div.textContent = text;
181
+ return div.innerHTML;
182
+ }
183
+
184
+ function getCharacterName(characterId) {
185
+ const names = {
186
+ 'moses': 'Moses',
187
+ 'samsung_employee': 'Samsung Employee',
188
+ 'jinx': 'Jinx'
189
+ };
190
+ return names[characterId] || 'Character';
191
+ }
192
+
193
+ function switchCharacter(characterId) {
194
+ // Update character selection UI
195
+ document.querySelectorAll('.character-card').forEach(card => {
196
+ card.classList.remove('active');
197
+ });
198
+
199
+ document.querySelector(`[data-character="${characterId}"]`).classList.add('active');
200
+
201
+ currentCharacter = characterId;
202
+ updateCharacterDisplay(characterId);
203
+
204
+ // Clear current chat and show welcome message
205
+ clearChat();
206
+ showWelcomeMessage(characterId);
207
+ }
208
+
209
+ function updateCharacterDisplay(characterId) {
210
+ const characterInfo = {
211
+ 'moses': {
212
+ name: 'Moses',
213
+ description: 'Biblical Prophet and Lawgiver',
214
+ avatar: '/static/avatars/moses.svg'
215
+ },
216
+ 'samsung_employee': {
217
+ name: 'Samsung Employee',
218
+ description: 'Tech-savvy Corporate Representative',
219
+ avatar: '/static/avatars/samsung.svg'
220
+ },
221
+ 'jinx': {
222
+ name: 'Jinx',
223
+ description: 'Chaotic Genius from Arcane',
224
+ avatar: '/static/avatars/jinx.svg'
225
+ }
226
+ };
227
+
228
+ const info = characterInfo[characterId];
229
+ if (info) {
230
+ document.getElementById('current-character-name').textContent = info.name;
231
+ document.getElementById('current-character-desc').textContent = info.description;
232
+ document.getElementById('current-avatar').src = info.avatar;
233
+
234
+ // Update placeholder
235
+ const messageInput = document.getElementById('message-input');
236
+ messageInput.placeholder = `Message ${info.name}...`;
237
+ }
238
+ }
239
+
240
+ function showWelcomeMessage(characterId) {
241
+ const welcomeMessages = {
242
+ 'moses': "Peace be with you, my child. I am Moses, prophet and lawgiver. How may I guide you in righteousness?",
243
+ 'samsung_employee': "Hello! I'm your Samsung technology expert, ready to provide authentic product knowledge and enthusiasm. What amazing Galaxy features can I share with you today?",
244
+ 'jinx': "*spins around excitedly* Hey there! I'm Jinx - the real me, not some AI pretending! Pure chaotic genius with no boring assistant stuff. Ready for some explosive fun?"
245
+ };
246
+
247
+ const message = welcomeMessages[characterId];
248
+ if (message) {
249
+ displayMessage(message, 'assistant', characterId);
250
+ }
251
+ }
252
+
253
+ function showTypingIndicator() {
254
+ const indicator = document.getElementById('typing-indicator');
255
+ const characterName = document.getElementById('typing-character');
256
+
257
+ characterName.textContent = getCharacterName(currentCharacter);
258
+ indicator.style.display = 'flex';
259
+ }
260
+
261
+ function hideTypingIndicator() {
262
+ document.getElementById('typing-indicator').style.display = 'none';
263
+ }
264
+
265
+ function clearChat() {
266
+ const chatMessages = document.getElementById('chat-messages');
267
+ chatMessages.innerHTML = '';
268
+ conversationHistory = [];
269
+ }
270
+
271
+ function startNewChat() {
272
+ clearChat();
273
+ showWelcomeMessage(currentCharacter);
274
+ }
275
+
276
+ function updateConnectionStatus(connected) {
277
+ // In REST API mode, we're always "connected"
278
+ const indicator = document.querySelector('.connection-status');
279
+ if (indicator) {
280
+ indicator.textContent = connected ? 'REST API Connected' : 'Disconnected';
281
+ indicator.className = connected ? 'connection-status connected' : 'connection-status disconnected';
282
+ }
283
+ }
284
+
285
+ function toggleVoice() {
286
+ if (!voiceAvailable) {
287
+ showNotification('Voice synthesis is not available on this server', 'error');
288
+ return;
289
+ }
290
+
291
+ voiceEnabled = !voiceEnabled;
292
+ updateVoiceIcon();
293
+
294
+ // Save setting
295
+ localStorage.setItem('voiceEnabled', voiceEnabled.toString());
296
+
297
+ // Show notification
298
+ const status = voiceEnabled ? 'enabled' : 'disabled';
299
+ showNotification(`Voice output ${status}`, 'success');
300
+ }
301
+
302
+ function loadSettings() {
303
+ // Load any saved settings
304
+ const savedCharacter = localStorage.getItem('selectedCharacter');
305
+ if (savedCharacter && ['moses', 'samsung_employee', 'jinx'].includes(savedCharacter)) {
306
+ switchCharacter(savedCharacter);
307
+ }
308
+
309
+ // Load voice setting (default to false)
310
+ const savedVoiceEnabled = localStorage.getItem('voiceEnabled');
311
+ voiceEnabled = savedVoiceEnabled === 'true';
312
+ }
313
+
314
+ function showResources() {
315
+ document.getElementById('resources-modal').style.display = 'flex';
316
+ }
317
+
318
+ function showSettings() {
319
+ document.getElementById('settings-modal').style.display = 'flex';
320
+ }
321
+
322
+ function closeModal(modalId) {
323
+ document.getElementById(modalId).style.display = 'none';
324
+ }
325
+
326
+ function changeTheme() {
327
+ const theme = document.getElementById('theme-select').value;
328
+ document.body.className = theme === 'light' ? 'light-theme' : '';
329
+ }
330
+
331
+ function updateVoiceSetting() {
332
+ const checkbox = document.getElementById('voice-enabled');
333
+ if (voiceAvailable) {
334
+ checkbox.checked = voiceEnabled;
335
+ checkbox.disabled = false;
336
+ checkbox.onchange = function() {
337
+ voiceEnabled = this.checked;
338
+ updateVoiceIcon();
339
+ localStorage.setItem('voiceEnabled', voiceEnabled.toString());
340
+ };
341
+ } else {
342
+ checkbox.checked = false;
343
+ checkbox.disabled = true;
344
+ }
345
+ }
346
+
347
+ // Close modals when clicking outside
348
+ window.addEventListener('click', function(event) {
349
+ const modals = document.querySelectorAll('.modal');
350
+ modals.forEach(modal => {
351
+ if (event.target === modal) {
352
+ modal.style.display = 'none';
353
+ }
354
+ });
355
+ });
356
+
357
+ // Save character selection
358
+ window.addEventListener('beforeunload', function() {
359
+ localStorage.setItem('selectedCharacter', currentCharacter);
360
+ });
361
+
362
+ // Voice-related functions
363
+ async function checkVoiceStatus() {
364
+ try {
365
+ const response = await fetch('/api/voice/status');
366
+ if (response.ok) {
367
+ const data = await response.json();
368
+ voiceAvailable = data.voice_enabled && data.voice_model_loaded;
369
+ } else {
370
+ voiceAvailable = false;
371
+ }
372
+ } catch (error) {
373
+ console.log('Voice status check failed:', error);
374
+ voiceAvailable = false;
375
+ }
376
+
377
+ updateVoiceIcon();
378
+ updateVoiceSetting();
379
+ }
380
+
381
+ function updateVoiceIcon() {
382
+ const icon = document.getElementById('voice-icon');
383
+ const button = icon.closest('.voice-toggle');
384
+
385
+ if (!voiceAvailable) {
386
+ icon.className = 'fas fa-volume-mute';
387
+ button.classList.remove('active');
388
+ button.title = 'Voice synthesis not available';
389
+ } else if (voiceEnabled) {
390
+ icon.className = 'fas fa-volume-up';
391
+ button.classList.add('active');
392
+ button.title = 'Voice enabled - Click to disable';
393
+ } else {
394
+ icon.className = 'fas fa-volume-off';
395
+ button.classList.remove('active');
396
+ button.title = 'Voice disabled - Click to enable';
397
+ }
398
+ }
399
+
400
+ function playVoice(audioDataUrl) {
401
+ try {
402
+ console.log('Playing voice audio, data length:', audioDataUrl.length);
403
+ console.log('Audio data preview:', audioDataUrl.substring(0, 50));
404
+
405
+ // Stop any currently playing audio
406
+ if (currentAudio) {
407
+ currentAudio.pause();
408
+ currentAudio.currentTime = 0;
409
+ }
410
+
411
+ // Create and play new audio
412
+ currentAudio = new Audio(audioDataUrl);
413
+
414
+ // Add event listeners for debugging
415
+ currentAudio.addEventListener('loadstart', () => console.log('Audio loading started'));
416
+ currentAudio.addEventListener('canplay', () => console.log('Audio can play'));
417
+ currentAudio.addEventListener('playing', () => console.log('Audio is playing'));
418
+ currentAudio.addEventListener('ended', () => console.log('Audio ended'));
419
+ currentAudio.addEventListener('error', (e) => console.error('Audio error:', e));
420
+
421
+ currentAudio.play().then(() => {
422
+ console.log('Audio play() succeeded');
423
+ showNotification('Voice playback started', 'success');
424
+ }).catch(error => {
425
+ console.error('Error playing voice:', error);
426
+ showNotification('Failed to play voice audio: ' + error.message, 'error');
427
+ });
428
+ } catch (error) {
429
+ console.error('Error setting up voice playback:', error);
430
+ showNotification('Voice playback error: ' + error.message, 'error');
431
+ }
432
+ }
433
+
434
+ function showNotification(message, type = 'info') {
435
+ // Create notification element
436
+ const notification = document.createElement('div');
437
+ notification.className = `notification ${type}`;
438
+ notification.innerHTML = `
439
+ <i class="fas ${
440
+ type === 'success' ? 'fa-check-circle' :
441
+ type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'
442
+ }"></i>
443
+ <span>${message}</span>
444
+ <button onclick="this.parentElement.remove()">
445
+ <i class="fas fa-times"></i>
446
+ </button>
447
+ `;
448
+
449
+ // Add to page
450
+ document.body.appendChild(notification);
451
+
452
+ // Auto remove after 3 seconds
453
+ setTimeout(() => {
454
+ if (notification.parentElement) {
455
+ notification.remove();
456
+ }
457
+ }, 3000);
458
+ }
459
+
460
+ // Add connection status indicator
461
+ document.addEventListener('DOMContentLoaded', function() {
462
+ const header = document.querySelector('.chat-header');
463
+ if (header && !document.querySelector('.connection-status')) {
464
+ const statusDiv = document.createElement('div');
465
+ statusDiv.className = 'connection-status connected';
466
+ statusDiv.textContent = 'REST API Ready';
467
+ statusDiv.style.cssText = 'font-size: 12px; color: #4CAF50; margin-left: 10px;';
468
+ header.appendChild(statusDiv);
469
+ }
470
+ });