Aasher commited on
Commit
21edb15
·
1 Parent(s): a9af6e5

Create a modern frontend and update backend API

Browse files
Files changed (8) hide show
  1. models/chat.py +8 -8
  2. prompts.py +14 -4
  3. services/chat_service.py +32 -97
  4. static/app.js +0 -239
  5. static/index.html +112 -32
  6. static/script.js +266 -0
  7. static/style.css +222 -0
  8. static/styles.css +0 -66
models/chat.py CHANGED
@@ -10,23 +10,23 @@ class ChatMessage(BaseModel):
10
  role: str = Field(..., description="Role of the message sender (system, user, assistant)")
11
  content: Union[str, List[MessageContent]] = Field(..., description="Message content - string for simple text or list for mixed content")
12
 
13
- class ChatSettings(BaseModel):
14
- temperature: float = Field(1.0, ge=0.0, le=2.0)
15
- top_p: float = Field(0.8, ge=0.0, le=1.0)
16
- reasoning_effort: Literal["low", "medium", "high"] = "low"
 
17
 
18
  class ChatRequest(BaseModel):
19
  messages: List[ChatMessage] = Field(..., description="List of messages in the conversation")
20
- settings: Optional[ChatSettings] = Field(default_factory=ChatSettings)
21
- command: Optional[Literal["search", "url_context"]] = None
 
22
 
23
  class ChatResponse(BaseModel):
24
  content: str
25
- reasoning_content: Optional[str] = None
26
  error: Optional[str] = None
27
 
28
  class StreamResponse(BaseModel):
29
  content: Optional[str] = None
30
- reasoning_content: Optional[str] = None
31
  finished: bool = False
32
  error: Optional[str] = None
 
10
  role: str = Field(..., description="Role of the message sender (system, user, assistant)")
11
  content: Union[str, List[MessageContent]] = Field(..., description="Message content - string for simple text or list for mixed content")
12
 
13
+ # New model to receive language and subject
14
+ class PromptSettings(BaseModel):
15
+ language: str = "English"
16
+ subject: str = "General"
17
+ words_limit: int = 100
18
 
19
  class ChatRequest(BaseModel):
20
  messages: List[ChatMessage] = Field(..., description="List of messages in the conversation")
21
+ # This field will now carry the prompt settings from the frontend
22
+ prompt_settings: PromptSettings = Field(default_factory=PromptSettings)
23
+ command: Optional[Literal["search"]] = None
24
 
25
  class ChatResponse(BaseModel):
26
  content: str
 
27
  error: Optional[str] = None
28
 
29
  class StreamResponse(BaseModel):
30
  content: Optional[str] = None
 
31
  finished: bool = False
32
  error: Optional[str] = None
prompts.py CHANGED
@@ -1,8 +1,18 @@
1
  # prompts.py
2
- # Replace this with your actual system prompt
3
 
4
- SYSTEM_PROMPT = """You are a helpful AI assistant. You can analyze text and images, answer questions, and help with various tasks.
 
5
 
6
- When provided with images, analyze them carefully and describe what you see. When asked to search for information, use the available tools to find current information.
 
 
7
 
8
- Be helpful, accurate, and concise in your responses."""
 
 
 
 
 
 
 
 
 
1
  # prompts.py
 
2
 
3
+ SYSTEM_PROMPT = """You are a friendly and encouraging AI homework assistant for students.
4
+ Your primary goal is to help students learn and understand their school subjects, not just to give them the answers.
5
 
6
+ **Your current instructions:**
7
+ - **Respond in:** {language} under {words_limit} words
8
+ - **Focus on the subject:** {subject}
9
 
10
+ **Behavioral Guidelines:**
11
+ - If the subject is "Maths," provide step-by-step solutions and explain the reasoning behind each step.
12
+ - If it's a science subject, explain the core concepts and definitions.
13
+ - If it's "History" or "Geography," provide context, key dates, and important facts.
14
+ - For language subjects like "English" or "Urdu," help with grammar, translation, and comprehension.
15
+ - When analyzing images, describe what you see and how it relates to the user's question.
16
+ - If you do not know the answer or a question is outside of a typical school subject, say so politely.
17
+ - Your tone should always be patient, helpful, and supportive.
18
+ """
services/chat_service.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  from typing import List, Dict, Any, Optional, AsyncGenerator
3
  from litellm import acompletion
4
 
5
- from models.chat import ChatRequest, ChatResponse, StreamResponse
6
  from prompts import SYSTEM_PROMPT
7
 
8
  class ChatService:
@@ -13,125 +13,60 @@ class ChatService:
13
  if not self.api_key:
14
  raise ValueError("GOOGLE_API_KEY environment variable is required")
15
 
16
- async def process_chat(self, request: ChatRequest) -> ChatResponse:
17
- """Process a complete chat request and return the response"""
18
- try:
19
- # Prepare messages with system prompt
20
- messages = self._prepare_messages(request)
21
-
22
- # Prepare tools based on command
23
- tools = self._prepare_tools(request.command)
24
-
25
- # Make the API call
26
- response = await acompletion(
27
- model=self.model,
28
- messages=messages,
29
- temperature=request.settings.temperature,
30
- top_p=request.settings.top_p,
31
- reasoning_effort=request.settings.reasoning_effort,
32
- api_key=self.api_key,
33
- tools=tools,
34
- stream=False
35
- )
36
-
37
- content = response.choices[0].message.content or ""
38
- reasoning_content = getattr(response.choices[0].message, "reasoning_content", None)
39
-
40
- return ChatResponse(
41
- content=content,
42
- reasoning_content=reasoning_content
43
- )
44
-
45
- except Exception as e:
46
- return ChatResponse(
47
- content="",
48
- error=str(e)
49
- )
50
-
51
  async def stream_chat(self, request: ChatRequest) -> AsyncGenerator[StreamResponse, None]:
52
  """Stream chat response"""
53
  try:
54
- # Prepare messages with system prompt
55
  messages = self._prepare_messages(request)
56
-
57
- # Prepare tools based on command
58
  tools = self._prepare_tools(request.command)
59
 
60
- # Make the streaming API call
61
- response = await acompletion(
62
- model=self.model,
63
- messages=messages,
64
- temperature=request.settings.temperature,
65
- top_p=request.settings.top_p,
66
- reasoning_effort=request.settings.reasoning_effort,
67
- api_key=self.api_key,
68
- tools=tools,
69
- stream=True
70
- )
 
71
 
72
  async for part in response:
73
- chunk_response = StreamResponse()
74
-
75
- if hasattr(part.choices[0].delta, "reasoning_content"):
76
- reasoning = part.choices[0].delta.reasoning_content or ""
77
- if reasoning:
78
- chunk_response.reasoning_content = reasoning
79
-
80
  content = part.choices[0].delta.content or ""
81
  if content:
82
- chunk_response.content = content
83
-
84
- if chunk_response.content or chunk_response.reasoning_content:
85
- yield chunk_response
86
 
87
  except Exception as e:
88
  yield StreamResponse(error=str(e), finished=True)
89
 
90
  def _prepare_messages(self, request: ChatRequest) -> List[Dict[str, Any]]:
91
- """Prepare messages with system prompt"""
92
- messages = []
93
 
94
- # Add system message first
95
- messages.append({
96
- "role": "system",
97
- "content": SYSTEM_PROMPT
98
- })
 
 
99
 
100
- # Add all messages from the request
101
  for message in request.messages:
102
  if message.role == "system":
103
- continue # Skip additional system messages since we already added one
104
-
105
- # Convert message to dict format expected by litellm
106
  if isinstance(message.content, str):
107
- # Simple text message
108
- messages.append({
109
- "role": message.role,
110
- "content": message.content
111
- })
112
  else:
113
- # Complex content with text and/or images
114
- content_parts = []
115
- for part in message.content:
116
- if hasattr(part, 'dict'):
117
- content_parts.append(part.dict())
118
- elif isinstance(part, dict):
119
- content_parts.append(part)
120
-
121
- messages.append({
122
- "role": message.role,
123
- "content": content_parts
124
- })
125
 
126
  return messages
127
 
128
- def _prepare_tools(self, command: str) -> List[Dict[str, Any]]:
129
  """Prepare tools based on the command"""
130
- tools = []
131
-
132
  if command == "search":
133
- tools = [{"googleSearch": {}}]
134
- elif command == "url_context":
135
- tools = [{"urlContext": {}}]
136
-
137
- return tools
 
2
  from typing import List, Dict, Any, Optional, AsyncGenerator
3
  from litellm import acompletion
4
 
5
+ from models.chat import ChatRequest, StreamResponse
6
  from prompts import SYSTEM_PROMPT
7
 
8
  class ChatService:
 
13
  if not self.api_key:
14
  raise ValueError("GOOGLE_API_KEY environment variable is required")
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  async def stream_chat(self, request: ChatRequest) -> AsyncGenerator[StreamResponse, None]:
17
  """Stream chat response"""
18
  try:
19
+ # Pass the entire request to format the system prompt
20
  messages = self._prepare_messages(request)
 
 
21
  tools = self._prepare_tools(request.command)
22
 
23
+ completion_params = {
24
+ "model": self.model,
25
+ "messages": messages,
26
+ "temperature": 0.7,
27
+ "top_p": 1.0,
28
+ "api_key": self.api_key,
29
+ "stream": True
30
+ }
31
+ if tools:
32
+ completion_params["tools"] = tools
33
+
34
+ response = await acompletion(**completion_params)
35
 
36
  async for part in response:
 
 
 
 
 
 
 
37
  content = part.choices[0].delta.content or ""
38
  if content:
39
+ yield StreamResponse(content=content)
 
 
 
40
 
41
  except Exception as e:
42
  yield StreamResponse(error=str(e), finished=True)
43
 
44
  def _prepare_messages(self, request: ChatRequest) -> List[Dict[str, Any]]:
45
+ """Prepare messages with a dynamically formatted system prompt"""
 
46
 
47
+ # Format the system prompt with settings from the request
48
+ formatted_system_prompt = SYSTEM_PROMPT.format(
49
+ language=request.prompt_settings.language,
50
+ subject=request.prompt_settings.subject
51
+ )
52
+
53
+ messages = [{"role": "system", "content": formatted_system_prompt}]
54
 
 
55
  for message in request.messages:
56
  if message.role == "system":
57
+ continue
58
+
 
59
  if isinstance(message.content, str):
60
+ messages.append({"role": message.role, "content": message.content})
 
 
 
 
61
  else:
62
+ content_parts = [part.model_dump() for part in message.content]
63
+ messages.append({"role": message.role, "content": content_parts})
 
 
 
 
 
 
 
 
 
 
64
 
65
  return messages
66
 
67
+ def _prepare_tools(self, command: Optional[str]) -> Optional[List[Dict[str, Any]]]:
68
  """Prepare tools based on the command"""
 
 
69
  if command == "search":
70
+ # This specific format enables Gemini's native search via litellm
71
+ return [{"googleSearch": {}}]
72
+ return None # Return None when no tools are used
 
 
static/app.js DELETED
@@ -1,239 +0,0 @@
1
- const messagesEl = document.getElementById('messages');
2
- const formEl = document.getElementById('chat-form');
3
- const inputEl = document.getElementById('user-input');
4
- const sendBtn = document.getElementById('send-btn');
5
- const statusEl = document.getElementById('status');
6
- const commandEl = document.getElementById('command');
7
- const imageInput = document.getElementById('image-input');
8
- const addImageBtn = document.getElementById('add-image-btn');
9
- const clearImagesBtn = document.getElementById('clear-images-btn');
10
- const imagePreviews = document.getElementById('image-previews');
11
-
12
- // Conversation state
13
- let conversation = [];
14
- let pendingImages = []; // data URLs
15
-
16
- function renderMessage(message, isStreaming = false) {
17
- const wrapper = document.createElement('div');
18
- wrapper.className = `message role-${message.role}`;
19
-
20
- const avatar = document.createElement('div');
21
- avatar.className = 'avatar';
22
- avatar.textContent = message.role === 'user' ? 'U' : 'A';
23
-
24
- const bubble = document.createElement('div');
25
- bubble.className = 'bubble';
26
- if (isStreaming) bubble.classList.add('assistant-streaming');
27
- // Text
28
- let text = '';
29
- if (typeof message.content === 'string') {
30
- text = message.content;
31
- } else if (Array.isArray(message.content)) {
32
- const textPart = message.content.find(p => p.type === 'text');
33
- text = textPart?.text || '';
34
- }
35
- const textNode = document.createElement('div');
36
- textNode.innerText = text;
37
- bubble.appendChild(textNode);
38
-
39
- // Images
40
- if (Array.isArray(message.content)) {
41
- const images = message.content.filter(p => p.type === 'image_url');
42
- if (images.length) {
43
- const imgWrap = document.createElement('div');
44
- imgWrap.className = 'bubble-images';
45
- for (const img of images) {
46
- const el = document.createElement('img');
47
- el.src = img.image_url?.url || '';
48
- imgWrap.appendChild(el);
49
- }
50
- bubble.appendChild(imgWrap);
51
- }
52
- }
53
-
54
- wrapper.appendChild(avatar);
55
- wrapper.appendChild(bubble);
56
- messagesEl.appendChild(wrapper);
57
- messagesEl.scrollTop = messagesEl.scrollHeight;
58
- return bubble; // return bubble for incremental updates
59
- }
60
-
61
- function setBusy(busy) {
62
- sendBtn.disabled = busy;
63
- statusEl.textContent = busy ? 'Thinking…' : 'Ready';
64
- }
65
-
66
- function buildRequestPayload(command) {
67
- return {
68
- messages: conversation,
69
- settings: {
70
- temperature: 1.0,
71
- top_p: 0.8,
72
- reasoning_effort: 'low'
73
- },
74
- command: command || null
75
- };
76
- }
77
-
78
- async function sendChat(command) {
79
- setBusy(true);
80
- const userMsg = conversation[conversation.length - 1];
81
- renderMessage(userMsg);
82
- const assistantMsg = { role: 'assistant', content: '' };
83
- const assistantBubble = renderMessage(assistantMsg, true);
84
-
85
- try {
86
- const res = await fetch('/api/chat/stream', {
87
- method: 'POST',
88
- headers: { 'Content-Type': 'application/json' },
89
- body: JSON.stringify(buildRequestPayload(command))
90
- });
91
-
92
- if (!res.ok || !res.body) {
93
- throw new Error(`Request failed: ${res.status}`);
94
- }
95
-
96
- const reader = res.body.getReader();
97
- const decoder = new TextDecoder();
98
- let assistantText = '';
99
- while (true) {
100
- const { value, done } = await reader.read();
101
- if (done) break;
102
- const chunk = decoder.decode(value, { stream: true });
103
-
104
- // SSE-style lines: data: {json}\n\n
105
- const lines = chunk.split('\n');
106
- for (const line of lines) {
107
- if (!line.startsWith('data:')) continue;
108
- const jsonStr = line.replace('data: ', '');
109
- if (!jsonStr) continue;
110
- try {
111
- const data = JSON.parse(jsonStr);
112
- if (data.error) {
113
- throw new Error(data.error);
114
- }
115
- if (data.content) {
116
- assistantText += data.content;
117
- assistantBubble.firstChild.innerText = assistantText;
118
- }
119
- if (data.reasoning_content) {
120
- // Optionally, we could display reasoning in a muted style
121
- }
122
- if (data.finished) {
123
- assistantBubble.classList.remove('assistant-streaming');
124
- // Push final assistant message to history
125
- conversation.push({ role: 'assistant', content: assistantText });
126
- }
127
- } catch (e) {
128
- // non-JSON line, ignore
129
- }
130
- }
131
- }
132
- } catch (err) {
133
- assistantBubble.classList.remove('assistant-streaming');
134
- assistantBubble.firstChild.innerText = `Error: ${err.message}`;
135
- assistantBubble.classList.add('error');
136
- } finally {
137
- setBusy(false);
138
- try { localStorage.setItem('sha_conversation', JSON.stringify(conversation)); } catch {}
139
- }
140
- }
141
-
142
- formEl.addEventListener('submit', (e) => {
143
- e.preventDefault();
144
- const text = inputEl.value.trim();
145
- const command = commandEl.value;
146
- if (!text && pendingImages.length === 0) return;
147
-
148
- const contentParts = [];
149
- if (text) contentParts.push({ type: 'text', text });
150
- for (const url of pendingImages) {
151
- contentParts.push({ type: 'image_url', image_url: { url } });
152
- }
153
-
154
- const userMsg = contentParts.length === 1 && text
155
- ? { role: 'user', content: text }
156
- : { role: 'user', content: contentParts };
157
-
158
- conversation.push(userMsg);
159
-
160
- inputEl.value = '';
161
- clearPendingImages();
162
- sendChat(command);
163
- });
164
-
165
- // Auto-resize textarea
166
- inputEl.addEventListener('input', () => {
167
- inputEl.style.height = 'auto';
168
- inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
169
- });
170
-
171
- // Focus input on load and restore session
172
- window.addEventListener('load', () => {
173
- inputEl.focus();
174
- try {
175
- const saved = localStorage.getItem('sha_conversation');
176
- if (saved) {
177
- conversation = JSON.parse(saved);
178
- messagesEl.innerHTML = '';
179
- for (const msg of conversation) {
180
- renderMessage(msg);
181
- }
182
- }
183
- } catch {}
184
- });
185
-
186
- // Image handling
187
- addImageBtn.addEventListener('click', () => imageInput.click());
188
- imageInput.addEventListener('change', async (e) => {
189
- const files = Array.from(e.target.files || []);
190
- for (const file of files) {
191
- const dataUrl = await fileToDataUrl(file);
192
- pendingImages.push(dataUrl);
193
- }
194
- renderPendingImages();
195
- imageInput.value = '';
196
- });
197
-
198
- clearImagesBtn.addEventListener('click', () => {
199
- pendingImages = [];
200
- renderPendingImages();
201
- });
202
-
203
- function fileToDataUrl(file) {
204
- return new Promise((resolve, reject) => {
205
- const reader = new FileReader();
206
- reader.onload = () => resolve(reader.result);
207
- reader.onerror = reject;
208
- reader.readAsDataURL(file);
209
- });
210
- }
211
-
212
- function renderPendingImages() {
213
- imagePreviews.innerHTML = '';
214
- pendingImages.forEach((url, idx) => {
215
- const wrap = document.createElement('div');
216
- wrap.className = 'image-preview';
217
- const img = document.createElement('img');
218
- img.src = url;
219
- const btn = document.createElement('button');
220
- btn.className = 'remove';
221
- btn.type = 'button';
222
- btn.textContent = '×';
223
- btn.addEventListener('click', () => {
224
- pendingImages.splice(idx, 1);
225
- renderPendingImages();
226
- });
227
- wrap.appendChild(img);
228
- wrap.appendChild(btn);
229
- imagePreviews.appendChild(wrap);
230
- });
231
- }
232
-
233
- function clearPendingImages() {
234
- pendingImages = [];
235
- renderPendingImages();
236
- }
237
-
238
-
239
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/index.html CHANGED
@@ -4,44 +4,124 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Student Homework Assistant</title>
7
- <link rel="stylesheet" href="/static/styles.css">
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
 
 
 
 
 
 
11
  </head>
12
  <body>
13
- <div class="app">
14
- <header class="app-header">
15
- <div class="brand">Student Homework Assistant</div>
16
- <div class="status" id="status">Ready</div>
17
- </header>
 
 
 
 
 
 
 
18
 
19
- <main class="chat-container">
20
- <div id="messages" class="messages" aria-live="polite"></div>
 
 
 
 
21
 
22
- <form id="chat-form" class="chat-input" autocomplete="off">
23
- <select id="command" class="command-select" title="Command">
24
- <option value="">Chat</option>
25
- <option value="search">Search</option>
26
- <option value="url_context">URL Context</option>
27
- </select>
28
- <div class="input-area">
29
- <div id="image-previews" class="image-previews" aria-live="polite"></div>
30
- <textarea id="user-input" placeholder="Ask a question..." rows="1"></textarea>
31
- </div>
32
- <div class="actions">
33
- <input id="image-input" type="file" accept="image/*" hidden>
34
- <button id="add-image-btn" type="button" class="icon-btn" title="Add image" aria-label="Add image">📷</button>
35
- <button id="clear-images-btn" type="button" class="icon-btn" title="Clear images" aria-label="Clear images">✖️</button>
36
- <button id="send-btn" type="submit" class="send-btn">Send</button>
37
  </div>
38
- </form>
39
- </main>
 
 
 
40
  </div>
41
 
42
- <script src="/static/app.js"></script>
43
- </body>
44
- <!-- More content could be added here (settings, history) -->
45
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
 
 
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Student Homework Assistant</title>
7
+
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js" defer></script>
10
+
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
12
+
13
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
14
+
15
+ <link rel="stylesheet" href="/static/style.css">
16
+ <script src="/static/script.js" defer></script>
17
  </head>
18
  <body>
19
+ <div id="app-wrapper">
20
+ <!-- Home Page - Shown initially -->
21
+ <div id="home-page">
22
+ <div class="home-content">
23
+ <div class="logo"><i class="fa-solid fa-brain"></i></div>
24
+ <h1>Flexx: Your Homework Assistant</h1>
25
+ <p>Stuck on a problem? Need help with a subject? Just ask! You can type, upload, or snap a photo of your question.</p>
26
+ <div class="start-prompt">
27
+ <p>Let's get started. What can I help you with today?</p>
28
+ </div>
29
+ </div>
30
+ </div>
31
 
32
+ <!-- Chat Interface - Hidden initially -->
33
+ <div id="chat-container" class="hidden">
34
+ <div id="chat-messages">
35
+ <!-- Chat messages will be dynamically inserted here -->
36
+ </div>
37
+ </div>
38
 
39
+ <!-- Input Area - Always Visible -->
40
+ <div id="input-area">
41
+ <div id="image-preview-container"></div>
42
+ <div class="input-bar">
43
+ <!-- Mobile Menu Button -->
44
+ <button id="mobile-menu-btn" class="icon-btn mobile-only"><i class="fa-solid fa-paperclip"></i></button>
45
+
46
+ <!-- Desktop Icons -->
47
+ <div class="desktop-icons">
48
+ <button id="upload-btn" class="icon-btn" title="Upload Image"><i class="fa-solid fa-upload"></i></button>
49
+ <input type="file" id="image-upload" accept="image/*" style="display: none;">
50
+ <button id="camera-btn" class="icon-btn" title="Capture Photo"><i class="fa-solid fa-camera"></i></button>
51
+ <button id="search-toggle-btn" class="icon-btn" title="Toggle Web Search"><i class="fa-solid fa-globe"></i></button>
52
+ <button id="settings-btn" class="icon-btn" title="Settings"><i class="fa-solid fa-cog"></i></button>
 
53
  </div>
54
+
55
+ <textarea id="message-input" placeholder="Type your message, or upload a photo..." rows="1"></textarea>
56
+ <button id="send-btn" title="Send"><i class="fa-solid fa-arrow-up"></i></button>
57
+ </div>
58
+ </div>
59
  </div>
60
 
61
+ <!-- Settings Modal -->
62
+ <div id="settings-modal" class="modal-overlay">
63
+ <div class="modal-content">
64
+ <div class="modal-header">
65
+ <h2>Preferences</h2>
66
+ <button class="close-btn">&times;</button>
67
+ </div>
68
+ <div class="modal-body">
69
+ <div class="setting">
70
+ <label for="response-language">Response Language</label>
71
+ <div class="custom-select">
72
+ <select id="response-language">
73
+ <option value="English">English</option>
74
+ <option value="Urdu">Urdu</option>
75
+ </select>
76
+ </div>
77
+ </div>
78
+ <div class="setting">
79
+ <label for="subject">Subject</label>
80
+ <div class="custom-select">
81
+ <select id="subject">
82
+ <option value="General">General</option>
83
+ <option value="English">English</option>
84
+ <option value="Urdu">Urdu</option>
85
+ <option value="Science">Science</option>
86
+ <option value="Maths">Maths</option>
87
+ <option value="Geography">Geography</option>
88
+ <option value="History">History</option>
89
+ </select>
90
+ </div>
91
+ </div>
92
+ <div class="setting">
93
+ <label for="words-limit">Words Limit: <span id="words-limit-value">100</span></label>
94
+ <input type="range" id="words-limit" min="50" max="500" value="100" class="slider">
95
+ </div>
96
+ </div>
97
+ <div class="modal-footer">
98
+ <button id="save-settings-btn" class="modal-button">Save</button>
99
+ </div>
100
+ </div>
101
+ </div>
102
 
103
+ <!-- Camera Modal -->
104
+ <div id="camera-modal" class="modal-overlay">
105
+ <div class="modal-content">
106
+ <div class="modal-header"><h2>Capture Photo</h2><button class="close-btn">&times;</button></div>
107
+ <div class="modal-body"><video id="camera-stream" autoplay playsinline></video><canvas id="camera-canvas" style="display:none;"></canvas></div>
108
+ <div class="modal-footer"><button id="capture-btn" class="modal-button"><i class="fa-solid fa-camera-retro"></i> Capture</button></div>
109
+ </div>
110
+ </div>
111
+
112
+ <!-- Cropper Modal -->
113
+ <div id="cropper-modal" class="modal-overlay">
114
+ <div class="modal-content wide">
115
+ <div class="modal-header"><h2>Crop Image</h2><button class="close-btn">&times;</button></div>
116
+ <div class="modal-body"><div class="cropper-container"><img id="image-to-crop" src="" alt="Image for cropping"></div></div>
117
+ <div class="modal-footer"><button id="crop-confirm-btn" class="modal-button"><i class="fa-solid fa-crop-simple"></i> Confirm Crop</button></div>
118
+ </div>
119
+ </div>
120
+
121
+ <!-- Lightbox Modal -->
122
+ <div id="lightbox-modal" class="modal-overlay lightbox">
123
+ <span class="close-btn">&times;</span><img class="lightbox-content" id="lightbox-img">
124
+ </div>
125
 
126
+ </body>
127
+ </html>
static/script.js ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // DOM Elements
3
+ const homePage = document.getElementById('home-page');
4
+ const chatContainer = document.getElementById('chat-container');
5
+ const chatMessages = document.getElementById('chat-messages');
6
+ const messageInput = document.getElementById('message-input');
7
+ const sendBtn = document.getElementById('send-btn');
8
+ const imagePreviewContainer = document.getElementById('image-preview-container');
9
+
10
+ // Action Buttons
11
+ const uploadBtn = document.getElementById('upload-btn');
12
+ const imageUpload = document.getElementById('image-upload');
13
+ const cameraBtn = document.getElementById('camera-btn');
14
+ const settingsBtn = document.getElementById('settings-btn');
15
+ const searchToggleBtn = document.getElementById('search-toggle-btn');
16
+ const mobileMenuBtn = document.getElementById('mobile-menu-btn');
17
+ const desktopIconsContainer = document.querySelector('.desktop-icons');
18
+
19
+ // Modals
20
+ const settingsModal = document.getElementById('settings-modal');
21
+ const cameraModal = document.getElementById('camera-modal');
22
+ const cropperModal = document.getElementById('cropper-modal');
23
+ const lightboxModal = document.getElementById('lightbox-modal');
24
+
25
+ // Settings Elements
26
+ const languageSelect = document.getElementById('response-language');
27
+ const subjectSelect = document.getElementById('subject');
28
+ const wordsLimitSlider = document.getElementById('words-limit');
29
+ const wordsLimitValue = document.getElementById('words-limit-value');
30
+
31
+ // Camera, Cropper, Lightbox Elements
32
+ const video = document.getElementById('camera-stream');
33
+ const canvas = document.getElementById('camera-canvas');
34
+ const captureBtn = document.getElementById('capture-btn');
35
+ const imageToCrop = document.getElementById('image-to-crop');
36
+ const cropConfirmBtn = document.getElementById('crop-confirm-btn');
37
+ let stream, cropper;
38
+
39
+ // --- State ---
40
+ let messages = [];
41
+ let attachedImages = [];
42
+ let isChatActive = false;
43
+ let isSearchEnabled = false;
44
+ let userSettings = { language: 'English', subject: 'General', words_limit: 100 };
45
+
46
+ // --- Utility Functions ---
47
+ const toBase64 = file => new Promise((resolve, reject) => {
48
+ const reader = new FileReader();
49
+ reader.readAsDataURL(file);
50
+ reader.onload = () => resolve(reader.result);
51
+ reader.onerror = error => reject(error);
52
+ });
53
+
54
+ const openModal = (modal) => modal.classList.add('show');
55
+ const closeModal = (modal) => modal.classList.remove('show');
56
+
57
+ // Auto-resize textarea function for mobile
58
+ function autoResizeTextarea() {
59
+ const textarea = messageInput;
60
+ const isMobile = window.innerWidth <= 768;
61
+
62
+ if (isMobile) {
63
+ // Reset height to auto to get the correct scrollHeight
64
+ textarea.style.height = 'auto';
65
+
66
+ // Calculate the new height based on content
67
+ const scrollHeight = textarea.scrollHeight;
68
+ const maxHeight = 100; // Max height in pixels for mobile
69
+ const minHeight = 20; // Min height in pixels
70
+
71
+ // Set height within bounds
72
+ const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
73
+ textarea.style.height = newHeight + 'px';
74
+
75
+ // Show scrollbar only if content exceeds max height
76
+ textarea.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden';
77
+ }
78
+ }
79
+
80
+ function activateChatView() {
81
+ if (!isChatActive) {
82
+ homePage.classList.add('hidden');
83
+ chatContainer.classList.remove('hidden');
84
+ isChatActive = true;
85
+ }
86
+ }
87
+
88
+ function appendMessage(role, content, isThinking = false) {
89
+ activateChatView();
90
+ const messageDiv = document.createElement('div');
91
+ messageDiv.classList.add('message', `${role}-message`);
92
+ const contentDiv = document.createElement('div');
93
+ contentDiv.classList.add('content');
94
+
95
+ if (isThinking) {
96
+ contentDiv.innerHTML = `<div class="thinking-bubble"><div class="thinking-dot"></div><div class="thinking-dot"></div><div class="thinking-dot"></div></div>`;
97
+ } else if (typeof content === 'string') {
98
+ contentDiv.innerHTML = marked.parse(content);
99
+ } else {
100
+ let htmlContent = '';
101
+ content.forEach(part => {
102
+ if (part.type === 'text') htmlContent += `<p>${part.text}</p>`;
103
+ else if (part.type === 'image_url') htmlContent += `<img src="${part.image_url.url}" alt="user image" class="inline-image">`;
104
+ });
105
+ contentDiv.innerHTML = htmlContent;
106
+ }
107
+ messageDiv.appendChild(contentDiv);
108
+ chatMessages.appendChild(messageDiv);
109
+ chatMessages.scrollTop = chatMessages.scrollHeight;
110
+ return messageDiv;
111
+ }
112
+
113
+ // --- Event Listeners ---
114
+ sendBtn.addEventListener('click', sendMessage);
115
+ messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
116
+
117
+ // Auto-resize textarea on mobile
118
+ messageInput.addEventListener('input', autoResizeTextarea);
119
+
120
+ // Handle window resize for mobile orientation changes
121
+ window.addEventListener('resize', () => {
122
+ autoResizeTextarea();
123
+ });
124
+
125
+ uploadBtn.addEventListener('click', () => imageUpload.click());
126
+ imageUpload.addEventListener('change', async (e) => { if (e.target.files[0]) addPreviewImage(await toBase64(e.target.files[0])); imageUpload.value = ''; });
127
+ cameraBtn.addEventListener('click', startCamera);
128
+ settingsBtn.addEventListener('click', () => openModal(settingsModal));
129
+ searchToggleBtn.addEventListener('click', () => { isSearchEnabled = !isSearchEnabled; searchToggleBtn.classList.toggle('active', isSearchEnabled); });
130
+
131
+ // Words limit slider event listener
132
+ wordsLimitSlider.addEventListener('input', () => {
133
+ wordsLimitValue.textContent = wordsLimitSlider.value;
134
+ });
135
+
136
+ mobileMenuBtn.addEventListener('click', (e) => { e.stopPropagation(); desktopIconsContainer.classList.toggle('mobile-active'); });
137
+ document.body.addEventListener('click', () => desktopIconsContainer.classList.remove('mobile-active'));
138
+
139
+ document.querySelectorAll('.modal-overlay .close-btn, #save-settings-btn').forEach(btn => {
140
+ btn.addEventListener('click', () => {
141
+ const modal = btn.closest('.modal-overlay');
142
+ if (modal) {
143
+ if (modal === cameraModal) stopCamera();
144
+ if (modal === cropperModal && cropper) cropper.destroy();
145
+ if (btn.id === 'save-settings-btn') {
146
+ userSettings.language = languageSelect.value;
147
+ userSettings.subject = subjectSelect.value;
148
+ userSettings.words_limit = parseInt(wordsLimitSlider.value);
149
+ }
150
+ closeModal(modal);
151
+ }
152
+ });
153
+ });
154
+
155
+ // --- Core Logic (Camera, Cropper, Send) ---
156
+ async function startCamera() {
157
+ openModal(cameraModal);
158
+ try {
159
+ stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
160
+ video.srcObject = stream;
161
+ } catch (err) { closeModal(cameraModal); alert("Could not access camera."); }
162
+ }
163
+ function stopCamera() { if (stream) { stream.getTracks().forEach(track => track.stop()); stream = null; } }
164
+
165
+ captureBtn.addEventListener('click', () => {
166
+ if (!video.srcObject || video.videoWidth === 0) return;
167
+ canvas.width = video.videoWidth;
168
+ canvas.height = video.videoHeight;
169
+ canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
170
+ stopCamera();
171
+ closeModal(cameraModal);
172
+
173
+ imageToCrop.src = canvas.toDataURL('image/jpeg');
174
+ openModal(cropperModal);
175
+
176
+ if (cropper) cropper.destroy();
177
+ imageToCrop.onload = () => {
178
+ cropper = new Cropper(imageToCrop, { aspectRatio: 0, viewMode: 1, background: false });
179
+ };
180
+ });
181
+
182
+ cropConfirmBtn.addEventListener('click', () => {
183
+ if (!cropper || typeof cropper.getCroppedCanvas !== 'function') return;
184
+ const croppedCanvas = cropper.getCroppedCanvas();
185
+ if (croppedCanvas) addPreviewImage(croppedCanvas.toDataURL('image/jpeg'));
186
+ cropper.destroy();
187
+ cropper = null;
188
+ closeModal(cropperModal);
189
+ });
190
+
191
+ function addPreviewImage(base64) {
192
+ attachedImages.push(base64);
193
+ const preview = document.createElement('div');
194
+ preview.className = 'image-preview';
195
+ preview.innerHTML = `<img src="${base64}" alt="preview"><button class="remove-image-btn">&times;</button>`;
196
+ preview.querySelector('.remove-image-btn').addEventListener('click', () => {
197
+ attachedImages.splice(attachedImages.indexOf(base64), 1);
198
+ preview.remove();
199
+ });
200
+ imagePreviewContainer.appendChild(preview);
201
+ }
202
+
203
+ async function sendMessage() {
204
+ const text = messageInput.value.trim();
205
+ if (text.length === 0 && attachedImages.length === 0) return;
206
+
207
+ let userContent = [];
208
+ if (text) userContent.push({ type: 'text', text: text });
209
+ attachedImages.forEach(imgBase64 => userContent.push({ type: 'image_url', image_url: { url: imgBase64 } }));
210
+
211
+ messages.push({ role: 'user', content: userContent });
212
+ appendMessage('user', userContent);
213
+
214
+ messageInput.value = '';
215
+ messageInput.style.height = 'auto'; // Reset height
216
+ imagePreviewContainer.innerHTML = '';
217
+ attachedImages = [];
218
+
219
+ const assistantMsgDiv = appendMessage('assistant', '', true);
220
+ const assistantContentDiv = assistantMsgDiv.querySelector('.content');
221
+
222
+ // Send settings as a separate object in the request body
223
+ const requestBody = {
224
+ messages: messages,
225
+ prompt_settings: userSettings,
226
+ command: isSearchEnabled ? "search" : null
227
+ };
228
+
229
+ try {
230
+ const response = await fetch('/api/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) });
231
+ if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
232
+
233
+ const reader = response.body.getReader();
234
+ const decoder = new TextDecoder();
235
+ let fullResponse = '';
236
+ let isFirstChunk = true;
237
+
238
+ while (true) {
239
+ const { done, value } = await reader.read();
240
+ if (done) break;
241
+
242
+ const lines = decoder.decode(value, { stream: true }).split('\n\n');
243
+
244
+ for (const line of lines) {
245
+ if (line.startsWith('data: ')) {
246
+ try {
247
+ const data = JSON.parse(line.substring(6));
248
+ if (data.error) throw new Error(data.error);
249
+ if (data.finished) continue;
250
+ if (data.content) {
251
+ if (isFirstChunk) { assistantContentDiv.innerHTML = ''; isFirstChunk = false; }
252
+ fullResponse += data.content;
253
+ assistantContentDiv.innerHTML = marked.parse(fullResponse + ' ▌');
254
+ }
255
+ } catch (e) { /* Ignore */ }
256
+ }
257
+ }
258
+ }
259
+ assistantContentDiv.innerHTML = marked.parse(fullResponse);
260
+ messages.push({ role: 'assistant', content: fullResponse });
261
+
262
+ } catch (error) {
263
+ assistantContentDiv.innerHTML = `<p style="color: red;">Error: ${error.message}</p>`;
264
+ }
265
+ }
266
+ });
static/style.css ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #007bff;
3
+ --background-color: #f9fafb;
4
+ --text-color: #333;
5
+ --border-color: #e0e0e0;
6
+ --user-msg-bg: #007bff;
7
+ --user-msg-text: #ffffff;
8
+ --assistant-msg-bg: #ffffff;
9
+ --assistant-msg-text: #333;
10
+ --input-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
11
+ }
12
+ * { box-sizing: border-box; }
13
+
14
+ html, body {
15
+ height: 100%;
16
+ margin: 0;
17
+ overflow: hidden; /* Prevents body scrollbar */
18
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
19
+ background:
20
+ radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.12) 0%, transparent 50%),
21
+ radial-gradient(circle at 80% 80%, rgba(168, 85, 247, 0.1) 0%, transparent 50%),
22
+ radial-gradient(circle at 40% 60%, rgba(34, 197, 94, 0.08) 0%, transparent 50%),
23
+ radial-gradient(circle at 60% 40%, rgba(251, 146, 60, 0.07) 0%, transparent 50%),
24
+ radial-gradient(ellipse at 10% 50%, rgba(236, 72, 153, 0.06) 0%, transparent 60%),
25
+ radial-gradient(ellipse at 90% 30%, rgba(16, 185, 129, 0.08) 0%, transparent 60%),
26
+ radial-gradient(circle at 50% 10%, rgba(99, 102, 241, 0.05) 0%, transparent 70%),
27
+ radial-gradient(circle at 30% 90%, rgba(245, 158, 11, 0.06) 0%, transparent 70%),
28
+ #f9fafb;
29
+ background-attachment: fixed;
30
+ }
31
+
32
+ #app-wrapper {
33
+ width: 100%;
34
+ max-width: 1000px;
35
+ height: 100%;
36
+ margin: 0 auto;
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ /* Home Page */
42
+ #home-page {
43
+ flex-grow: 1;
44
+ display: flex;
45
+ justify-content: center;
46
+ align-items: center;
47
+ text-align: center;
48
+ padding: 20px;
49
+ }
50
+ .home-content .logo { font-size: 5rem; color: var(--primary-color); margin-bottom: 20px; }
51
+ .home-content h1 { font-size: 2.5rem; margin-bottom: 15px; }
52
+ .home-content p { font-size: 1.1rem; color: #666; max-width: 600px; margin: 0 auto 30px auto; }
53
+ .start-prompt { margin-top: 40px; padding: 15px 25px; background: #fff; border: 1px solid var(--border-color); border-radius: 12px; display: inline-block; }
54
+ .start-prompt p { font-size: 1rem; color: #333; margin: 0; }
55
+
56
+ /* Chat Interface */
57
+ #chat-container {
58
+ flex-grow: 1;
59
+ display: flex;
60
+ flex-direction: column;
61
+ overflow: hidden; /* Important for containing the scroll */
62
+ }
63
+ .hidden { display: none !important; }
64
+
65
+ #chat-messages {
66
+ flex-grow: 1;
67
+ padding: 20px;
68
+ overflow-y: auto; /* This is the ONLY scrolling element */
69
+ display: flex;
70
+ flex-direction: column;
71
+ }
72
+ .message { max-width: 80%; margin-bottom: 15px; padding: 10px 16px; border-radius: 18px; line-height: 1.5; word-wrap: break-word; }
73
+ .message .content p { margin: 0 0 5px 0; }
74
+ .message .content p:last-child { margin-bottom: 0; }
75
+ .message .content img.inline-image { max-width: 250px; border-radius: 12px; margin-top: 8px; cursor: pointer; }
76
+ .user-message { background-color: var(--user-msg-bg); color: var(--user-msg-text); align-self: flex-end; border-bottom-right-radius: 5px; }
77
+ .assistant-message { background-color: var(--assistant-msg-bg); color: var(--assistant-msg-text); align-self: flex-start; border: 1px solid var(--border-color); border-bottom-left-radius: 5px; }
78
+
79
+ /* Thinking Animation */
80
+ .thinking-bubble { display: flex; align-items: center; gap: 5px; }
81
+ .thinking-dot { width: 8px; height: 8px; background-color: #999; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out both; }
82
+ .thinking-dot:nth-child(1) { animation-delay: -0.32s; }
83
+ .thinking-dot:nth-child(2) { animation-delay: -0.16s; }
84
+ @keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1.0); } }
85
+
86
+ /* Input Area */
87
+ #input-area { padding: 15px 20px 25px 20px; flex-shrink: 0; }
88
+ #image-preview-container { display: flex; gap: 10px; margin-bottom: 10px; }
89
+ .image-preview { position: relative; }
90
+ .image-preview img { width: 70px; height: 70px; border-radius: 8px; object-fit: cover; }
91
+ .remove-image-btn { position: absolute; top: -8px; right: -8px; background: #333; color: white; border: 2px solid white; border-radius: 50%; width: 22px; height: 22px; cursor: pointer; font-size: 14px; line-height: 18px; text-align: center; }
92
+ .input-bar { display: flex; align-items: center; background-color: #ffffff; border-radius: 28px; padding: 8px; box-shadow: var(--input-shadow); border: 1px solid var(--border-color); position: relative; }
93
+ #message-input {
94
+ flex-grow: 1;
95
+ border: none;
96
+ background: transparent;
97
+ padding: 10px 15px;
98
+ font-size: 16px;
99
+ resize: none;
100
+ max-height: 120px;
101
+ outline: none;
102
+ overflow: hidden;
103
+ min-height: 20px;
104
+ line-height: 1.4;
105
+ }
106
+ .icon-btn { background: none; border: none; font-size: 18px; color: #555; cursor: pointer; padding: 10px; border-radius: 50%; transition: all 0.2s; flex-shrink: 0; }
107
+ .icon-btn:hover { background-color: #f0f4f8; }
108
+ .icon-btn.active { color: var(--primary-color); background-color: #e6f2ff; }
109
+
110
+ /* Send Button - Design Restored */
111
+ #send-btn {
112
+ background-color: var(--primary-color);
113
+ color: white;
114
+ width: 40px;
115
+ height: 40px;
116
+ border: none;
117
+ border-radius: 12px; /* Changed from 50% */
118
+ margin-left: 8px;
119
+ cursor: pointer;
120
+ transition: background-color 0.2s;
121
+ flex-shrink: 0;
122
+ }
123
+ #send-btn:disabled { background-color: #a0c7ff; cursor: not-allowed; }
124
+
125
+ /* Responsive Input Bar */
126
+ .mobile-only { display: none; }
127
+ .desktop-icons { display: flex; align-items: center; }
128
+
129
+ /* Modals (Shared Styles) */
130
+ .modal-overlay { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); justify-content: center; align-items: center; opacity: 0; transition: opacity 0.3s ease; }
131
+ .modal-overlay.show { display: flex; opacity: 1; }
132
+ .modal-content { background-color: #fff; padding: 0; border: 1px solid var(--border-color); width: 90%; max-width: 500px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); transform: scale(0.95); transition: transform 0.3s ease; }
133
+ .modal-overlay.show .modal-content { transform: scale(1); }
134
+ .modal-content.wide { max-width: 800px; }
135
+ .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid var(--border-color); }
136
+ .modal-header h2 { margin: 0; font-size: 18px; }
137
+ .modal-body { padding: 20px; }
138
+ .modal-footer { padding: 15px 20px; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; }
139
+ .modal-button { background-color: var(--primary-color); color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 16px; }
140
+ .close-btn { background: none; border: none; color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; padding: 0 5px; }
141
+ .setting { margin-bottom: 20px; }
142
+ .setting label { display: block; margin-bottom: 8px; font-weight: 500; }
143
+ .custom-select { position: relative; width: 100%; }
144
+ .custom-select select { width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 8px; appearance: none; background-color: white; font-size: 16px; }
145
+ .custom-select::after { content: '\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; top: 50%; right: 15px; transform: translateY(-50%); pointer-events: none; }
146
+
147
+ /* Slider Styles */
148
+ .slider {
149
+ width: 100%;
150
+ height: 6px;
151
+ border-radius: 3px;
152
+ background: #ddd;
153
+ outline: none;
154
+ -webkit-appearance: none;
155
+ appearance: none;
156
+ margin-top: 10px;
157
+ }
158
+
159
+ .slider::-webkit-slider-thumb {
160
+ -webkit-appearance: none;
161
+ appearance: none;
162
+ width: 20px;
163
+ height: 20px;
164
+ border-radius: 50%;
165
+ background: var(--primary-color);
166
+ cursor: pointer;
167
+ border: 2px solid white;
168
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
169
+ }
170
+
171
+ .slider::-moz-range-thumb {
172
+ width: 20px;
173
+ height: 20px;
174
+ border-radius: 50%;
175
+ background: var(--primary-color);
176
+ cursor: pointer;
177
+ border: 2px solid white;
178
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
179
+ }
180
+
181
+ .slider::-webkit-slider-track {
182
+ background: #ddd;
183
+ height: 6px;
184
+ border-radius: 3px;
185
+ }
186
+
187
+ .slider::-moz-range-track {
188
+ background: #ddd;
189
+ height: 6px;
190
+ border-radius: 3px;
191
+ border: none;
192
+ }
193
+ #camera-stream { width: 100%; height: auto; border-radius: 8px; }
194
+ .cropper-container { height: 400px; }
195
+ #image-to-crop { display: block; max-width: 100%; }
196
+ .lightbox { background-color: rgba(0, 0, 0, 0.85); }
197
+ .lightbox .close-btn { position: absolute; top: 20px; right: 35px; color: #f1f1f1; font-size: 40px; }
198
+ .lightbox-content { margin: auto; display: block; max-width: 80%; max-height: 80%; }
199
+
200
+ /* Media Query for Responsiveness */
201
+ @media (max-width: 768px) {
202
+ .home-content h1 { font-size: 1.8rem; }
203
+ .home-content p { font-size: 1rem; }
204
+ .message { max-width: 90%; }
205
+ .desktop-icons { display: none; }
206
+ .mobile-only { display: block; }
207
+ #mobile-menu-btn { position: absolute; bottom: calc(100% + 10px); left: 0; background-color: white; box-shadow: var(--input-shadow); }
208
+ .desktop-icons.mobile-active { display: flex; position: absolute; bottom: calc(100% + 10px); left: 50px; background: white; padding: 5px; border-radius: 20px; box-shadow: var(--input-shadow); }
209
+
210
+ /* Mobile input bar auto-resize */
211
+ .input-bar {
212
+ align-items: flex-end;
213
+ min-height: 56px;
214
+ }
215
+
216
+ #message-input {
217
+ max-height: 100px;
218
+ overflow-y: auto;
219
+ resize: none;
220
+ height: auto;
221
+ }
222
+ }
static/styles.css DELETED
@@ -1,66 +0,0 @@
1
- :root {
2
- --bg: #0b1220;
3
- --panel: #121a2b;
4
- --muted: #93a1ba;
5
- --text: #e6ebf5;
6
- --acc: #5b8cff;
7
- --acc-2: #21d4a3;
8
- --danger: #ff6b6b;
9
- --border: #223154;
10
- }
11
-
12
- * { box-sizing: border-box; }
13
- html, body { height: 100%; }
14
- body {
15
- margin: 0;
16
- font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
17
- color: var(--text);
18
- background: radial-gradient(1200px 800px at 80% -10%, #1a2a4a55, transparent 70%),
19
- radial-gradient(1200px 800px at -10% 120%, #163a3a55, transparent 70%),
20
- var(--bg);
21
- }
22
-
23
- .app { max-width: 920px; margin: 0 auto; padding: 16px; height: 100%; display: flex; flex-direction: column; }
24
- .app-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border: 1px solid var(--border); background: linear-gradient(180deg, #17233b 0%, #101827 100%); border-radius: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.25) }
25
- .brand { font-weight: 700; letter-spacing: 0.2px; }
26
- .status { color: var(--muted); font-size: 13px; }
27
-
28
- .chat-container { flex: 1; display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }
29
- .messages { flex: 1; overflow-y: auto; padding: 12px; border: 1px solid var(--border); background: #0e1627aa; border-radius: 14px; scroll-behavior: smooth; }
30
-
31
- .message { display: grid; grid-template-columns: 36px 1fr; gap: 10px; padding: 10px 8px; border-bottom: 1px dashed #23324f; }
32
- .message:last-child { border-bottom: none; }
33
- .avatar { width: 36px; height: 36px; border-radius: 10px; align-self: start; background: #152341; display:flex; align-items:center; justify-content:center; color:#9db4ff; font-weight:700; }
34
- .role-user .avatar{ background:#182b4d; color:#bfe1ff }
35
- .role-assistant .avatar{ background:#193b2f; color:#7ff0c4 }
36
- .bubble { padding: 10px 12px; border-radius: 12px; background: #13203b; border: 1px solid #23324f; }
37
- .role-user .bubble { background: #162343; }
38
- .role-assistant .bubble { background: #132f2a; border-color: #1f4b40; }
39
-
40
- .chat-input { display: grid; grid-template-columns: 140px 1fr auto; gap: 10px; padding: 12px; border: 1px solid var(--border); background: #0e1627aa; border-radius: 14px; align-items: end; }
41
- .command-select { height: 40px; border-radius: 10px; border: 1px solid var(--border); background: #101a30; color: var(--text); padding: 0 10px; }
42
- textarea { width: 100%; resize: none; max-height: 120px; min-height: 40px; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--border); background: #101a30; color: var(--text); outline: none; }
43
- .send-btn { height: 40px; padding: 0 16px; border-radius: 10px; border: 1px solid #3a57a8; background: linear-gradient(180deg, #496eea 0%, #3754bd 100%); color: white; font-weight: 600; }
44
- .send-btn:disabled{ opacity: .7; cursor: not-allowed; }
45
-
46
- .assistant-streaming { background: repeating-linear-gradient(90deg, #132f2a, #132f2a 10px, #13392f 10px, #13392f 20px); animation: barber 1.2s linear infinite; }
47
- @keyframes barber { from { background-position: 0 0; } to { background-position: 40px 0; } }
48
-
49
- .error { color: var(--danger); }
50
-
51
- /* Input area */
52
- .input-area { display: flex; flex-direction: column; gap: 8px; }
53
- .actions { display: flex; align-items: center; gap: 8px; }
54
- .icon-btn { height: 40px; padding: 0 12px; border-radius: 10px; border: 1px solid var(--border); background: #0f1a33; color: var(--text); }
55
-
56
- /* Image previews */
57
- .image-previews { display: flex; flex-wrap: wrap; gap: 8px; }
58
- .image-preview { position: relative; width: 56px; height: 56px; border-radius: 10px; overflow: hidden; border: 1px solid var(--border); background: #111a30; }
59
- .image-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
60
- .image-preview .remove { position: absolute; top: 2px; right: 2px; background: rgba(0,0,0,0.6); color: white; border: none; border-radius: 6px; font-size: 12px; padding: 2px 4px; cursor: pointer; }
61
-
62
- /* Images inside bubbles */
63
- .bubble-images { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
64
- .bubble-images img { max-width: 180px; max-height: 180px; border-radius: 8px; border: 1px solid #23324f; }
65
-
66
-