fixed linting
Browse files- lightrag/api/lightrag_server.py +29 -28
- lightrag/api/static/index.html +61 -61
lightrag/api/lightrag_server.py
CHANGED
@@ -468,13 +468,12 @@ def parse_args() -> argparse.Namespace:
|
|
468 |
help="Path to SSL private key file (required if --ssl is enabled)",
|
469 |
)
|
470 |
parser.add_argument(
|
471 |
-
|
472 |
-
action=
|
473 |
default=False,
|
474 |
-
help=
|
475 |
)
|
476 |
|
477 |
-
|
478 |
args = parser.parse_args()
|
479 |
|
480 |
return args
|
@@ -912,7 +911,7 @@ def create_app(args):
|
|
912 |
"""Lifespan context manager for startup and shutdown events"""
|
913 |
# Startup logic
|
914 |
# Now only if this option is active, we can scan. This is better for big databases where there are hundreds of
|
915 |
-
# files. Makes the startup faster
|
916 |
if args.auto_scan_at_startup:
|
917 |
ASCIIColors.info("Auto scan is active, rescanning the input directory.")
|
918 |
try:
|
@@ -923,8 +922,10 @@ def create_app(args):
|
|
923 |
except Exception as e:
|
924 |
trace_exception(e)
|
925 |
logging.error(f"Error indexing file {file_path}: {str(e)}")
|
926 |
-
|
927 |
-
logging.info(
|
|
|
|
|
928 |
except Exception as e:
|
929 |
logging.error(f"Error during startup indexing: {str(e)}")
|
930 |
|
@@ -932,17 +933,17 @@ def create_app(args):
|
|
932 |
async def scan_for_new_documents():
|
933 |
"""
|
934 |
Manually trigger scanning for new documents in the directory managed by `doc_manager`.
|
935 |
-
|
936 |
This endpoint facilitates manual initiation of a document scan to identify and index new files.
|
937 |
It processes all newly detected files, attempts indexing each file, logs any errors that occur,
|
938 |
and returns a summary of the operation.
|
939 |
-
|
940 |
Returns:
|
941 |
dict: A dictionary containing:
|
942 |
- "status" (str): Indicates success or failure of the scanning process.
|
943 |
- "indexed_count" (int): The number of successfully indexed documents.
|
944 |
- "total_documents" (int): Total number of documents that have been indexed so far.
|
945 |
-
|
946 |
Raises:
|
947 |
HTTPException: If an error occurs during the document scanning process, a 500 status
|
948 |
code is returned with details about the exception.
|
@@ -970,25 +971,25 @@ def create_app(args):
|
|
970 |
async def upload_to_input_dir(file: UploadFile = File(...)):
|
971 |
"""
|
972 |
Endpoint for uploading a file to the input directory and indexing it.
|
973 |
-
|
974 |
-
This API endpoint accepts a file through an HTTP POST request, checks if the
|
975 |
uploaded file is of a supported type, saves it in the specified input directory,
|
976 |
indexes it for retrieval, and returns a success status with relevant details.
|
977 |
-
|
978 |
Parameters:
|
979 |
file (UploadFile): The file to be uploaded. It must have an allowed extension as per
|
980 |
`doc_manager.supported_extensions`.
|
981 |
-
|
982 |
Returns:
|
983 |
-
dict: A dictionary containing the upload status ("success"),
|
984 |
-
a message detailing the operation result, and
|
985 |
the total number of indexed documents.
|
986 |
-
|
987 |
Raises:
|
988 |
HTTPException: If the file type is not supported, it raises a 400 Bad Request error.
|
989 |
If any other exception occurs during the file handling or indexing,
|
990 |
it raises a 500 Internal Server Error with details about the exception.
|
991 |
-
"""
|
992 |
try:
|
993 |
if not doc_manager.is_supported_file(file.filename):
|
994 |
raise HTTPException(
|
@@ -1017,23 +1018,23 @@ def create_app(args):
|
|
1017 |
async def query_text(request: QueryRequest):
|
1018 |
"""
|
1019 |
Handle a POST request at the /query endpoint to process user queries using RAG capabilities.
|
1020 |
-
|
1021 |
Parameters:
|
1022 |
request (QueryRequest): A Pydantic model containing the following fields:
|
1023 |
- query (str): The text of the user's query.
|
1024 |
- mode (ModeEnum): Optional. Specifies the mode of retrieval augmentation.
|
1025 |
- stream (bool): Optional. Determines if the response should be streamed.
|
1026 |
- only_need_context (bool): Optional. If true, returns only the context without further processing.
|
1027 |
-
|
1028 |
Returns:
|
1029 |
-
QueryResponse: A Pydantic model containing the result of the query processing.
|
1030 |
If a string is returned (e.g., cache hit), it's directly returned.
|
1031 |
Otherwise, an async generator may be used to build the response.
|
1032 |
-
|
1033 |
Raises:
|
1034 |
HTTPException: Raised when an error occurs during the request handling process,
|
1035 |
with status code 500 and detail containing the exception message.
|
1036 |
-
"""
|
1037 |
try:
|
1038 |
response = await rag.aquery(
|
1039 |
request.query,
|
@@ -1074,7 +1075,7 @@ def create_app(args):
|
|
1074 |
|
1075 |
Returns:
|
1076 |
StreamingResponse: A streaming response containing the RAG query results.
|
1077 |
-
"""
|
1078 |
try:
|
1079 |
response = await rag.aquery( # Use aquery instead of query, and add await
|
1080 |
request.query,
|
@@ -1134,7 +1135,7 @@ def create_app(args):
|
|
1134 |
|
1135 |
Returns:
|
1136 |
InsertResponse: A response object containing the status of the operation, a message, and the number of documents inserted.
|
1137 |
-
"""
|
1138 |
try:
|
1139 |
await rag.ainsert(request.text)
|
1140 |
return InsertResponse(
|
@@ -1772,7 +1773,7 @@ def create_app(args):
|
|
1772 |
"max_tokens": args.max_tokens,
|
1773 |
},
|
1774 |
}
|
1775 |
-
|
1776 |
# Serve the static files
|
1777 |
static_dir = Path(__file__).parent / "static"
|
1778 |
static_dir.mkdir(exist_ok=True)
|
@@ -1780,13 +1781,13 @@ def create_app(args):
|
|
1780 |
|
1781 |
return app
|
1782 |
|
1783 |
-
|
1784 |
def main():
|
1785 |
args = parse_args()
|
1786 |
import uvicorn
|
1787 |
|
1788 |
app = create_app(args)
|
1789 |
-
display_splash_screen(args)
|
1790 |
uvicorn_config = {
|
1791 |
"app": app,
|
1792 |
"host": args.host,
|
|
|
468 |
help="Path to SSL private key file (required if --ssl is enabled)",
|
469 |
)
|
470 |
parser.add_argument(
|
471 |
+
"--auto-scan-at-startup",
|
472 |
+
action="store_true",
|
473 |
default=False,
|
474 |
+
help="Enable automatic scanning when the program starts",
|
475 |
)
|
476 |
|
|
|
477 |
args = parser.parse_args()
|
478 |
|
479 |
return args
|
|
|
911 |
"""Lifespan context manager for startup and shutdown events"""
|
912 |
# Startup logic
|
913 |
# Now only if this option is active, we can scan. This is better for big databases where there are hundreds of
|
914 |
+
# files. Makes the startup faster
|
915 |
if args.auto_scan_at_startup:
|
916 |
ASCIIColors.info("Auto scan is active, rescanning the input directory.")
|
917 |
try:
|
|
|
922 |
except Exception as e:
|
923 |
trace_exception(e)
|
924 |
logging.error(f"Error indexing file {file_path}: {str(e)}")
|
925 |
+
|
926 |
+
logging.info(
|
927 |
+
f"Indexed {len(new_files)} documents from {args.input_dir}"
|
928 |
+
)
|
929 |
except Exception as e:
|
930 |
logging.error(f"Error during startup indexing: {str(e)}")
|
931 |
|
|
|
933 |
async def scan_for_new_documents():
|
934 |
"""
|
935 |
Manually trigger scanning for new documents in the directory managed by `doc_manager`.
|
936 |
+
|
937 |
This endpoint facilitates manual initiation of a document scan to identify and index new files.
|
938 |
It processes all newly detected files, attempts indexing each file, logs any errors that occur,
|
939 |
and returns a summary of the operation.
|
940 |
+
|
941 |
Returns:
|
942 |
dict: A dictionary containing:
|
943 |
- "status" (str): Indicates success or failure of the scanning process.
|
944 |
- "indexed_count" (int): The number of successfully indexed documents.
|
945 |
- "total_documents" (int): Total number of documents that have been indexed so far.
|
946 |
+
|
947 |
Raises:
|
948 |
HTTPException: If an error occurs during the document scanning process, a 500 status
|
949 |
code is returned with details about the exception.
|
|
|
971 |
async def upload_to_input_dir(file: UploadFile = File(...)):
|
972 |
"""
|
973 |
Endpoint for uploading a file to the input directory and indexing it.
|
974 |
+
|
975 |
+
This API endpoint accepts a file through an HTTP POST request, checks if the
|
976 |
uploaded file is of a supported type, saves it in the specified input directory,
|
977 |
indexes it for retrieval, and returns a success status with relevant details.
|
978 |
+
|
979 |
Parameters:
|
980 |
file (UploadFile): The file to be uploaded. It must have an allowed extension as per
|
981 |
`doc_manager.supported_extensions`.
|
982 |
+
|
983 |
Returns:
|
984 |
+
dict: A dictionary containing the upload status ("success"),
|
985 |
+
a message detailing the operation result, and
|
986 |
the total number of indexed documents.
|
987 |
+
|
988 |
Raises:
|
989 |
HTTPException: If the file type is not supported, it raises a 400 Bad Request error.
|
990 |
If any other exception occurs during the file handling or indexing,
|
991 |
it raises a 500 Internal Server Error with details about the exception.
|
992 |
+
"""
|
993 |
try:
|
994 |
if not doc_manager.is_supported_file(file.filename):
|
995 |
raise HTTPException(
|
|
|
1018 |
async def query_text(request: QueryRequest):
|
1019 |
"""
|
1020 |
Handle a POST request at the /query endpoint to process user queries using RAG capabilities.
|
1021 |
+
|
1022 |
Parameters:
|
1023 |
request (QueryRequest): A Pydantic model containing the following fields:
|
1024 |
- query (str): The text of the user's query.
|
1025 |
- mode (ModeEnum): Optional. Specifies the mode of retrieval augmentation.
|
1026 |
- stream (bool): Optional. Determines if the response should be streamed.
|
1027 |
- only_need_context (bool): Optional. If true, returns only the context without further processing.
|
1028 |
+
|
1029 |
Returns:
|
1030 |
+
QueryResponse: A Pydantic model containing the result of the query processing.
|
1031 |
If a string is returned (e.g., cache hit), it's directly returned.
|
1032 |
Otherwise, an async generator may be used to build the response.
|
1033 |
+
|
1034 |
Raises:
|
1035 |
HTTPException: Raised when an error occurs during the request handling process,
|
1036 |
with status code 500 and detail containing the exception message.
|
1037 |
+
"""
|
1038 |
try:
|
1039 |
response = await rag.aquery(
|
1040 |
request.query,
|
|
|
1075 |
|
1076 |
Returns:
|
1077 |
StreamingResponse: A streaming response containing the RAG query results.
|
1078 |
+
"""
|
1079 |
try:
|
1080 |
response = await rag.aquery( # Use aquery instead of query, and add await
|
1081 |
request.query,
|
|
|
1135 |
|
1136 |
Returns:
|
1137 |
InsertResponse: A response object containing the status of the operation, a message, and the number of documents inserted.
|
1138 |
+
"""
|
1139 |
try:
|
1140 |
await rag.ainsert(request.text)
|
1141 |
return InsertResponse(
|
|
|
1773 |
"max_tokens": args.max_tokens,
|
1774 |
},
|
1775 |
}
|
1776 |
+
|
1777 |
# Serve the static files
|
1778 |
static_dir = Path(__file__).parent / "static"
|
1779 |
static_dir.mkdir(exist_ok=True)
|
|
|
1781 |
|
1782 |
return app
|
1783 |
|
1784 |
+
|
1785 |
def main():
|
1786 |
args = parse_args()
|
1787 |
import uvicorn
|
1788 |
|
1789 |
app = create_app(args)
|
1790 |
+
display_splash_screen(args)
|
1791 |
uvicorn_config = {
|
1792 |
"app": app,
|
1793 |
"host": args.host,
|
lightrag/api/static/index.html
CHANGED
@@ -11,7 +11,7 @@
|
|
11 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/prism.min.js"></script>
|
12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-python.min.js"></script>
|
13 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-javascript.min.js"></script>
|
14 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-css.min.js"></script>
|
15 |
<style>
|
16 |
body {
|
17 |
font-family: 'Inter', sans-serif;
|
@@ -37,12 +37,12 @@
|
|
37 |
overflow-x: auto;
|
38 |
margin: 1rem 0;
|
39 |
}
|
40 |
-
|
41 |
code {
|
42 |
font-family: 'Fira Code', monospace;
|
43 |
font-size: 0.9em;
|
44 |
}
|
45 |
-
|
46 |
/* Inline code styling */
|
47 |
:not(pre) > code {
|
48 |
background: #f4f4f4;
|
@@ -50,7 +50,7 @@
|
|
50 |
border-radius: 0.3em;
|
51 |
font-size: 0.9em;
|
52 |
}
|
53 |
-
|
54 |
/* Prose modifications for better markdown rendering */
|
55 |
.prose pre {
|
56 |
background: #f4f4f4;
|
@@ -58,7 +58,7 @@
|
|
58 |
border-radius: 0.5rem;
|
59 |
margin: 1rem 0;
|
60 |
}
|
61 |
-
|
62 |
.prose code {
|
63 |
color: #374151;
|
64 |
background: #f4f4f4;
|
@@ -66,7 +66,7 @@
|
|
66 |
border-radius: 0.3em;
|
67 |
font-size: 0.9em;
|
68 |
}
|
69 |
-
|
70 |
.prose {
|
71 |
max-width: none;
|
72 |
}
|
@@ -82,14 +82,14 @@
|
|
82 |
<!-- Health Check Button -->
|
83 |
<button id="healthCheckBtn" class="p-2 text-slate-600 hover:text-slate-800 transition-colors rounded-lg hover:bg-slate-100">
|
84 |
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
85 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
86 |
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
87 |
</svg>
|
88 |
</button>
|
89 |
<!-- Settings Button -->
|
90 |
<button id="settingsBtn" class="p-2 text-slate-600 hover:text-slate-800 transition-colors rounded-lg hover:bg-slate-100">
|
91 |
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
92 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
93 |
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
94 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
95 |
d="M12 15a3 3 0 100-6 3 3 0 000 6z" />
|
@@ -107,7 +107,7 @@
|
|
107 |
<section class="mb-8">
|
108 |
<div class="bg-slate-50 p-6 rounded-lg">
|
109 |
<h2 class="text-xl font-semibold text-slate-800 mb-4">Upload Documents</h2>
|
110 |
-
|
111 |
<!-- Upload Form -->
|
112 |
<form id="uploadForm" class="space-y-4">
|
113 |
<!-- Drop Zone -->
|
@@ -135,9 +135,9 @@
|
|
135 |
</div>
|
136 |
|
137 |
<!-- Upload Button -->
|
138 |
-
<button type="submit"
|
139 |
-
class="w-full bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700
|
140 |
-
transition-colors font-medium focus:outline-none focus:ring-2
|
141 |
focus:ring-blue-500 focus:ring-offset-2">
|
142 |
Upload Documents
|
143 |
</button>
|
@@ -149,19 +149,19 @@
|
|
149 |
<section>
|
150 |
<div class="bg-slate-50 p-6 rounded-lg">
|
151 |
<h2 class="text-xl font-semibold text-slate-800 mb-4">Query Documents</h2>
|
152 |
-
|
153 |
<form id="queryForm" class="space-y-4">
|
154 |
<textarea id="queryInput"
|
155 |
-
class="w-full p-4 border border-slate-300 rounded-lg focus:ring-2
|
156 |
focus:ring-blue-500 focus:border-blue-500 transition-all
|
157 |
min-h-[120px] resize-y"
|
158 |
placeholder="Enter your query here..."
|
159 |
></textarea>
|
160 |
|
161 |
<button type="submit"
|
162 |
-
class="w-full bg-green-600 text-white px-6 py-3 rounded-lg
|
163 |
hover:bg-green-700 transition-colors font-medium
|
164 |
-
focus:outline-none focus:ring-2 focus:ring-green-500
|
165 |
focus:ring-offset-2">
|
166 |
Submit Query
|
167 |
</button>
|
@@ -178,11 +178,11 @@
|
|
178 |
<div id="settingsModal" class="hidden fixed inset-0 bg-slate-900/50 backdrop-blur-sm flex items-center justify-center">
|
179 |
<div class="bg-white rounded-xl shadow-lg p-6 w-full max-w-md m-4">
|
180 |
<h3 class="text-lg font-semibold text-slate-900 mb-4">Settings</h3>
|
181 |
-
<input type="text" id="apiKeyInput"
|
182 |
-
class="w-full p-2 border rounded focus:ring-2 focus:ring-blue-500"
|
183 |
placeholder="Enter API Key">
|
184 |
<div class="mt-6 flex justify-end space-x-4">
|
185 |
-
<button id="closeSettingsBtn"
|
186 |
class="px-4 py-2 text-slate-700 hover:text-slate-900">
|
187 |
Cancel
|
188 |
</button>
|
@@ -239,7 +239,7 @@
|
|
239 |
<div class="flex items-center justify-between p-3 bg-white rounded-lg border">
|
240 |
<div class="flex items-center">
|
241 |
<svg class="w-5 h-5 text-slate-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
242 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
243 |
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
244 |
</svg>
|
245 |
<span class="text-sm text-slate-600">${file.name}</span>
|
@@ -247,8 +247,8 @@
|
|
247 |
</div>
|
248 |
<button type="button" data-index="${index}" class="text-red-500 hover:text-red-700 p-1">
|
249 |
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
250 |
-
<path fill-rule="evenodd"
|
251 |
-
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
252 |
clip-rule="evenodd" />
|
253 |
</svg>
|
254 |
</button>
|
@@ -290,7 +290,7 @@
|
|
290 |
dropZone.addEventListener('click', () => {
|
291 |
fileInput.click();
|
292 |
});
|
293 |
-
|
294 |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
295 |
dropZone.addEventListener(eventName, (e) => {
|
296 |
e.preventDefault();
|
@@ -319,25 +319,25 @@
|
|
319 |
uploadForm.addEventListener('submit', async (e) => {
|
320 |
e.preventDefault();
|
321 |
const files = fileInput.files;
|
322 |
-
|
323 |
if (files.length === 0) {
|
324 |
uploadStatus.innerHTML = '<span class="text-red-500">Please select files to upload</span>';
|
325 |
return;
|
326 |
}
|
327 |
-
|
328 |
uploadProgress.classList.remove('hidden');
|
329 |
const progressBar = uploadProgress.querySelector('.bg-blue-600');
|
330 |
uploadStatus.textContent = 'Starting upload...';
|
331 |
-
|
332 |
try {
|
333 |
for (let i = 0; i < files.length; i++) {
|
334 |
const file = files[i];
|
335 |
const formData = new FormData();
|
336 |
formData.append('file', file);
|
337 |
-
|
338 |
uploadStatus.textContent = `Uploading ${file.name} (${i + 1}/${files.length})...`;
|
339 |
console.log(`Uploading file: ${file.name}`);
|
340 |
-
|
341 |
try {
|
342 |
const response = await fetch('/documents/upload', {
|
343 |
method: 'POST',
|
@@ -346,30 +346,30 @@
|
|
346 |
},
|
347 |
body: formData
|
348 |
});
|
349 |
-
|
350 |
console.log('Response status:', response.status);
|
351 |
-
|
352 |
if (!response.ok) {
|
353 |
const errorData = await response.json();
|
354 |
throw new Error(`Upload failed: ${errorData.detail || response.statusText}`);
|
355 |
}
|
356 |
-
|
357 |
// Update progress
|
358 |
const progress = ((i + 1) / files.length) * 100;
|
359 |
progressBar.style.width = `${progress}%`;
|
360 |
console.log(`Progress: ${progress}%`);
|
361 |
-
|
362 |
} catch (error) {
|
363 |
console.error('Upload error:', error);
|
364 |
uploadStatus.innerHTML = `<span class="text-red-500">Error uploading ${file.name}: ${error.message}</span>`;
|
365 |
return;
|
366 |
}
|
367 |
}
|
368 |
-
|
369 |
// All files uploaded successfully
|
370 |
uploadStatus.innerHTML = '<span class="text-green-500">All files uploaded successfully!</span>';
|
371 |
progressBar.style.width = '100%';
|
372 |
-
|
373 |
// Clear the file input and selection display
|
374 |
setTimeout(() => {
|
375 |
fileInput.value = '';
|
@@ -377,7 +377,7 @@
|
|
377 |
uploadProgress.classList.add('hidden');
|
378 |
progressBar.style.width = '0%';
|
379 |
}, 3000);
|
380 |
-
|
381 |
} catch (error) {
|
382 |
console.error('General upload error:', error);
|
383 |
uploadStatus.innerHTML = `<span class="text-red-500">Upload failed: ${error.message}</span>`;
|
@@ -391,20 +391,20 @@
|
|
391 |
button.className = 'copy-button absolute top-1 right-1 p-1 bg-slate-700/80 hover:bg-slate-700 text-white rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity';
|
392 |
button.innerHTML = `
|
393 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
394 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
395 |
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
396 |
</svg>
|
397 |
`;
|
398 |
-
|
399 |
pre.style.position = 'relative';
|
400 |
pre.classList.add('group');
|
401 |
-
|
402 |
button.addEventListener('click', async () => {
|
403 |
const codeElement = pre.querySelector('code');
|
404 |
if (!codeElement) return;
|
405 |
-
|
406 |
const text = codeElement.textContent;
|
407 |
-
|
408 |
try {
|
409 |
// First try using the Clipboard API
|
410 |
if (navigator.clipboard && window.isSecureContext) {
|
@@ -428,29 +428,29 @@
|
|
428 |
return;
|
429 |
}
|
430 |
}
|
431 |
-
|
432 |
// Show success feedback
|
433 |
button.innerHTML = `
|
434 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
435 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
436 |
</svg>
|
437 |
`;
|
438 |
-
|
439 |
// Reset button after 2 seconds
|
440 |
setTimeout(() => {
|
441 |
button.innerHTML = `
|
442 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
443 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
444 |
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
445 |
</svg>
|
446 |
`;
|
447 |
}, 2000);
|
448 |
-
|
449 |
} catch (err) {
|
450 |
console.error('Copy failed:', err);
|
451 |
}
|
452 |
});
|
453 |
-
|
454 |
pre.appendChild(button);
|
455 |
}
|
456 |
});
|
@@ -460,12 +460,12 @@
|
|
460 |
queryForm.addEventListener('submit', async (e) => {
|
461 |
e.preventDefault();
|
462 |
const query = queryInput.value.trim();
|
463 |
-
|
464 |
if (!query) {
|
465 |
queryResponse.innerHTML = '<p class="text-red-500">Please enter a query</p>';
|
466 |
return;
|
467 |
}
|
468 |
-
|
469 |
// Show loading state
|
470 |
queryResponse.innerHTML = `
|
471 |
<div class="animate-pulse">
|
@@ -478,10 +478,10 @@
|
|
478 |
</div>
|
479 |
</div>
|
480 |
`;
|
481 |
-
|
482 |
try {
|
483 |
console.log('Sending query:', query);
|
484 |
-
|
485 |
const response = await fetch('/query', {
|
486 |
method: 'POST',
|
487 |
headers: {
|
@@ -490,17 +490,17 @@
|
|
490 |
},
|
491 |
body: JSON.stringify({ query })
|
492 |
});
|
493 |
-
|
494 |
console.log('Response status:', response.status);
|
495 |
-
|
496 |
if (!response.ok) {
|
497 |
const errorData = await response.json();
|
498 |
throw new Error(`Query failed: ${errorData.detail || response.statusText}`);
|
499 |
}
|
500 |
-
|
501 |
const data = await response.json();
|
502 |
console.log('Query response:', data);
|
503 |
-
|
504 |
// Format and display the response
|
505 |
if (data.response) {
|
506 |
const formattedResponse = marked.parse(data.response, {
|
@@ -510,19 +510,19 @@
|
|
510 |
}
|
511 |
return code;
|
512 |
}
|
513 |
-
});
|
514 |
queryResponse.innerHTML = `
|
515 |
<div class="prose prose-slate max-w-none">
|
516 |
${formattedResponse}
|
517 |
</div>
|
518 |
`;
|
519 |
-
|
520 |
// Re-trigger Prism highlighting
|
521 |
Prism.highlightAllUnder(queryResponse);
|
522 |
} else {
|
523 |
queryResponse.innerHTML = '<p class="text-slate-600">No response data received</p>';
|
524 |
}
|
525 |
-
|
526 |
// Call this after loading markdown content
|
527 |
addCopyButtons();
|
528 |
// Optional: Add sources if available
|
@@ -534,7 +534,7 @@
|
|
534 |
${data.sources.map(source => `
|
535 |
<li class="flex items-center space-x-2">
|
536 |
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
537 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
538 |
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
539 |
</svg>
|
540 |
<span>${source}</span>
|
@@ -545,7 +545,7 @@
|
|
545 |
`;
|
546 |
queryResponse.insertAdjacentHTML('beforeend', sourcesHtml);
|
547 |
}
|
548 |
-
|
549 |
} catch (error) {
|
550 |
console.error('Query error:', error);
|
551 |
queryResponse.innerHTML = `
|
@@ -555,14 +555,14 @@
|
|
555 |
</div>
|
556 |
`;
|
557 |
}
|
558 |
-
|
559 |
// Optional: Add a copy button for the response
|
560 |
const copyButton = document.createElement('button');
|
561 |
copyButton.className = 'mt-4 px-3 py-1 text-sm text-slate-600 hover:text-slate-800 border border-slate-300 rounded hover:bg-slate-50 transition-colors';
|
562 |
copyButton.innerHTML = `
|
563 |
<span class="flex items-center space-x-1">
|
564 |
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
565 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
566 |
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
567 |
</svg>
|
568 |
<span>Copy Response</span>
|
@@ -583,7 +583,7 @@
|
|
583 |
copyButton.innerHTML = `
|
584 |
<span class="flex items-center space-x-1">
|
585 |
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
586 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
587 |
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
588 |
</svg>
|
589 |
<span>Copy Response</span>
|
|
|
11 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/prism.min.js"></script>
|
12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-python.min.js"></script>
|
13 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-javascript.min.js"></script>
|
14 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-css.min.js"></script>
|
15 |
<style>
|
16 |
body {
|
17 |
font-family: 'Inter', sans-serif;
|
|
|
37 |
overflow-x: auto;
|
38 |
margin: 1rem 0;
|
39 |
}
|
40 |
+
|
41 |
code {
|
42 |
font-family: 'Fira Code', monospace;
|
43 |
font-size: 0.9em;
|
44 |
}
|
45 |
+
|
46 |
/* Inline code styling */
|
47 |
:not(pre) > code {
|
48 |
background: #f4f4f4;
|
|
|
50 |
border-radius: 0.3em;
|
51 |
font-size: 0.9em;
|
52 |
}
|
53 |
+
|
54 |
/* Prose modifications for better markdown rendering */
|
55 |
.prose pre {
|
56 |
background: #f4f4f4;
|
|
|
58 |
border-radius: 0.5rem;
|
59 |
margin: 1rem 0;
|
60 |
}
|
61 |
+
|
62 |
.prose code {
|
63 |
color: #374151;
|
64 |
background: #f4f4f4;
|
|
|
66 |
border-radius: 0.3em;
|
67 |
font-size: 0.9em;
|
68 |
}
|
69 |
+
|
70 |
.prose {
|
71 |
max-width: none;
|
72 |
}
|
|
|
82 |
<!-- Health Check Button -->
|
83 |
<button id="healthCheckBtn" class="p-2 text-slate-600 hover:text-slate-800 transition-colors rounded-lg hover:bg-slate-100">
|
84 |
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
85 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
86 |
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
87 |
</svg>
|
88 |
</button>
|
89 |
<!-- Settings Button -->
|
90 |
<button id="settingsBtn" class="p-2 text-slate-600 hover:text-slate-800 transition-colors rounded-lg hover:bg-slate-100">
|
91 |
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
92 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
93 |
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
94 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
95 |
d="M12 15a3 3 0 100-6 3 3 0 000 6z" />
|
|
|
107 |
<section class="mb-8">
|
108 |
<div class="bg-slate-50 p-6 rounded-lg">
|
109 |
<h2 class="text-xl font-semibold text-slate-800 mb-4">Upload Documents</h2>
|
110 |
+
|
111 |
<!-- Upload Form -->
|
112 |
<form id="uploadForm" class="space-y-4">
|
113 |
<!-- Drop Zone -->
|
|
|
135 |
</div>
|
136 |
|
137 |
<!-- Upload Button -->
|
138 |
+
<button type="submit"
|
139 |
+
class="w-full bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700
|
140 |
+
transition-colors font-medium focus:outline-none focus:ring-2
|
141 |
focus:ring-blue-500 focus:ring-offset-2">
|
142 |
Upload Documents
|
143 |
</button>
|
|
|
149 |
<section>
|
150 |
<div class="bg-slate-50 p-6 rounded-lg">
|
151 |
<h2 class="text-xl font-semibold text-slate-800 mb-4">Query Documents</h2>
|
152 |
+
|
153 |
<form id="queryForm" class="space-y-4">
|
154 |
<textarea id="queryInput"
|
155 |
+
class="w-full p-4 border border-slate-300 rounded-lg focus:ring-2
|
156 |
focus:ring-blue-500 focus:border-blue-500 transition-all
|
157 |
min-h-[120px] resize-y"
|
158 |
placeholder="Enter your query here..."
|
159 |
></textarea>
|
160 |
|
161 |
<button type="submit"
|
162 |
+
class="w-full bg-green-600 text-white px-6 py-3 rounded-lg
|
163 |
hover:bg-green-700 transition-colors font-medium
|
164 |
+
focus:outline-none focus:ring-2 focus:ring-green-500
|
165 |
focus:ring-offset-2">
|
166 |
Submit Query
|
167 |
</button>
|
|
|
178 |
<div id="settingsModal" class="hidden fixed inset-0 bg-slate-900/50 backdrop-blur-sm flex items-center justify-center">
|
179 |
<div class="bg-white rounded-xl shadow-lg p-6 w-full max-w-md m-4">
|
180 |
<h3 class="text-lg font-semibold text-slate-900 mb-4">Settings</h3>
|
181 |
+
<input type="text" id="apiKeyInput"
|
182 |
+
class="w-full p-2 border rounded focus:ring-2 focus:ring-blue-500"
|
183 |
placeholder="Enter API Key">
|
184 |
<div class="mt-6 flex justify-end space-x-4">
|
185 |
+
<button id="closeSettingsBtn"
|
186 |
class="px-4 py-2 text-slate-700 hover:text-slate-900">
|
187 |
Cancel
|
188 |
</button>
|
|
|
239 |
<div class="flex items-center justify-between p-3 bg-white rounded-lg border">
|
240 |
<div class="flex items-center">
|
241 |
<svg class="w-5 h-5 text-slate-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
242 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
243 |
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
244 |
</svg>
|
245 |
<span class="text-sm text-slate-600">${file.name}</span>
|
|
|
247 |
</div>
|
248 |
<button type="button" data-index="${index}" class="text-red-500 hover:text-red-700 p-1">
|
249 |
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
250 |
+
<path fill-rule="evenodd"
|
251 |
+
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
252 |
clip-rule="evenodd" />
|
253 |
</svg>
|
254 |
</button>
|
|
|
290 |
dropZone.addEventListener('click', () => {
|
291 |
fileInput.click();
|
292 |
});
|
293 |
+
|
294 |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
295 |
dropZone.addEventListener(eventName, (e) => {
|
296 |
e.preventDefault();
|
|
|
319 |
uploadForm.addEventListener('submit', async (e) => {
|
320 |
e.preventDefault();
|
321 |
const files = fileInput.files;
|
322 |
+
|
323 |
if (files.length === 0) {
|
324 |
uploadStatus.innerHTML = '<span class="text-red-500">Please select files to upload</span>';
|
325 |
return;
|
326 |
}
|
327 |
+
|
328 |
uploadProgress.classList.remove('hidden');
|
329 |
const progressBar = uploadProgress.querySelector('.bg-blue-600');
|
330 |
uploadStatus.textContent = 'Starting upload...';
|
331 |
+
|
332 |
try {
|
333 |
for (let i = 0; i < files.length; i++) {
|
334 |
const file = files[i];
|
335 |
const formData = new FormData();
|
336 |
formData.append('file', file);
|
337 |
+
|
338 |
uploadStatus.textContent = `Uploading ${file.name} (${i + 1}/${files.length})...`;
|
339 |
console.log(`Uploading file: ${file.name}`);
|
340 |
+
|
341 |
try {
|
342 |
const response = await fetch('/documents/upload', {
|
343 |
method: 'POST',
|
|
|
346 |
},
|
347 |
body: formData
|
348 |
});
|
349 |
+
|
350 |
console.log('Response status:', response.status);
|
351 |
+
|
352 |
if (!response.ok) {
|
353 |
const errorData = await response.json();
|
354 |
throw new Error(`Upload failed: ${errorData.detail || response.statusText}`);
|
355 |
}
|
356 |
+
|
357 |
// Update progress
|
358 |
const progress = ((i + 1) / files.length) * 100;
|
359 |
progressBar.style.width = `${progress}%`;
|
360 |
console.log(`Progress: ${progress}%`);
|
361 |
+
|
362 |
} catch (error) {
|
363 |
console.error('Upload error:', error);
|
364 |
uploadStatus.innerHTML = `<span class="text-red-500">Error uploading ${file.name}: ${error.message}</span>`;
|
365 |
return;
|
366 |
}
|
367 |
}
|
368 |
+
|
369 |
// All files uploaded successfully
|
370 |
uploadStatus.innerHTML = '<span class="text-green-500">All files uploaded successfully!</span>';
|
371 |
progressBar.style.width = '100%';
|
372 |
+
|
373 |
// Clear the file input and selection display
|
374 |
setTimeout(() => {
|
375 |
fileInput.value = '';
|
|
|
377 |
uploadProgress.classList.add('hidden');
|
378 |
progressBar.style.width = '0%';
|
379 |
}, 3000);
|
380 |
+
|
381 |
} catch (error) {
|
382 |
console.error('General upload error:', error);
|
383 |
uploadStatus.innerHTML = `<span class="text-red-500">Upload failed: ${error.message}</span>`;
|
|
|
391 |
button.className = 'copy-button absolute top-1 right-1 p-1 bg-slate-700/80 hover:bg-slate-700 text-white rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity';
|
392 |
button.innerHTML = `
|
393 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
394 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
395 |
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
396 |
</svg>
|
397 |
`;
|
398 |
+
|
399 |
pre.style.position = 'relative';
|
400 |
pre.classList.add('group');
|
401 |
+
|
402 |
button.addEventListener('click', async () => {
|
403 |
const codeElement = pre.querySelector('code');
|
404 |
if (!codeElement) return;
|
405 |
+
|
406 |
const text = codeElement.textContent;
|
407 |
+
|
408 |
try {
|
409 |
// First try using the Clipboard API
|
410 |
if (navigator.clipboard && window.isSecureContext) {
|
|
|
428 |
return;
|
429 |
}
|
430 |
}
|
431 |
+
|
432 |
// Show success feedback
|
433 |
button.innerHTML = `
|
434 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
435 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
436 |
</svg>
|
437 |
`;
|
438 |
+
|
439 |
// Reset button after 2 seconds
|
440 |
setTimeout(() => {
|
441 |
button.innerHTML = `
|
442 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
443 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
444 |
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
445 |
</svg>
|
446 |
`;
|
447 |
}, 2000);
|
448 |
+
|
449 |
} catch (err) {
|
450 |
console.error('Copy failed:', err);
|
451 |
}
|
452 |
});
|
453 |
+
|
454 |
pre.appendChild(button);
|
455 |
}
|
456 |
});
|
|
|
460 |
queryForm.addEventListener('submit', async (e) => {
|
461 |
e.preventDefault();
|
462 |
const query = queryInput.value.trim();
|
463 |
+
|
464 |
if (!query) {
|
465 |
queryResponse.innerHTML = '<p class="text-red-500">Please enter a query</p>';
|
466 |
return;
|
467 |
}
|
468 |
+
|
469 |
// Show loading state
|
470 |
queryResponse.innerHTML = `
|
471 |
<div class="animate-pulse">
|
|
|
478 |
</div>
|
479 |
</div>
|
480 |
`;
|
481 |
+
|
482 |
try {
|
483 |
console.log('Sending query:', query);
|
484 |
+
|
485 |
const response = await fetch('/query', {
|
486 |
method: 'POST',
|
487 |
headers: {
|
|
|
490 |
},
|
491 |
body: JSON.stringify({ query })
|
492 |
});
|
493 |
+
|
494 |
console.log('Response status:', response.status);
|
495 |
+
|
496 |
if (!response.ok) {
|
497 |
const errorData = await response.json();
|
498 |
throw new Error(`Query failed: ${errorData.detail || response.statusText}`);
|
499 |
}
|
500 |
+
|
501 |
const data = await response.json();
|
502 |
console.log('Query response:', data);
|
503 |
+
|
504 |
// Format and display the response
|
505 |
if (data.response) {
|
506 |
const formattedResponse = marked.parse(data.response, {
|
|
|
510 |
}
|
511 |
return code;
|
512 |
}
|
513 |
+
});
|
514 |
queryResponse.innerHTML = `
|
515 |
<div class="prose prose-slate max-w-none">
|
516 |
${formattedResponse}
|
517 |
</div>
|
518 |
`;
|
519 |
+
|
520 |
// Re-trigger Prism highlighting
|
521 |
Prism.highlightAllUnder(queryResponse);
|
522 |
} else {
|
523 |
queryResponse.innerHTML = '<p class="text-slate-600">No response data received</p>';
|
524 |
}
|
525 |
+
|
526 |
// Call this after loading markdown content
|
527 |
addCopyButtons();
|
528 |
// Optional: Add sources if available
|
|
|
534 |
${data.sources.map(source => `
|
535 |
<li class="flex items-center space-x-2">
|
536 |
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
537 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
538 |
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
539 |
</svg>
|
540 |
<span>${source}</span>
|
|
|
545 |
`;
|
546 |
queryResponse.insertAdjacentHTML('beforeend', sourcesHtml);
|
547 |
}
|
548 |
+
|
549 |
} catch (error) {
|
550 |
console.error('Query error:', error);
|
551 |
queryResponse.innerHTML = `
|
|
|
555 |
</div>
|
556 |
`;
|
557 |
}
|
558 |
+
|
559 |
// Optional: Add a copy button for the response
|
560 |
const copyButton = document.createElement('button');
|
561 |
copyButton.className = 'mt-4 px-3 py-1 text-sm text-slate-600 hover:text-slate-800 border border-slate-300 rounded hover:bg-slate-50 transition-colors';
|
562 |
copyButton.innerHTML = `
|
563 |
<span class="flex items-center space-x-1">
|
564 |
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
565 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
566 |
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
567 |
</svg>
|
568 |
<span>Copy Response</span>
|
|
|
583 |
copyButton.innerHTML = `
|
584 |
<span class="flex items-center space-x-1">
|
585 |
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
586 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
587 |
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
588 |
</svg>
|
589 |
<span>Copy Response</span>
|