riteshraut commited on
Commit
ba4d135
·
1 Parent(s): 07bb4d0

Add Dockerfile and final setup for Hugging Face deployment

Browse files
Files changed (6) hide show
  1. Dockerfile +27 -0
  2. app.py +100 -0
  3. packages.txt +2 -0
  4. rag_processor.py +131 -0
  5. requirements.txt +0 -0
  6. templates/index.html +342 -0
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.10-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /code
6
+
7
+ # Copy the system dependencies file and install them
8
+ # We need tesseract for OCR on images (used by unstructured)
9
+ COPY packages.txt .
10
+ RUN apt-get update && apt-get install -y --no-install-recommends $(cat packages.txt) && \
11
+ rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy the Python requirements file
14
+ COPY requirements.txt .
15
+
16
+ # Install the Python dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy the rest of your application code into the container
20
+ COPY . .
21
+
22
+ # Expose the port that Hugging Face Spaces uses
23
+ EXPOSE 7860
24
+
25
+ # Command to run your application using gunicorn (a production-ready server)
26
+ # It binds the app to port 7860, which is the standard for HF Spaces.
27
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
app.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, request, render_template, session, jsonify, Response, stream_with_context
3
+ from werkzeug.utils import secure_filename
4
+ from rag_processor import create_rag_chain
5
+ import time
6
+
7
+ # --- Basic Flask App Setup ---
8
+ app = Flask(__name__)
9
+ # A secret key is needed for session management
10
+ app.config['SECRET_KEY'] = os.urandom(24)
11
+ # Configure the upload folder
12
+ app.config['UPLOAD_FOLDER'] = 'uploads'
13
+ # Ensure the upload folder exists
14
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
15
+
16
+ # In-memory storage for RAG chains to avoid re-creating them on every request.
17
+ # In a production scenario, you might want a more persistent cache like Redis.
18
+ rag_chains = {}
19
+
20
+ @app.route('/', methods=['GET'])
21
+ def index():
22
+ """
23
+ Renders the main page.
24
+ """
25
+ return render_template('index.html')
26
+
27
+ @app.route('/upload', methods=['POST'])
28
+ def upload_file():
29
+ """
30
+ Handles file uploads and processing.
31
+ """
32
+ if 'file' not in request.files:
33
+ return jsonify({'status': 'error', 'message': 'No file part in the request.'}), 400
34
+
35
+ file = request.files['file']
36
+
37
+ if file.filename == '':
38
+ return jsonify({'status': 'error', 'message': 'No selected file.'}), 400
39
+
40
+ if file:
41
+ filename = secure_filename(file.filename)
42
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
43
+ file.save(filepath)
44
+
45
+ try:
46
+ # --- RAG Chain Creation ---
47
+ print(f"Creating RAG chain for {filename}...")
48
+ # Simulate a delay for demonstration purposes of the loading animation
49
+ time.sleep(2)
50
+ rag_chains[filename] = create_rag_chain(filepath)
51
+ print("RAG chain created successfully.")
52
+
53
+ # Store the filename in the user's session
54
+ session['filename'] = filename
55
+
56
+ return jsonify({'status': 'success', 'filename': filename})
57
+
58
+ except Exception as e:
59
+ print(f"Error creating RAG chain: {e}")
60
+ if os.path.exists(filepath):
61
+ os.remove(filepath)
62
+ return jsonify({'status': 'error', 'message': f'Failed to process file: {str(e)}'}), 500
63
+
64
+ return jsonify({'status': 'error', 'message': 'An unexpected error occurred.'}), 500
65
+
66
+
67
+ @app.route('/chat', methods=['POST'])
68
+ def chat():
69
+ """
70
+ Handles chat messages from the user and streams the response.
71
+ """
72
+ data = request.get_json()
73
+ question = data.get('question')
74
+ filename = session.get('filename')
75
+
76
+ if not question:
77
+ return jsonify({'status': 'error', 'message': 'Question is missing.'}), 400
78
+
79
+ if not filename or filename not in rag_chains:
80
+ return jsonify({'status': 'error', 'message': 'File not uploaded or processed yet.'}), 400
81
+
82
+ try:
83
+ rag_chain = rag_chains[filename]
84
+
85
+ def generate():
86
+ """A generator function to stream the response."""
87
+ for chunk in rag_chain.stream(question):
88
+ yield chunk
89
+
90
+ # Use stream_with_context to ensure the generator has access to the request context
91
+ return Response(stream_with_context(generate()), mimetype='text/plain')
92
+
93
+ except Exception as e:
94
+ print(f"Error during chat invocation: {e}")
95
+ # This error won't be sent as a stream, handle appropriately
96
+ return Response("An error occurred while getting the answer.", status=500, mimetype='text/plain')
97
+
98
+ if __name__ == '__main__':
99
+ app.run(debug=True, port=5001)
100
+
packages.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ libfaiss-dev
2
+ tesseract-ocr
rag_processor.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ # Document Loaders
5
+ from langchain_community.document_loaders import (
6
+ TextLoader,
7
+ PyPDFLoader,
8
+ Docx2txtLoader,
9
+ UnstructuredImageLoader,
10
+ )
11
+
12
+ # Text Splitter
13
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
14
+
15
+ # Embeddings
16
+ from langchain_huggingface import HuggingFaceEmbeddings
17
+
18
+ # Vector Stores
19
+ from langchain_community.vectorstores import FAISS
20
+
21
+ # LLM
22
+ from langchain_groq import ChatGroq
23
+
24
+ # Prompting
25
+ from langchain.prompts import PromptTemplate
26
+
27
+ # Chains
28
+ from langchain_core.runnables import RunnableParallel, RunnablePassthrough
29
+ from langchain_core.output_parsers import StrOutputParser
30
+
31
+ # A dictionary to map file extensions to their corresponding loader classes
32
+ LOADER_MAPPING = {
33
+ ".txt": TextLoader,
34
+ ".pdf": PyPDFLoader,
35
+ ".docx": Docx2txtLoader,
36
+ ".jpeg": UnstructuredImageLoader,
37
+ ".jpg": UnstructuredImageLoader,
38
+ ".png": UnstructuredImageLoader,
39
+ }
40
+
41
+ def create_rag_chain(filepath):
42
+ """
43
+ Creates a Retrieval-Augmented Generation (RAG) chain from a given file path.
44
+
45
+ Args:
46
+ filepath (str): The path to the document file.
47
+
48
+ Returns:
49
+ A LangChain runnable object representing the RAG chain.
50
+
51
+ Raises:
52
+ ValueError: If the file extension is not supported.
53
+ """
54
+ # Load environment variables from .env file
55
+ load_dotenv()
56
+ api_key = os.getenv("GROQ_API_KEY")
57
+ if not api_key:
58
+ raise ValueError("GROQ_API_KEY not found in environment variables.")
59
+
60
+ # --- 1. Load Document ---
61
+ print("Loading document...")
62
+ file_extension = "." + filepath.rsplit(".", 1)[-1].lower()
63
+
64
+ if file_extension in LOADER_MAPPING:
65
+ loader_class = LOADER_MAPPING[file_extension]
66
+ # For image loaders, mode="single" can be useful to treat the image as one document
67
+ if file_extension in [".jpeg", ".jpg", ".png"]:
68
+ loader = loader_class(filepath, mode="single")
69
+ else:
70
+ loader = loader_class(filepath)
71
+ docs = loader.load()
72
+ else:
73
+ raise ValueError(f"Unsupported file type: '{file_extension}'")
74
+
75
+ print(f"Document loaded successfully. Number of pages/docs: {len(docs)}")
76
+
77
+ # --- 2. Split Text ---
78
+ print("\nSplitting document into chunks...")
79
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
80
+ splits = text_splitter.split_documents(docs)
81
+ print(f"{len(splits)} chunks created.")
82
+
83
+ # --- 3. Create Embeddings ---
84
+ print("\nInitializing Hugging Face embeddings model...")
85
+ model_name = "BAAI/bge-base-en-v1.5"
86
+ model_kwargs = {'device': 'cpu'}
87
+ encode_kwargs = {'normalize_embeddings': True}
88
+ embeddings = HuggingFaceEmbeddings(
89
+ model_name=model_name,
90
+ model_kwargs=model_kwargs,
91
+ encode_kwargs=encode_kwargs
92
+ )
93
+ print("Embeddings model loaded.")
94
+
95
+ # --- 4. Create Vector Store ---
96
+ print("\nCreating FAISS vector store from document chunks...")
97
+ vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
98
+ print("Vector store created successfully.")
99
+
100
+ # --- 5. Create Retriever ---
101
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
102
+
103
+ # --- 6. Define the Prompt Template ---
104
+ template = """
105
+ You are an expert program. Your job is to provide accurate and helpful answers based ONLY on the provided context.
106
+ If the information is not in the context, say that you don't know the answer.
107
+ Keep your more ellaborated and explain in a clear way.
108
+
109
+ Context: {context}
110
+
111
+ Question: {question}
112
+
113
+ Answer:
114
+ """
115
+ prompt = PromptTemplate.from_template(template)
116
+
117
+ # --- 7. Initialize the LLM ---
118
+ llm = ChatGroq(model_name="llama-3.1-8b-instant", api_key=api_key, temperature=0)
119
+
120
+ # --- 8. Create the RAG Chain ---
121
+ rag_chain = (
122
+ RunnableParallel(
123
+ context=retriever,
124
+ question=RunnablePassthrough()
125
+ )
126
+ | prompt
127
+ | llm
128
+ | StrOutputParser()
129
+ )
130
+
131
+ return rag_chain
requirements.txt ADDED
Binary file (1.21 kB). View file
 
templates/index.html ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CogniChat - Chat with your Documents</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
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=Google+Sans:wght@400;500;700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
11
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
12
+ <style>
13
+ :root {
14
+ --background: #f0f4f9;
15
+ --foreground: #1f1f1f;
16
+ --primary: #1a73e8;
17
+ --primary-hover: #1867cf;
18
+ --card: #ffffff;
19
+ --card-border: #dadce0;
20
+ --input-bg: #e8f0fe;
21
+ --user-bubble: #d9e7ff;
22
+ --bot-bubble: #f1f3f4;
23
+ }
24
+
25
+ /* Dark mode styles */
26
+ .dark {
27
+ --background: #202124;
28
+ --foreground: #e8eaed;
29
+ --primary: #8ab4f8;
30
+ --primary-hover: #99bdfa;
31
+ --card: #303134;
32
+ --card-border: #5f6368;
33
+ --input-bg: #303134;
34
+ --user-bubble: #3c4043;
35
+ --bot-bubble: #3c4043;
36
+ }
37
+
38
+ body {
39
+ font-family: 'Google Sans', 'Roboto', sans-serif;
40
+ background-color: var(--background);
41
+ color: var(--foreground);
42
+ overflow: hidden;
43
+ }
44
+
45
+ #chat-window::-webkit-scrollbar { width: 8px; }
46
+ #chat-window::-webkit-scrollbar-track { background: transparent; }
47
+ #chat-window::-webkit-scrollbar-thumb { background-color: #bdc1c6; border-radius: 20px; }
48
+ .dark #chat-window::-webkit-scrollbar-thumb { background-color: #5f6368; }
49
+
50
+ .drop-zone--over {
51
+ border-color: var(--primary);
52
+ box-shadow: 0 0 15px rgba(26, 115, 232, 0.3);
53
+ }
54
+
55
+ /* Loading Spinner */
56
+ .loader {
57
+ width: 48px;
58
+ height: 48px;
59
+ border: 3px solid var(--card-border);
60
+ border-radius: 50%;
61
+ display: inline-block;
62
+ position: relative;
63
+ box-sizing: border-box;
64
+ animation: rotation 1s linear infinite;
65
+ }
66
+ .loader::after {
67
+ content: '';
68
+ box-sizing: border-box;
69
+ position: absolute;
70
+ left: 50%;
71
+ top: 50%;
72
+ transform: translate(-50%, -50%);
73
+ width: 56px;
74
+ height: 56px;
75
+ border-radius: 50%;
76
+ border: 3px solid;
77
+ border-color: var(--primary) transparent;
78
+ }
79
+
80
+ @keyframes rotation {
81
+ 0% { transform: rotate(0deg); }
82
+ 100% { transform: rotate(360deg); }
83
+ }
84
+
85
+ /* Markdown Styling */
86
+ .markdown-content p { margin-bottom: 0.75rem; line-height: 1.75; }
87
+ .markdown-content ul, .markdown-content ol { margin-left: 1.5rem; margin-bottom: 0.75rem; }
88
+ .markdown-content code { background-color: rgba(0,0,0,0.05); padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: 'Roboto Mono', monospace; font-size: 0.9em; }
89
+ .dark .markdown-content code { background-color: rgba(255,255,255,0.1); }
90
+ .markdown-content pre { position: relative; background-color: #f8f9fa; border: 1px solid var(--card-border); border-radius: 0.5rem; margin-bottom: 1rem; }
91
+ .dark .markdown-content pre { background-color: #2e2f32; }
92
+ .markdown-content pre code { background: none; padding: 1rem; display: block; overflow-x: auto; }
93
+ .markdown-content pre .copy-code-btn { position: absolute; top: 0.5rem; right: 0.5rem; background-color: #e8eaed; border: 1px solid #dadce0; color: #5f6368; padding: 0.3rem 0.6rem; border-radius: 0.25rem; cursor: pointer; opacity: 0; transition: opacity 0.2s; font-size: 0.8em;}
94
+ .dark .markdown-content pre .copy-code-btn { background-color: #3c4043; border-color: #5f6368; color: #e8eaed; }
95
+ .markdown-content pre:hover .copy-code-btn { opacity: 1; }
96
+ </style>
97
+ </head>
98
+ <body class="w-screen h-screen dark"> <!-- Default to dark mode -->
99
+
100
+ <main id="main-content" class="h-full flex flex-col transition-opacity duration-500">
101
+ <!-- Chat Area -->
102
+ <div id="chat-container" class="hidden flex-1 flex flex-col w-full mx-auto overflow-hidden">
103
+ <header class="text-center p-4 border-b border-[var(--card-border)] flex-shrink-0">
104
+ <h1 class="text-xl font-medium">Chat with your Docs</h1>
105
+ <p id="chat-filename" class="text-xs text-gray-500 dark:text-gray-400 mt-1"></p>
106
+ </header>
107
+ <div id="chat-window" class="flex-1 overflow-y-auto p-4 md:p-6 lg:p-10">
108
+ <div id="chat-content" class="max-w-4xl mx-auto space-y-8">
109
+ <!-- Chat messages will be appended here -->
110
+ </div>
111
+ </div>
112
+ <div class="p-4 flex-shrink-0 bg-[var(--background)] border-t border-[var(--card-border)]">
113
+ <form id="chat-form" class="max-w-4xl mx-auto bg-[var(--card)] rounded-full p-2 flex items-center shadow-sm border border-transparent focus-within:border-[var(--primary)] transition-colors">
114
+ <input type="text" id="chat-input" placeholder="Ask a question about your document..." class="flex-grow bg-transparent focus-outline-none px-4 text-sm" autocomplete="off">
115
+ <button type="submit" id="chat-submit-btn" class="bg-[var(--primary)] hover:bg-[var(--primary-hover)] text-white p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-500" title="butn">
116
+ <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.49941 11.5556L11.555 3.5L12.4438 4.38889L6.27721 10.5556H21.9994V11.5556H6.27721L12.4438 17.7222L11.555 18.6111L3.49941 10.5556V11.5556Z" transform="rotate(180, 12.7497, 11.0556)" fill="currentColor"></path></svg>
117
+ </button>
118
+ </form>
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Upload Area -->
123
+ <div id="upload-container" class="flex-1 flex flex-col items-center justify-center p-8 transition-opacity duration-300">
124
+ <div class="text-center">
125
+ <h1 class="text-5xl font-medium mb-4">Upload docs to chat</h1>
126
+ <div id="drop-zone" class="w-full max-w-lg text-center border-2 border-dashed border-[var(--card-border)] rounded-2xl p-10 transition-all duration-300 cursor-pointer bg-[var(--card)] hover:border-[var(--primary)]">
127
+ <input id="file-upload" type="file" class="hidden" accept=".pdf,.txt,.docx,.jpg,.jpeg,.png" placeholder="upload your documents here">
128
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" ><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3-3m-3 3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"></path></svg>
129
+ <p class="mt-4 text-sm font-medium">Drag & drop a file or click to upload</p>
130
+ <p id="file-name" class="mt-2 text-xs text-gray-500"></p>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Loading Overlay -->
136
+ <div id="loading-overlay" class="hidden fixed inset-0 bg-[var(--background)] bg-opacity-80 backdrop-blur-sm flex flex-col items-center justify-center z-50">
137
+ <div class="loader"></div>
138
+ <p id="loading-text" class="mt-6 text-sm">Processing...</p>
139
+ </div>
140
+ </main>
141
+
142
+ <script>
143
+ document.addEventListener('DOMContentLoaded', () => {
144
+ const uploadContainer = document.getElementById('upload-container');
145
+ const chatContainer = document.getElementById('chat-container');
146
+ const dropZone = document.getElementById('drop-zone');
147
+ const fileUploadInput = document.getElementById('file-upload');
148
+ const fileNameSpan = document.getElementById('file-name');
149
+ const loadingOverlay = document.getElementById('loading-overlay');
150
+ const loadingText = document.getElementById('loading-text');
151
+
152
+ const chatForm = document.getElementById('chat-form');
153
+ const chatInput = document.getElementById('chat-input');
154
+ const chatSubmitBtn = document.getElementById('chat-submit-btn');
155
+ const chatWindow = document.getElementById('chat-window');
156
+ const chatContent = document.getElementById('chat-content');
157
+ const chatFilename = document.getElementById('chat-filename');
158
+
159
+ let selectedFile = null;
160
+
161
+ // --- File Upload Logic ---
162
+ dropZone.addEventListener('click', () => fileUploadInput.click());
163
+
164
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
165
+ dropZone.addEventListener(eventName, preventDefaults, false);
166
+ document.body.addEventListener(eventName, preventDefaults, false);
167
+ });
168
+
169
+ ['dragenter', 'dragover'].forEach(eventName => dropZone.classList.add('drop-zone--over'));
170
+ ['dragleave', 'drop'].forEach(eventName => dropZone.classList.remove('drop-zone--over'));
171
+
172
+ dropZone.addEventListener('drop', (e) => {
173
+ const files = e.dataTransfer.files;
174
+ if (files.length > 0) handleFile(files[0]);
175
+ });
176
+
177
+ fileUploadInput.addEventListener('change', (e) => {
178
+ if (e.target.files.length > 0) handleFile(e.target.files[0]);
179
+ });
180
+
181
+ function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
182
+
183
+ async function handleFile(file) {
184
+ selectedFile = file;
185
+ fileNameSpan.textContent = `Selected: ${file.name}`;
186
+ await uploadAndProcessFile();
187
+ }
188
+
189
+ async function uploadAndProcessFile() {
190
+ if (!selectedFile) return;
191
+
192
+ const formData = new FormData();
193
+ formData.append('file', selectedFile);
194
+
195
+ loadingOverlay.classList.remove('hidden');
196
+
197
+ const loadingSteps = [
198
+ `Uploading ${selectedFile.name}...`,
199
+ "Parsing document...",
200
+ "Extracting text...",
201
+ "Creating embeddings (this may take a moment)...",
202
+ "Building knowledge base..."
203
+ ];
204
+ let stepIndex = 0;
205
+ loadingText.textContent = loadingSteps[stepIndex];
206
+
207
+ const stepInterval = setInterval(() => {
208
+ stepIndex++;
209
+ if (stepIndex < loadingSteps.length) {
210
+ loadingText.textContent = loadingSteps[stepIndex];
211
+ } else {
212
+ loadingText.textContent = "Finalizing...";
213
+ }
214
+ }, 1500);
215
+
216
+ try {
217
+ const response = await fetch('/upload', { method: 'POST', body: formData });
218
+ const result = await response.json();
219
+
220
+ if (!response.ok) throw new Error(result.message || 'Unknown error occurred.');
221
+
222
+ chatFilename.textContent = `Chatting with ${result.filename}`;
223
+ uploadContainer.classList.add('hidden');
224
+ chatContainer.classList.remove('hidden');
225
+ appendMessage("I've analyzed your document. What would you like to know?", "bot");
226
+
227
+ } catch (error) {
228
+ console.error('Upload error:', error);
229
+ alert(`Error: ${error.message}`);
230
+ } finally {
231
+ clearInterval(stepInterval);
232
+ loadingOverlay.classList.add('hidden');
233
+ fileNameSpan.textContent = '';
234
+ selectedFile = null;
235
+ }
236
+ }
237
+
238
+ // --- Chat Logic ---
239
+ chatForm.addEventListener('submit', async (e) => {
240
+ e.preventDefault();
241
+ const question = chatInput.value.trim();
242
+ if (!question) return;
243
+
244
+ appendMessage(question, 'user');
245
+ chatInput.value = '';
246
+ chatInput.disabled = true;
247
+ chatSubmitBtn.disabled = true;
248
+
249
+ const botMessageContainer = appendMessage('', 'bot');
250
+ const contentDiv = botMessageContainer.querySelector('.markdown-content');
251
+
252
+ try {
253
+ const response = await fetch('/chat', {
254
+ method: 'POST',
255
+ headers: { 'Content-Type': 'application/json' },
256
+ body: JSON.stringify({ question: question }),
257
+ });
258
+
259
+ if (!response.ok) throw new Error(`Server error: ${response.statusText}`);
260
+
261
+ const reader = response.body.getReader();
262
+ const decoder = new TextDecoder();
263
+ let fullResponse = '';
264
+
265
+ while (true) {
266
+ const { value, done } = await reader.read();
267
+ if (done) break;
268
+
269
+ fullResponse += decoder.decode(value, { stream: true });
270
+ contentDiv.innerHTML = marked.parse(fullResponse);
271
+ scrollToBottom();
272
+ }
273
+ contentDiv.querySelectorAll('pre').forEach(addCopyButton);
274
+
275
+ } catch (error) {
276
+ console.error('Chat error:', error);
277
+ contentDiv.innerHTML = `<p class="text-red-500">Error: ${error.message}</p>`;
278
+ } finally {
279
+ chatInput.disabled = false;
280
+ chatSubmitBtn.disabled = false;
281
+ chatInput.focus();
282
+ }
283
+ });
284
+
285
+ // --- UI Helper Functions ---
286
+ function appendMessage(text, sender) {
287
+ const messageWrapper = document.createElement('div');
288
+ messageWrapper.className = `flex items-start gap-4`;
289
+
290
+ const iconSVG = sender === 'user'
291
+ ? `<div class="bg-blue-100 dark:bg-gray-700 p-2.5 rounded-full flex-shrink-0 mt-1"><svg class="w-5 h-5 text-blue-600 dark:text-blue-300" viewBox="0 0 24 24"><path fill="currentColor" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"></path></svg></div>`
292
+ : `<div class="bg-gray-200 dark:bg-gray-700 rounded-full flex-shrink-0 mt-1 text-xl flex items-center justify-center w-10 h-10">✨</div>`;
293
+
294
+ const messageBubble = document.createElement('div');
295
+ messageBubble.className = `flex-1 pt-1`;
296
+
297
+ const senderName = document.createElement('p');
298
+ senderName.className = 'font-medium text-sm mb-1';
299
+ senderName.textContent = sender === 'user' ? 'You' : 'CogniChat';
300
+
301
+ const contentDiv = document.createElement('div');
302
+ contentDiv.className = 'text-base markdown-content';
303
+ contentDiv.innerHTML = marked.parse(text);
304
+
305
+ messageBubble.appendChild(senderName);
306
+ messageBubble.appendChild(contentDiv);
307
+ messageWrapper.innerHTML = iconSVG;
308
+ messageWrapper.appendChild(messageBubble);
309
+
310
+ chatContent.appendChild(messageWrapper);
311
+ scrollToBottom();
312
+
313
+ return messageBubble;
314
+ }
315
+
316
+ function scrollToBottom() {
317
+ chatWindow.scrollTo({
318
+ top: chatWindow.scrollHeight,
319
+ behavior: 'smooth'
320
+ });
321
+ }
322
+
323
+ function addCopyButton(pre) {
324
+ const button = document.createElement('button');
325
+ button.className = 'copy-code-btn';
326
+ button.textContent = 'Copy';
327
+ pre.appendChild(button);
328
+
329
+ button.addEventListener('click', () => {
330
+ const code = pre.querySelector('code').innerText;
331
+ navigator.clipboard.writeText(code).then(() => {
332
+ button.textContent = 'Copied!';
333
+ setTimeout(() => button.textContent = 'Copy', 2000);
334
+ });
335
+ });
336
+ }
337
+ });
338
+ </script>
339
+ </body>
340
+ </html>
341
+
342
+