Krishkanth commited on
Commit
999fe83
·
1 Parent(s): 6d574ae

Update: ChatGPT-style file upload, PDF/DOCX/PPT support

Browse files
Files changed (4) hide show
  1. .gitignore +0 -0
  2. app_backup.py +596 -0
  3. requirements.txt +10 -0
  4. static/index.html +436 -16
.gitignore CHANGED
Binary files a/.gitignore and b/.gitignore differ
 
app_backup.py ADDED
@@ -0,0 +1,596 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ Krish Mind Local Server (GGUF)
4
+ ==============================
5
+ Works with index_local.html
6
+ Features: GGUF Model + Web Search + Image Generation + RAG + File Upload
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import urllib.parse
12
+ import pickle
13
+ import tempfile
14
+ from datetime import datetime
15
+
16
+ print("=" * 60)
17
+ print("🧠 Krish Mind Local Server (GGUF)")
18
+ print("=" * 60)
19
+
20
+ # --- Core dependencies ---
21
+ try:
22
+ from llama_cpp import Llama
23
+ print("✅ llama-cpp-python")
24
+ except ImportError:
25
+ print("❌ Run: pip install llama-cpp-python")
26
+ sys.exit(1)
27
+
28
+ try:
29
+ from fastapi import FastAPI, UploadFile, File
30
+ from fastapi.middleware.cors import CORSMiddleware
31
+ from pydantic import BaseModel
32
+ import uvicorn
33
+ print("✅ fastapi + uvicorn")
34
+ except ImportError:
35
+ print("❌ Run: pip install fastapi uvicorn python-multipart")
36
+ sys.exit(1)
37
+
38
+ # --- Config ---
39
+ GGUF_PATH = "d:/Krish Mind/gguf/krish-mind-standalone-Q4.gguf"
40
+ EMBEDDINGS_FILE = "../data/krce_embeddings.pkl"
41
+ DATA_FILE = "../data/krce_college_data.jsonl"
42
+
43
+ # --- Load GGUF Model ---
44
+ print(f"\n⏳ Loading GGUF model...")
45
+ try:
46
+ model = Llama(
47
+ model_path=GGUF_PATH,
48
+ n_ctx=4096,
49
+ n_gpu_layers=0,
50
+ verbose=False
51
+ )
52
+ print("✅ Model loaded!")
53
+ except Exception as e:
54
+ print(f"❌ Model error: {e}")
55
+ sys.exit(1)
56
+
57
+ # --- DuckDuckGo Web Search ---
58
+ print("\n📦 Loading optional features...")
59
+ ddgs = None
60
+ try:
61
+ import warnings
62
+ warnings.filterwarnings("ignore")
63
+ from duckduckgo_search import DDGS
64
+ ddgs = DDGS()
65
+ print("✅ DuckDuckGo web search")
66
+ except Exception as e:
67
+ print(f"⚠️ Web search disabled: {e}")
68
+
69
+ # --- RAG SETUP (Load Pre-computed Embeddings) ---
70
+ print("📚 Loading Knowledge Base...")
71
+ knowledge_base = []
72
+ doc_embeddings = None
73
+ rag_model = None
74
+
75
+ # Try to load pre-computed embeddings first
76
+ if os.path.exists(EMBEDDINGS_FILE):
77
+ try:
78
+ import numpy as np
79
+ from sentence_transformers import SentenceTransformer
80
+
81
+ print(f"📂 Loading pre-computed embeddings from {EMBEDDINGS_FILE}...")
82
+ with open(EMBEDDINGS_FILE, 'rb') as f:
83
+ data = pickle.load(f)
84
+
85
+ knowledge_base = data['knowledge_base']
86
+ doc_embeddings = data['embeddings']
87
+
88
+ # Load the model for query encoding (needed for search)
89
+ rag_model = SentenceTransformer(data.get('model_name', 'all-MiniLM-L6-v2'))
90
+ print(f"✅ Embeddings loaded! ({len(knowledge_base)} facts)")
91
+
92
+ except Exception as e:
93
+ print(f"⚠️ Could not load embeddings: {e}")
94
+ print(" Falling back to live embedding...")
95
+
96
+ # Fallback: compute embeddings if pkl not found
97
+ if doc_embeddings is None and os.path.exists(DATA_FILE):
98
+ try:
99
+ from sentence_transformers import SentenceTransformer
100
+ import numpy as np
101
+ import json
102
+
103
+ rag_model = SentenceTransformer('all-MiniLM-L6-v2')
104
+ print("✅ Embedding model loaded (SentenceTransformer)")
105
+
106
+ with open(DATA_FILE, 'r') as f:
107
+ for line in f:
108
+ if line.strip():
109
+ try:
110
+ knowledge_base.append(json.loads(line))
111
+ except:
112
+ pass
113
+
114
+ if knowledge_base:
115
+ docs = [f"{k['instruction']} {k['output']}" for k in knowledge_base]
116
+ doc_embeddings = rag_model.encode(docs)
117
+ print(f"✅ Embeddings computed! ({len(knowledge_base)} facts)")
118
+ print(" ⚠️ Run 'python scripts/build_embeddings.py' for faster startup!")
119
+
120
+ except Exception as e:
121
+ print(f"❌ RAG disabled: {e}")
122
+ rag_model = None
123
+ else:
124
+ if doc_embeddings is None:
125
+ print("⚠️ Data file not found! RAG disabled.")
126
+
127
+ # Initialize Cross-Encoder for re-ranking
128
+ cross_encoder = None
129
+ try:
130
+ from sentence_transformers import CrossEncoder
131
+ cross_encoder = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
132
+ print("✅ Cross-Encoder loaded for re-ranking")
133
+ except Exception as e:
134
+ print(f"⚠️ Cross-Encoder not available: {e}")
135
+
136
+ # ============================================
137
+ # FILE UPLOAD RAG: Session-based document analysis
138
+ # ============================================
139
+ # Store uploaded file embeddings per session (in-memory)
140
+ session_file_data = {}
141
+
142
+ def extract_text_from_file(file_path: str, filename: str) -> str:
143
+ """Extract text from uploaded file"""
144
+ text = ""
145
+ ext = filename.lower().split('.')[-1]
146
+
147
+ try:
148
+ if ext == 'txt':
149
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
150
+ text = f.read()
151
+
152
+ elif ext == 'pdf':
153
+ try:
154
+ import PyPDF2
155
+ with open(file_path, 'rb') as f:
156
+ reader = PyPDF2.PdfReader(f)
157
+ for page in reader.pages:
158
+ text += page.extract_text() + "\n"
159
+ except ImportError:
160
+ try:
161
+ import fitz # PyMuPDF
162
+ doc = fitz.open(file_path)
163
+ for page in doc:
164
+ text += page.get_text() + "\n"
165
+ doc.close()
166
+ except ImportError:
167
+ return "Error: Install PyPDF2 or PyMuPDF to read PDFs"
168
+
169
+ elif ext in ['doc', 'docx']:
170
+ try:
171
+ import docx
172
+ doc = docx.Document(file_path)
173
+ text = "\n".join([para.text for para in doc.paragraphs])
174
+ except ImportError:
175
+ return "Error: Install python-docx to read Word files"
176
+
177
+ elif ext in ['md', 'json', 'csv']:
178
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
179
+ text = f.read()
180
+
181
+ else:
182
+ # Try reading as plain text
183
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
184
+ text = f.read()
185
+
186
+ except Exception as e:
187
+ return f"Error reading file: {e}"
188
+
189
+ return text.strip()
190
+
191
+ def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list:
192
+ """Split text into chunks for embedding"""
193
+ words = text.split()
194
+ chunks = []
195
+
196
+ for i in range(0, len(words), chunk_size - overlap):
197
+ chunk = " ".join(words[i:i + chunk_size])
198
+ if chunk.strip():
199
+ chunks.append(chunk)
200
+
201
+ return chunks
202
+
203
+ # ============================================
204
+ # ADVANCED RAG: Query Expansion + Hybrid Search
205
+ # ============================================
206
+
207
+ ABBREVIATIONS = {
208
+ "aids": "AI&DS Artificial Intelligence and Data Science",
209
+ "ai&ds": "AI&DS Artificial Intelligence and Data Science",
210
+ "aid": "AI&DS Artificial Intelligence and Data Science",
211
+ "aiml": "AI&ML Artificial Intelligence and Machine Learning",
212
+ "ai&ml": "AI&ML Artificial Intelligence and Machine Learning",
213
+ "cse": "Computer Science Engineering CSE",
214
+ "ece": "Electronics Communication Engineering ECE",
215
+ "eee": "Electrical Electronics Engineering EEE",
216
+ "mech": "Mechanical Engineering",
217
+ "it": "Information Technology IT",
218
+ "hod": "Head of Department HOD",
219
+ "mam": "madam professor female faculty",
220
+ "sir": "male professor faculty",
221
+ "staffs": "staff faculty members",
222
+ "sase": "Sasikumar Sasidevi",
223
+ "krce": "K. Ramakrishnan College of Engineering",
224
+ }
225
+
226
+ def expand_query(query):
227
+ """Expand abbreviations and add synonyms for better matching"""
228
+ expanded = query.lower()
229
+ for abbr, full in ABBREVIATIONS.items():
230
+ if abbr in expanded.split():
231
+ expanded = expanded + " " + full
232
+ return expanded
233
+
234
+ def search_krce(query, threshold=0.25):
235
+ """Advanced RAG: Query Expansion + Vector Search + Cross-Encoder Re-ranking"""
236
+ if not rag_model or doc_embeddings is None:
237
+ return ""
238
+
239
+ try:
240
+ expanded_query = expand_query(query)
241
+
242
+ print(f"\n📊 RAG Search")
243
+ print(f" Original: '{query}'")
244
+ print(f" Expanded: '{expanded_query}'")
245
+ print("-" * 50)
246
+
247
+ from sklearn.metrics.pairwise import cosine_similarity
248
+ q_emb = rag_model.encode([expanded_query])
249
+ vector_scores = cosine_similarity(q_emb, doc_embeddings).flatten()
250
+
251
+ top_indices = vector_scores.argsort()[-10:][::-1]
252
+ top_candidates = [(idx, vector_scores[idx]) for idx in top_indices]
253
+
254
+ print("Vector Search Results:")
255
+ for i, (idx, v) in enumerate(top_candidates[:5]):
256
+ instruction = knowledge_base[idx]['instruction'][:35]
257
+ print(f" #{i+1} V:{v:.3f} | {instruction}...")
258
+
259
+ if cross_encoder:
260
+ pairs = [[query, f"{knowledge_base[idx]['instruction']} {knowledge_base[idx]['output']}"]
261
+ for idx, _ in top_candidates]
262
+ ce_scores = cross_encoder.predict(pairs)
263
+
264
+ final_ranking = sorted(zip(top_candidates, ce_scores), key=lambda x: x[1], reverse=True)
265
+
266
+ print("Cross-Encoder Re-ranking:")
267
+ for i, ((idx, v), ce) in enumerate(final_ranking[:3]):
268
+ instruction = knowledge_base[idx]['instruction'][:35]
269
+ print(f" #{i+1} CE:{ce:.3f} | {instruction}...")
270
+ print("-" * 50)
271
+
272
+ final_context = []
273
+ print(f"✅ RAG Retrieval (Top 5):")
274
+ for i, ((idx, v), ce) in enumerate(final_ranking[:5]):
275
+ if ce > -6.0:
276
+ content = knowledge_base[idx]['output']
277
+ final_context.append(content)
278
+ print(f" Took #{i+1}: {knowledge_base[idx]['instruction'][:30]}...")
279
+
280
+ if final_context:
281
+ return "\n\n".join(final_context)
282
+ else:
283
+ final_context = []
284
+ for i, (idx, score) in enumerate(top_candidates[:5]):
285
+ if score > threshold:
286
+ final_context.append(knowledge_base[idx]['output'])
287
+
288
+ if final_context:
289
+ return "\n\n".join(final_context)
290
+
291
+ print("❌ No confident match found")
292
+ return ""
293
+
294
+ except Exception as e:
295
+ print(f"RAG Error: {e}")
296
+ return ""
297
+
298
+ def search_file_context(query: str, session_id: str) -> str:
299
+ """Search uploaded file for relevant context"""
300
+ if session_id not in session_file_data or not rag_model:
301
+ print(f"⚠️ File context not found: session_id={session_id}, in_session={session_id in session_file_data}")
302
+ return ""
303
+
304
+ # Maximum characters to return (prevent context overflow)
305
+ MAX_CONTEXT_CHARS = 2000
306
+
307
+ try:
308
+ file_data = session_file_data[session_id]
309
+ chunks = file_data['chunks']
310
+ embeddings = file_data['embeddings']
311
+ filename = file_data.get('filename', 'uploaded file')
312
+
313
+ # Detect general queries about the file (summarize, what's in the file, etc.)
314
+ general_triggers = ['summarize', 'summary', 'what is in', "what's in", 'tell me about',
315
+ 'describe', 'overview', 'main points', 'key points', 'the file',
316
+ 'the document', 'uploaded', 'attached', 'read the']
317
+ is_general_query = any(t in query.lower() for t in general_triggers)
318
+
319
+ if is_general_query:
320
+ # For general queries, return content up to limit
321
+ print(f"📄 General file query detected - returning limited content")
322
+ all_content = ""
323
+ for chunk in chunks:
324
+ if len(all_content) + len(chunk) > MAX_CONTEXT_CHARS:
325
+ break
326
+ all_content += chunk + "\n\n"
327
+
328
+ if len(all_content) < sum(len(c) for c in chunks):
329
+ all_content += f"\n[...content truncated, showing {len(all_content)} of {sum(len(c) for c in chunks)} chars]"
330
+
331
+ return f"[Content from {filename}]:\n\n{all_content.strip()}"
332
+
333
+ # For specific queries, use semantic search
334
+ from sklearn.metrics.pairwise import cosine_similarity
335
+ q_emb = rag_model.encode([query])
336
+ scores = cosine_similarity(q_emb, embeddings).flatten()
337
+
338
+ # Get top 3 most relevant chunks
339
+ top_indices = scores.argsort()[-3:][::-1]
340
+ context_parts = []
341
+ total_chars = 0
342
+
343
+ for idx in top_indices:
344
+ if scores[idx] > 0.15 and total_chars < MAX_CONTEXT_CHARS:
345
+ chunk = chunks[idx]
346
+ if total_chars + len(chunk) > MAX_CONTEXT_CHARS:
347
+ # Truncate to fit
348
+ remaining = MAX_CONTEXT_CHARS - total_chars
349
+ chunk = chunk[:remaining] + "..."
350
+ context_parts.append(chunk)
351
+ total_chars += len(chunk)
352
+
353
+ if context_parts:
354
+ print(f"📄 File context found ({len(context_parts)} chunks, {total_chars} chars)")
355
+ return f"[Content from {filename}]:\n\n" + "\n\n".join(context_parts)
356
+
357
+ # Fallback: if no good matches, return first chunk truncated
358
+ print(f"📄 Low confidence match - returning truncated first chunk")
359
+ first_chunk = chunks[0][:MAX_CONTEXT_CHARS] if chunks else ""
360
+ return f"[Content from {filename}]:\n\n{first_chunk}"
361
+
362
+ except Exception as e:
363
+ print(f"File search error: {e}")
364
+ return ""
365
+
366
+ def search_web(query):
367
+ if not ddgs:
368
+ return ""
369
+ try:
370
+ results = ddgs.text(query, max_results=3)
371
+ if not results:
372
+ return ""
373
+ return "\n\n".join([f"**{r['title']}**\n{r['body']}" for r in results])
374
+ except:
375
+ return ""
376
+
377
+ # --- FastAPI ---
378
+ app = FastAPI()
379
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
380
+
381
+ class ChatRequest(BaseModel):
382
+ message: str
383
+ max_tokens: int = 512
384
+ temperature: float = 0.7
385
+ summary: str = ""
386
+ history: list = []
387
+ session_id: str = "" # For file upload sessions
388
+
389
+ class SummarizeRequest(BaseModel):
390
+ messages: list
391
+
392
+ @app.get("/")
393
+ async def root():
394
+ return {"name": "Krish Mind", "status": "online", "rag": rag_model is not None, "web": ddgs is not None}
395
+
396
+ @app.post("/upload")
397
+ async def upload_file(file: UploadFile = File(...), session_id: str = "default"):
398
+ """Upload a file for RAG analysis"""
399
+ if not rag_model:
400
+ return {"success": False, "error": "Embedding model not available"}
401
+
402
+ try:
403
+ # Save uploaded file temporarily
404
+ suffix = '.' + file.filename.split('.')[-1] if '.' in file.filename else '.txt'
405
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
406
+ content = await file.read()
407
+ tmp.write(content)
408
+ tmp_path = tmp.name
409
+
410
+ # Extract text
411
+ print(f"📤 Processing uploaded file: {file.filename}")
412
+ text = extract_text_from_file(tmp_path, file.filename)
413
+
414
+ # Clean up temp file
415
+ os.unlink(tmp_path)
416
+
417
+ if text.startswith("Error"):
418
+ return {"success": False, "error": text}
419
+
420
+ if not text.strip():
421
+ return {"success": False, "error": "Could not extract text from file"}
422
+
423
+ # Chunk text
424
+ chunks = chunk_text(text)
425
+ if not chunks:
426
+ return {"success": False, "error": "File too small or empty"}
427
+
428
+ # Create embeddings
429
+ print(f"🔄 Creating embeddings for {len(chunks)} chunks...")
430
+ embeddings = rag_model.encode(chunks)
431
+
432
+ # Store in session
433
+ session_file_data[session_id] = {
434
+ "filename": file.filename,
435
+ "chunks": chunks,
436
+ "embeddings": embeddings,
437
+ "full_text": text[:2000] # First 2000 chars for context
438
+ }
439
+
440
+ print(f"✅ File processed: {len(chunks)} chunks, {len(text)} chars")
441
+
442
+ return {
443
+ "success": True,
444
+ "filename": file.filename,
445
+ "chunks": len(chunks),
446
+ "chars": len(text),
447
+ "preview": text[:200] + "..." if len(text) > 200 else text
448
+ }
449
+
450
+ except Exception as e:
451
+ print(f"❌ Upload error: {e}")
452
+ return {"success": False, "error": str(e)}
453
+
454
+ @app.delete("/upload/{session_id}")
455
+ async def clear_file(session_id: str):
456
+ """Clear uploaded file from session"""
457
+ if session_id in session_file_data:
458
+ del session_file_data[session_id]
459
+ return {"success": True, "message": "File cleared"}
460
+ return {"success": False, "message": "No file found for session"}
461
+
462
+ @app.post("/summarize")
463
+ async def summarize(request: SummarizeRequest):
464
+ """Summarize older messages to compress context"""
465
+ try:
466
+ messages_text = ""
467
+ for msg in request.messages:
468
+ role = msg.get("role", "user")
469
+ content = msg.get("content", "")
470
+ messages_text += f"{role.capitalize()}: {content}\n"
471
+
472
+ summary_prompt = f"""<|start_header_id|>system<|end_header_id|>
473
+
474
+ You are a conversation summarizer. Condense the following conversation into a brief summary (2-3 sentences max) that captures the key topics and context. Focus on what was discussed, not exact words.<|eot_id|><|start_header_id|>user<|end_header_id|>
475
+
476
+ Summarize this conversation:
477
+ {messages_text}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
478
+
479
+ Summary: """
480
+
481
+ output = model(summary_prompt, max_tokens=150, temperature=0.3, stop=["<|eot_id|>"], echo=False)
482
+ summary = output["choices"][0]["text"].strip()
483
+ print(f"📝 Summarized {len(request.messages)} messages: {summary[:50]}...")
484
+ return {"summary": summary}
485
+ except Exception as e:
486
+ print(f"❌ Summarization error: {e}")
487
+ return {"summary": "", "error": str(e)}
488
+
489
+
490
+ @app.post("/chat")
491
+ async def chat(request: ChatRequest):
492
+ user_input = request.message
493
+ session_id = request.session_id or "default"
494
+
495
+ # Image generation
496
+ img_triggers = ["generate image", "create image", "draw", "imagine"]
497
+ if any(t in user_input.lower() for t in img_triggers):
498
+ prompt = user_input
499
+ for t in img_triggers:
500
+ prompt = prompt.lower().replace(t, "")
501
+ prompt = prompt.strip()
502
+ if prompt:
503
+ url = f"https://image.pollinations.ai/prompt/{urllib.parse.quote(prompt)}"
504
+ return {"response": f"Here's your image of **{prompt}**:\n\n![{prompt}]({url})"}
505
+
506
+ # RAG Search (college knowledge base)
507
+ rag_context = ""
508
+ if rag_model:
509
+ rag_context = search_krce(user_input)
510
+ if rag_context:
511
+ print(f"🧠 RAG Context found: {rag_context[:50]}...")
512
+
513
+ # File context (uploaded document)
514
+ file_context = ""
515
+ if session_id in session_file_data:
516
+ file_context = search_file_context(user_input, session_id)
517
+ if file_context:
518
+ print(f"📄 File Context found: {file_context[:50]}...")
519
+
520
+ # Web search
521
+ web_context = ""
522
+ search_triggers = ["search", "find", "latest", "news", "who is", "what is", "when", "where", "how"]
523
+ if ddgs and any(t in user_input.lower() for t in search_triggers):
524
+ if len(user_input.split()) > 2:
525
+ print(f"🔎 Searching web...")
526
+ web_context = search_web(user_input)
527
+
528
+ # Build prompt
529
+ now = datetime.now().strftime("%A, %B %d, %Y %I:%M %p")
530
+
531
+ sys_prompt = f"""You are Krish Mind, a helpful AI assistant created by Krish CS. Current time: {now}
532
+
533
+ IMPORTANT STRICT RULES:
534
+ 1. IDENTITY: You were created by Krish CS. Do NOT claim to be created by anyone mentioned in the context (like faculty, HODs, or staff). If the context mentions a name, that person is a subject of the data, NOT your creator.
535
+ 2. CONTEXT USAGE: Use the provided context to answer questions. If the context contains a list (e.g., faculty names), make sure to include ALL items found in the context chunks.
536
+ 3. FORMATTING: Use Markdown. For letters, use **bold** for headers (e.g., **Subject:**) and use DOUBLE LINE BREAKS between sections (Place, Date, From, To, Subject, Body) to create clear distinct paragraphs.
537
+ 4. AMBIGUITY: 'AID' or 'AIDS' in this context ALWAYS refers to 'Artificial Intelligence and Data Science', NEVER 'Aerospace' or 'Disease'.
538
+ 5. ACCURACY: If the context contains a name like 'Mrs. C. Rani', she is a faculty member. Do NOT say "I was created by Mrs. C. Rani".
539
+ """
540
+
541
+ if file_context:
542
+ sys_prompt += f"\n\nUploaded Document Context:\n{file_context}"
543
+
544
+ if rag_context:
545
+ sys_prompt += f"\n\nKnowledge Base Context:\n{rag_context}"
546
+
547
+ if web_context:
548
+ sys_prompt += f"\n\nWeb Results:\n{web_context}"
549
+
550
+ if request.summary:
551
+ sys_prompt += f"\n\nPrevious conversation summary:\n{request.summary}"
552
+
553
+ # Build history context
554
+ history_context = ""
555
+ if request.history:
556
+ for msg in request.history[-6:]:
557
+ role = msg.get("role", "user")
558
+ content = msg.get("content", "")
559
+ if role == "user":
560
+ history_context += f"<|start_header_id|>user<|end_header_id|>\n\n{content}<|eot_id|>"
561
+ else:
562
+ history_context += f"<|start_header_id|>assistant<|end_header_id|>\n\n{content}<|eot_id|>"
563
+
564
+ # Build full prompt
565
+ if history_context:
566
+ full_prompt = f"""<|start_header_id|>system<|end_header_id|>
567
+
568
+ {sys_prompt}<|eot_id|>{history_context}<|start_header_id|>user<|end_header_id|>
569
+
570
+ {user_input}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
571
+
572
+ """
573
+ else:
574
+ full_prompt = f"""<|start_header_id|>system<|end_header_id|>
575
+
576
+ {sys_prompt}<|eot_id|><|start_header_id|>user<|end_header_id|>
577
+
578
+ {user_input}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
579
+
580
+ """
581
+
582
+ try:
583
+ print(f"💬 Generating response...")
584
+ output = model(full_prompt, max_tokens=request.max_tokens, temperature=request.temperature, stop=["<|eot_id|>"], echo=False)
585
+ response = output["choices"][0]["text"].strip()
586
+ print(f"✅ Done")
587
+ return {"response": response}
588
+ except Exception as e:
589
+ return {"response": f"Error: {e}"}
590
+
591
+ if __name__ == "__main__":
592
+ print("\n" + "=" * 60)
593
+ print("🚀 Server running at: http://127.0.0.1:8000")
594
+ print("📱 Open index_local.html in your browser")
595
+ print("=" * 60 + "\n")
596
+ uvicorn.run(app, host="0.0.0.0", port=8000)
requirements.txt CHANGED
@@ -1,3 +1,4 @@
 
1
  fastapi==0.109.0
2
  uvicorn==0.27.0
3
  pydantic==2.6.0
@@ -7,3 +8,12 @@ scikit-learn
7
  duckduckgo-search>=5.0
8
  python-multipart
9
  huggingface_hub>=0.20.0
 
 
 
 
 
 
 
 
 
 
1
+ # Krish Mind Deployment Requirements
2
  fastapi==0.109.0
3
  uvicorn==0.27.0
4
  pydantic==2.6.0
 
8
  duckduckgo-search>=5.0
9
  python-multipart
10
  huggingface_hub>=0.20.0
11
+
12
+ # For PDF file support
13
+ PyPDF2
14
+
15
+ # For Word document support
16
+ python-docx
17
+
18
+ # For PowerPoint support
19
+ python-pptx
static/index.html CHANGED
@@ -990,6 +990,213 @@
990
  height: 16px;
991
  }
992
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
993
  /* Copy Feedback */
994
  .action-btn.copied {
995
  color: #10b981;
@@ -1427,10 +1634,50 @@
1427
  </svg>
1428
  Create Image
1429
  </button>
 
 
 
 
 
 
 
 
1430
  </div>
 
1431
  <div class="input-wrapper">
1432
- <textarea id="messageInput" placeholder="Message Krish Mind..." rows="1"
1433
- onkeydown="handleKeyDown(event)" oninput="autoResize(this)"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1434
  <button class="send-btn" id="sendBtn" onclick="sendMessage()" disabled>
1435
  <svg width="16" height="16" viewBox="0 0 24 24" fill="white">
1436
  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
@@ -1513,18 +1760,22 @@
1513
 
1514
  <script>
1515
  // State
1516
- let serverUrl = ''; // Empty = relative URL (works on HF Spaces or any host)
1517
  let isConnected = true; // Always connected when hosted on same server
1518
  let isGenerating = false; // Prevent multiple requests
1519
  let imageGenMode = false; // Image generation tool toggle
1520
  let messages = []; // Current display messages
1521
- let chatHistory = JSON.parse(sessionStorage.getItem('krishMindChatHistory') || '[]');
1522
  let currentChatId = null;
1523
  let currentAbortController = null; // For aborting ongoing requests
1524
 
1525
- // Smart Memory State
1526
- let fullMessages = JSON.parse(sessionStorage.getItem('krishMindFullMessages') || '[]'); // Full conversation
1527
- let conversationSummary = sessionStorage.getItem('krishMindSummary') || ''; // Compressed context
 
 
 
 
1528
  const SUMMARIZE_THRESHOLD = 10; // Trigger summarization after this many messages
1529
  const KEEP_RECENT = 6; // Keep this many recent messages in full context
1530
 
@@ -1709,8 +1960,22 @@
1709
  const sendBtn = document.getElementById('sendBtn');
1710
  const message = input.value.trim();
1711
 
1712
- if (!message) return;
1713
- if (isGenerating) return; // Block if already generating
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1714
  if (!isConnected) {
1715
  showConnectModal();
1716
  return;
@@ -1729,8 +1994,14 @@
1729
  input.value = '';
1730
  input.style.height = 'auto';
1731
 
1732
- // Add user message
1733
- addMessage('user', message);
 
 
 
 
 
 
1734
 
1735
  // Scroll to bottom after adding user message
1736
  const chatContainer = document.getElementById('chatContainer');
@@ -1771,9 +2042,51 @@
1771
  isGenerating = false;
1772
  sendBtn.innerHTML = SEND_ICON;
1773
  sendBtn.disabled = true;
 
 
1774
  return;
1775
  }
1776
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1777
  // Show thinking
1778
  showThinking();
1779
 
@@ -1818,7 +2131,8 @@
1818
  max_tokens: 1024,
1819
  temperature: 0.7,
1820
  summary: conversationSummary,
1821
- history: recentHistory
 
1822
  }),
1823
  signal: currentAbortController.signal
1824
  });
@@ -1846,11 +2160,16 @@
1846
  isGenerating = false;
1847
  sendBtn.innerHTML = SEND_ICON;
1848
  sendBtn.disabled = true;
 
 
 
 
 
1849
  }
1850
  }
1851
 
1852
  // Add message with action buttons
1853
- function addMessage(role, content) {
1854
  const container = document.getElementById('chatMessages');
1855
  const msgIndex = messages.length;
1856
 
@@ -1863,6 +2182,25 @@
1863
  });
1864
  html = html.replace(/<\/code><\/pre>/g, '</code></pre></div>');
1865
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1866
  // Action buttons based on role
1867
  const editIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
1868
 
@@ -1887,6 +2225,7 @@
1887
  div.setAttribute('data-index', msgIndex);
1888
  div.innerHTML = `
1889
  <div class="message-wrapper">
 
1890
  <div class="message-content">${html}</div>
1891
  ${actionsHtml}
1892
  </div>
@@ -1901,11 +2240,10 @@
1901
  // Highlight code
1902
  div.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
1903
 
1904
- messages.push({ role, content });
1905
 
1906
- // Smart Memory: Save to fullMessages and persist
1907
  fullMessages.push({ role, content });
1908
- sessionStorage.setItem('krishMindFullMessages', JSON.stringify(fullMessages));
1909
  }
1910
 
1911
  // Copy message content with tick feedback
@@ -2238,6 +2576,88 @@
2238
  document.getElementById('connectModal').addEventListener('click', (e) => {
2239
  if (e.target.id === 'connectModal') hideConnectModal();
2240
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2241
  </script>
2242
  </body>
2243
 
 
990
  height: 16px;
991
  }
992
 
993
+ /* ============================================ */
994
+ /* FILE CARD - ChatGPT Style */
995
+ /* ============================================ */
996
+
997
+ .file-card {
998
+ display: none;
999
+ align-items: center;
1000
+ gap: 12px;
1001
+ padding: 12px 14px;
1002
+ background: var(--bg-tertiary);
1003
+ border: 1px solid var(--border);
1004
+ border-radius: 12px;
1005
+ animation: fadeIn 0.3s ease;
1006
+ position: relative;
1007
+ max-width: 280px;
1008
+ }
1009
+
1010
+ .file-card.show {
1011
+ display: flex;
1012
+ }
1013
+
1014
+ .file-card .file-icon-wrapper {
1015
+ width: 40px;
1016
+ height: 40px;
1017
+ background: #2d8cbe;
1018
+ border-radius: 8px;
1019
+ display: flex;
1020
+ align-items: center;
1021
+ justify-content: center;
1022
+ flex-shrink: 0;
1023
+ }
1024
+
1025
+ .file-card .file-icon-wrapper svg {
1026
+ width: 20px;
1027
+ height: 20px;
1028
+ stroke: white;
1029
+ }
1030
+
1031
+ .file-card .file-info {
1032
+ display: flex;
1033
+ flex-direction: column;
1034
+ gap: 2px;
1035
+ min-width: 0;
1036
+ flex: 1;
1037
+ }
1038
+
1039
+ .file-card .file-name {
1040
+ font-size: 14px;
1041
+ font-weight: 500;
1042
+ color: var(--text-primary);
1043
+ overflow: hidden;
1044
+ text-overflow: ellipsis;
1045
+ white-space: nowrap;
1046
+ }
1047
+
1048
+ .file-card .file-type {
1049
+ font-size: 12px;
1050
+ color: var(--text-muted);
1051
+ }
1052
+
1053
+ .file-card .remove-file {
1054
+ position: absolute;
1055
+ top: -6px;
1056
+ right: -6px;
1057
+ width: 20px;
1058
+ height: 20px;
1059
+ background: var(--bg-secondary);
1060
+ border: 1px solid var(--border);
1061
+ border-radius: 50%;
1062
+ cursor: pointer;
1063
+ display: flex;
1064
+ align-items: center;
1065
+ justify-content: center;
1066
+ font-size: 14px;
1067
+ color: var(--text-muted);
1068
+ transition: all 0.2s;
1069
+ }
1070
+
1071
+ .file-card .remove-file:hover {
1072
+ background: var(--bg-hover);
1073
+ color: var(--text-primary);
1074
+ }
1075
+
1076
+ #fileInput {
1077
+ display: none;
1078
+ }
1079
+
1080
+ /* File card in message bubble */
1081
+ .message-file-card {
1082
+ display: flex;
1083
+ align-items: center;
1084
+ gap: 10px;
1085
+ padding: 10px 12px;
1086
+ background: rgba(45, 140, 190, 0.15);
1087
+ border: 1px solid rgba(45, 140, 190, 0.3);
1088
+ border-radius: 10px;
1089
+ margin-bottom: 8px;
1090
+ max-width: 250px;
1091
+ }
1092
+
1093
+ .message.user .message-file-card {
1094
+ background: rgba(255, 255, 255, 0.15);
1095
+ border-color: rgba(255, 255, 255, 0.2);
1096
+ }
1097
+
1098
+ .message-file-card .file-icon-wrapper {
1099
+ width: 32px;
1100
+ height: 32px;
1101
+ background: #2d8cbe;
1102
+ border-radius: 6px;
1103
+ display: flex;
1104
+ align-items: center;
1105
+ justify-content: center;
1106
+ flex-shrink: 0;
1107
+ }
1108
+
1109
+ .message-file-card .file-icon-wrapper svg {
1110
+ width: 16px;
1111
+ height: 16px;
1112
+ stroke: white;
1113
+ }
1114
+
1115
+ .message-file-card .file-info {
1116
+ display: flex;
1117
+ flex-direction: column;
1118
+ gap: 1px;
1119
+ min-width: 0;
1120
+ }
1121
+
1122
+ .message-file-card .file-name {
1123
+ font-size: 13px;
1124
+ font-weight: 500;
1125
+ overflow: hidden;
1126
+ text-overflow: ellipsis;
1127
+ white-space: nowrap;
1128
+ }
1129
+
1130
+ .message-file-card .file-type {
1131
+ font-size: 11px;
1132
+ opacity: 0.7;
1133
+ }
1134
+
1135
+ /* Input wrapper */
1136
+ .input-wrapper {
1137
+ display: flex;
1138
+ align-items: flex-end;
1139
+ gap: 8px;
1140
+ padding: 12px 14px;
1141
+ background: var(--bg-input);
1142
+ border: 1px solid var(--border);
1143
+ border-radius: 24px;
1144
+ box-shadow: var(--shadow);
1145
+ transition: border-color 0.2s, box-shadow 0.2s;
1146
+ }
1147
+
1148
+ .input-wrapper:focus-within {
1149
+ border-color: var(--accent);
1150
+ box-shadow: 0 0 0 2px rgba(45, 140, 190, 0.2);
1151
+ }
1152
+
1153
+ .input-content {
1154
+ display: flex;
1155
+ flex-direction: column;
1156
+ flex: 1;
1157
+ gap: 10px;
1158
+ min-width: 0;
1159
+ }
1160
+
1161
+ .input-row {
1162
+ display: flex;
1163
+ align-items: center;
1164
+ gap: 8px;
1165
+ }
1166
+
1167
+ .attach-btn {
1168
+ width: 28px;
1169
+ height: 28px;
1170
+ display: flex;
1171
+ align-items: center;
1172
+ justify-content: center;
1173
+ background: transparent;
1174
+ border: none;
1175
+ border-radius: 6px;
1176
+ color: var(--accent);
1177
+ cursor: pointer;
1178
+ transition: all 0.2s;
1179
+ flex-shrink: 0;
1180
+ }
1181
+
1182
+ .attach-btn:hover {
1183
+ background: rgba(45, 140, 190, 0.15);
1184
+ color: var(--accent);
1185
+ transform: scale(1.1);
1186
+ }
1187
+
1188
+ .attach-btn svg {
1189
+ width: 20px;
1190
+ height: 20px;
1191
+ stroke-width: 2.5;
1192
+ }
1193
+
1194
+ @keyframes spin {
1195
+ to {
1196
+ transform: rotate(360deg);
1197
+ }
1198
+ }
1199
+
1200
  /* Copy Feedback */
1201
  .action-btn.copied {
1202
  color: #10b981;
 
1634
  </svg>
1635
  Create Image
1636
  </button>
1637
+ <button class="tool-btn" id="fileUploadBtn" onclick="document.getElementById('fileInput').click()">
1638
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1639
+ <path
1640
+ d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48">
1641
+ </path>
1642
+ </svg>
1643
+ Attach
1644
+ </button>
1645
  </div>
1646
+
1647
  <div class="input-wrapper">
1648
+ <input type="file" id="fileInput" accept=".pdf,.txt,.doc,.docx,.ppt,.pptx,.md,.json,.csv,.xlsx,.xls"
1649
+ onchange="handleFileSelect(event)">
1650
+
1651
+ <div class="input-content">
1652
+ <!-- File Card (ChatGPT Style) -->
1653
+ <div class="file-card" id="fileCard">
1654
+ <div class="file-icon-wrapper">
1655
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1656
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
1657
+ <polyline points="14 2 14 8 20 8"></polyline>
1658
+ </svg>
1659
+ </div>
1660
+ <div class="file-info">
1661
+ <span class="file-name" id="fileName"></span>
1662
+ <span class="file-type">Document</span>
1663
+ </div>
1664
+ <button class="remove-file" onclick="clearStagedFile()">×</button>
1665
+ </div>
1666
+
1667
+ <!-- Input Row with + button and textarea -->
1668
+ <div class="input-row">
1669
+ <button class="attach-btn" onclick="document.getElementById('fileInput').click()"
1670
+ title="Attach file">
1671
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1672
+ <line x1="12" y1="5" x2="12" y2="19"></line>
1673
+ <line x1="5" y1="12" x2="19" y2="12"></line>
1674
+ </svg>
1675
+ </button>
1676
+ <textarea id="messageInput" placeholder="Message Krish Mind..." rows="1"
1677
+ onkeydown="handleKeyDown(event)" oninput="autoResize(this)"></textarea>
1678
+ </div>
1679
+ </div>
1680
+
1681
  <button class="send-btn" id="sendBtn" onclick="sendMessage()" disabled>
1682
  <svg width="16" height="16" viewBox="0 0 24 24" fill="white">
1683
  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
 
1760
 
1761
  <script>
1762
  // State
1763
+ let serverUrl = ''; // Empty = relative URL (works on HF Spaces)
1764
  let isConnected = true; // Always connected when hosted on same server
1765
  let isGenerating = false; // Prevent multiple requests
1766
  let imageGenMode = false; // Image generation tool toggle
1767
  let messages = []; // Current display messages
1768
+ let chatHistory = []; // Chat history (clears on refresh)
1769
  let currentChatId = null;
1770
  let currentAbortController = null; // For aborting ongoing requests
1771
 
1772
+ // File Upload State
1773
+ let sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1774
+ let uploadedFile = null; // Currently uploaded file info
1775
+
1776
+ // Smart Memory State (clears on refresh)
1777
+ let fullMessages = []; // Full conversation
1778
+ let conversationSummary = ''; // Compressed context
1779
  const SUMMARIZE_THRESHOLD = 10; // Trigger summarization after this many messages
1780
  const KEEP_RECENT = 6; // Keep this many recent messages in full context
1781
 
 
1960
  const sendBtn = document.getElementById('sendBtn');
1961
  const message = input.value.trim();
1962
 
1963
+ if (!message && !stagedFile) return;
1964
+
1965
+ // If already generating, this is a STOP request
1966
+ if (isGenerating) {
1967
+ console.log('⛔ Stopping generation...');
1968
+ if (currentAbortController) {
1969
+ currentAbortController.abort();
1970
+ currentAbortController = null;
1971
+ }
1972
+ hideThinking();
1973
+ isGenerating = false;
1974
+ sendBtn.innerHTML = SEND_ICON;
1975
+ sendBtn.disabled = !input.value.trim();
1976
+ return;
1977
+ }
1978
+
1979
  if (!isConnected) {
1980
  showConnectModal();
1981
  return;
 
1994
  input.value = '';
1995
  input.style.height = 'auto';
1996
 
1997
+ // Add user message (with file card if file attached)
1998
+ const attachedFileName = stagedFileName; // Capture before clearing
1999
+ const fileToUpload = stagedFile; // Store file reference before clearing UI
2000
+ addMessage('user', message, attachedFileName);
2001
+
2002
+ // Clear file card immediately (no delay) - only clears UI, not file reference
2003
+ const fileCard = document.getElementById('fileCard');
2004
+ fileCard.classList.remove('show');
2005
 
2006
  // Scroll to bottom after adding user message
2007
  const chatContainer = document.getElementById('chatContainer');
 
2042
  isGenerating = false;
2043
  sendBtn.innerHTML = SEND_ICON;
2044
  sendBtn.disabled = true;
2045
+ stagedFile = null;
2046
+ stagedFileName = null;
2047
  return;
2048
  }
2049
 
2050
+ // Upload file if one was attached
2051
+ const hadFile = !!fileToUpload;
2052
+ if (hadFile) {
2053
+ console.log(`📤 Uploading file: ${attachedFileName}`);
2054
+ const formData = new FormData();
2055
+ formData.append('file', fileToUpload);
2056
+
2057
+ try {
2058
+ const response = await fetch(`${serverUrl}/upload?session_id=${sessionId}`, {
2059
+ method: 'POST',
2060
+ body: formData
2061
+ });
2062
+ const data = await response.json();
2063
+ if (data.success) {
2064
+ console.log(`✅ File uploaded: ${data.filename} (${data.chunks} chunks)`);
2065
+ } else {
2066
+ console.error('Upload failed:', data.error);
2067
+ addMessage('assistant', 'Failed to process file: ' + data.error);
2068
+ isGenerating = false;
2069
+ sendBtn.innerHTML = SEND_ICON;
2070
+ sendBtn.disabled = true;
2071
+ stagedFile = null;
2072
+ stagedFileName = null;
2073
+ return;
2074
+ }
2075
+ } catch (err) {
2076
+ console.error('Upload error:', err);
2077
+ addMessage('assistant', 'Failed to upload file. Please try again.');
2078
+ isGenerating = false;
2079
+ sendBtn.innerHTML = SEND_ICON;
2080
+ sendBtn.disabled = true;
2081
+ stagedFile = null;
2082
+ stagedFileName = null;
2083
+ return;
2084
+ }
2085
+ }
2086
+ // Reset stagedFile reference
2087
+ stagedFile = null;
2088
+ stagedFileName = null;
2089
+
2090
  // Show thinking
2091
  showThinking();
2092
 
 
2131
  max_tokens: 1024,
2132
  temperature: 0.7,
2133
  summary: conversationSummary,
2134
+ history: recentHistory,
2135
+ session_id: sessionId
2136
  }),
2137
  signal: currentAbortController.signal
2138
  });
 
2160
  isGenerating = false;
2161
  sendBtn.innerHTML = SEND_ICON;
2162
  sendBtn.disabled = true;
2163
+
2164
+ // Clear server file if one was uploaded (ChatGPT style - one-time use)
2165
+ if (hadFile) {
2166
+ await clearServerFile();
2167
+ }
2168
  }
2169
  }
2170
 
2171
  // Add message with action buttons
2172
+ function addMessage(role, content, attachedFile = null) {
2173
  const container = document.getElementById('chatMessages');
2174
  const msgIndex = messages.length;
2175
 
 
2182
  });
2183
  html = html.replace(/<\/code><\/pre>/g, '</code></pre></div>');
2184
 
2185
+ // File card HTML for user messages (ChatGPT style)
2186
+ let fileCardHtml = '';
2187
+ if (attachedFile && role === 'user') {
2188
+ fileCardHtml = `
2189
+ <div class="message-file-card">
2190
+ <div class="file-icon-wrapper">
2191
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2192
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
2193
+ <polyline points="14 2 14 8 20 8"></polyline>
2194
+ </svg>
2195
+ </div>
2196
+ <div class="file-info">
2197
+ <span class="file-name">${attachedFile}</span>
2198
+ <span class="file-type">Document</span>
2199
+ </div>
2200
+ </div>
2201
+ `;
2202
+ }
2203
+
2204
  // Action buttons based on role
2205
  const editIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
2206
 
 
2225
  div.setAttribute('data-index', msgIndex);
2226
  div.innerHTML = `
2227
  <div class="message-wrapper">
2228
+ ${fileCardHtml}
2229
  <div class="message-content">${html}</div>
2230
  ${actionsHtml}
2231
  </div>
 
2240
  // Highlight code
2241
  div.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
2242
 
2243
+ messages.push({ role, content, attachedFile });
2244
 
2245
+ // Smart Memory: Save to fullMessages (in-memory only, clears on refresh)
2246
  fullMessages.push({ role, content });
 
2247
  }
2248
 
2249
  // Copy message content with tick feedback
 
2576
  document.getElementById('connectModal').addEventListener('click', (e) => {
2577
  if (e.target.id === 'connectModal') hideConnectModal();
2578
  });
2579
+
2580
+ // ==========================================
2581
+ // FILE UPLOAD FUNCTIONS (ChatGPT Style)
2582
+ // ==========================================
2583
+
2584
+ let stagedFile = null; // File staged for upload with next message
2585
+ let stagedFileName = null; // Store filename for message display
2586
+
2587
+ function handleFileSelect(event) {
2588
+ const file = event.target.files[0];
2589
+ if (!file) return;
2590
+
2591
+ // Stage the file (don't upload yet)
2592
+ stagedFile = file;
2593
+ stagedFileName = file.name;
2594
+
2595
+ // Show file card in input area
2596
+ const fileCard = document.getElementById('fileCard');
2597
+ const fileNameEl = document.getElementById('fileName');
2598
+ fileNameEl.textContent = file.name;
2599
+ fileCard.classList.add('show');
2600
+
2601
+ // Enable send button since we have a file
2602
+ document.getElementById('sendBtn').disabled = false;
2603
+
2604
+ console.log(`📎 File staged: ${file.name}`);
2605
+
2606
+ // Clear input so same file can be re-selected
2607
+ event.target.value = '';
2608
+ }
2609
+
2610
+ function clearStagedFile() {
2611
+ stagedFile = null;
2612
+ stagedFileName = null;
2613
+ const fileCard = document.getElementById('fileCard');
2614
+ fileCard.classList.remove('show');
2615
+
2616
+ // Disable send button if no text
2617
+ const input = document.getElementById('messageInput');
2618
+ document.getElementById('sendBtn').disabled = !input.value.trim();
2619
+
2620
+ console.log('📎 Staged file cleared');
2621
+ }
2622
+
2623
+ async function uploadStagedFile() {
2624
+ if (!stagedFile) return true; // No file to upload
2625
+
2626
+ try {
2627
+ const formData = new FormData();
2628
+ formData.append('file', stagedFile);
2629
+
2630
+ console.log(`📤 Uploading ${stagedFile.name}...`);
2631
+
2632
+ const response = await fetch(`${serverUrl}/upload?session_id=${sessionId}`, {
2633
+ method: 'POST',
2634
+ body: formData
2635
+ });
2636
+
2637
+ const data = await response.json();
2638
+
2639
+ if (data.success) {
2640
+ console.log(`✅ File uploaded: ${data.filename} (${data.chunks} chunks)`);
2641
+ return true;
2642
+ } else {
2643
+ console.error('Upload failed:', data.error);
2644
+ return false;
2645
+ }
2646
+ } catch (err) {
2647
+ console.error('Upload error:', err);
2648
+ return false;
2649
+ }
2650
+ }
2651
+
2652
+ async function clearServerFile() {
2653
+ try {
2654
+ await fetch(`${serverUrl}/upload/${sessionId}`, {
2655
+ method: 'DELETE'
2656
+ });
2657
+ } catch (err) {
2658
+ // Ignore errors when clearing
2659
+ }
2660
+ }
2661
  </script>
2662
  </body>
2663