1Egyb commited on
Commit
f39f28f
·
verified ·
1 Parent(s): 7e014a3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -155
app.py CHANGED
@@ -1,12 +1,18 @@
1
- r"""
2
- Smart Dev Sandbox - تطبيق بسيط وآمن يدعم ZIP / DOCX / PDF مع واجهة Gradio
3
- متطلبات:
4
- pip install gradio huggingface-hub pyzipper python-docx PyPDF2
5
-
6
- متغيرات بيئة:
7
- HF_TOKEN (اختياري، لتكامل Hugging Face)
8
- HF_MODEL_ID (اختياري، معرف النموذج)
9
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  import os
11
  import re
12
  import shutil
@@ -17,109 +23,128 @@ from typing import Dict, Tuple, Optional, List
17
 
18
  import pyzipper
19
  import gradio as gr
 
 
 
20
 
21
- # استيراد مكتبات لتحليل docx/pdf
22
- try:
23
- from docx import Document
24
- except Exception:
25
- Document = None
26
-
27
- try:
28
- from PyPDF2 import PdfReader
29
- except Exception:
30
- PdfReader = None
31
-
32
- # إعدادات عامة
33
- ALLOWED_EXTENSIONS = {
34
- '.py', '.js', '.html', '.css', '.txt', '.json',
35
- '.md', '.php', '.yml', '.yaml', '.docx', '.pdf'
36
- }
37
  MODEL_ID = os.environ.get('HF_MODEL_ID', 'moonshotai/Kimi-K2-Instruct')
38
  HF_TOKEN = os.environ.get('HF_TOKEN')
39
 
40
  if not HF_TOKEN:
41
- print("ملاحظة: HF_TOKEN غير معطى سيعمل التطبيق لكن بدون اتصال بنموذج Hugging Face.")
 
 
42
 
43
- # ---------- Sandbox بسيط وآمن ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  class Sandbox:
45
  def __init__(self):
46
  self.dir: Optional[str] = None
47
  self.files_content: Dict[str, str] = {}
48
- self.original_file_path: Optional[str] = None
 
49
 
50
  def cleanup(self):
51
  if self.dir and os.path.isdir(self.dir):
52
- shutil.rmtree(self.dir, ignore_errors=True)
 
 
 
53
  self.dir = None
54
  self.files_content = {}
55
- self.original_file_path = None
 
56
 
57
- def reset(self, file_path: str, password: Optional[str] = None) -> Tuple[Optional[str], str]:
58
- """
59
- يدعم: .zip (استخراج)، .docx (استخراج نص)، .pdf (استخراج نص).
60
- يعيد (path, message) أو (None, error_message).
61
- """
62
  self.cleanup()
63
- if not file_path:
64
- return None, "⚠️ يرجى رفع ملف ZIP / DOCX / PDF أولاً."
65
  self.dir = tempfile.mkdtemp(prefix='sandbox_')
66
- self.original_file_path = file_path
 
67
  try:
68
- ext = Path(file_path).suffix.lower()
69
- if ext == '.zip':
70
- with pyzipper.AESZipFile(file_path, 'r') as zf:
71
- # استخراج آمن: منع zip-slip
72
- for info in zf.infolist():
73
- name = info.filename
74
- if name.startswith('__MACOSX') or name.strip() == '':
75
- continue
76
- # منع المسارات المطلقة أو ../../
77
- safe_name = os.path.normpath(name).lstrip(os.sep)
78
- dest_path = os.path.join(self.dir, safe_name)
79
- os.makedirs(os.path.dirname(dest_path), exist_ok=True)
80
- zf.extract(info, path=self.dir)
81
- elif ext == '.docx':
82
- if Document is None:
83
- raise RuntimeError("مكتبة python-docx غير مثبتة")
84
- doc = Document(file_path)
85
- out_txt = os.path.join(self.dir, 'document.txt')
86
- os.makedirs(os.path.dirname(out_txt), exist_ok=True)
87
- with open(out_txt, 'w', encoding='utf-8') as f:
88
- for para in doc.paragraphs:
89
- f.write(para.text + '\n')
90
- elif ext == '.pdf':
91
- if PdfReader is None:
92
- raise RuntimeError("مكتبة PyPDF2 غير مثبتة")
93
- reader = PdfReader(file_path)
94
- out_txt = os.path.join(self.dir, 'document.txt')
95
- os.makedirs(os.path.dirname(out_txt), exist_ok=True)
96
- with open(out_txt, 'w', encoding='utf-8') as f:
97
- for page in reader.pages:
98
- text = page.extract_text() or ''
99
- f.write(text + '\n')
100
- else:
101
- self.cleanup()
102
- return None, "❌ صيغة الملف غير مدعومة. ادعم: .zip, .docx, .pdf"
103
-
104
- # جمع المحتويات للعرض / للسياق
105
- self.files_content = {}
106
- for p in Path(self.dir).rglob('*'):
107
- if p.is_file():
108
- rel = str(p.relative_to(self.dir))
109
- try:
110
- self.files_content[rel] = p.read_text(encoding='utf-8', errors='ignore')
111
- except Exception:
112
- self.files_content[rel] = "/* لا يمكن قراءة الملف */"
113
-
114
- return file_path, f"✅ تم تجهيز {len(self.files_content)} ملف/ملفات داخل الـ sandbox."
115
  except Exception as e:
 
116
  self.cleanup()
117
- return None, f"❌ استثناء أثناء المعالجة: {e}"
118
 
119
  def get_context(self) -> str:
120
- if not self.files_content:
121
- return ""
122
- return "\n".join(f"--- FILE: {name} ---\n{content}" for name, content in self.files_content.items())
 
123
 
124
  def update_file(self, name: str, content: str) -> None:
125
  if not self.dir:
@@ -134,83 +159,124 @@ class Sandbox:
134
  def create_zip(self) -> Tuple[Optional[str], str]:
135
  if not self.dir:
136
  return None, "❌ لا توجد ملفات لحزمها"
137
- fd, temp_zip = tempfile.mkstemp(prefix='modified_', suffix='.zip')
138
- os.close(fd)
139
- base = temp_zip[:-4]
140
- shutil.make_archive(base, 'zip', self.dir)
141
- return base + '.zip', "✅ تم إنشاء ZIP المعدل"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
 
 
 
 
 
 
 
 
 
 
143
  sandbox = Sandbox()
144
 
145
- # ---------- واجهة Gradio ----------
146
- with gr.Blocks(title="Smart Dev Sandbox") as demo:
147
- gr.Markdown("# Smart Dev Sandbox دعم ZIP / DOCX / PDF")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  with gr.Row():
149
  with gr.Column(scale=3):
150
- chatbot = gr.Chatbot(label="محادثة")
151
- msg = gr.Textbox(placeholder="اطرح سؤالاً أو اطلب تعديلًا...", label="الرسالة")
152
  send_btn = gr.Button("إرسال")
153
  with gr.Column(scale=1):
154
- file_input = gr.File(label="اختر ملف (ZIP / DOCX / PDF)", file_types=[".zip", ".docx", ".pdf"])
155
- pwd_input = gr.Textbox(label="كلمة مرور ZIP (اختياري)", type="password")
156
- analyze_btn = gr.Button("🔍 تجهيز الملف")
157
- status_md = gr.Markdown("**الحالة:** جاهز")
158
  gr.HTML("<hr>")
159
- save_btn = gr.Button("✨ تطبيق التعديلات وإنشاء ZIP")
160
- download_file = gr.File(label="تحميل الناتج")
161
-
162
- # دوال الواجهة
163
- def api_reset(uploaded_file, password):
164
- if uploaded_file is None:
165
- return None, "⚠️ ارفع ملفًا أولًا"
166
- # gradio يعطي كائن له .name أو .temp_path
167
- file_path = getattr(uploaded_file, "name", None) or getattr(uploaded_file, "temp_path", None) or uploaded_file
168
- return sandbox.reset(file_path, password)
169
-
170
- def api_send_message(user_message, chat_history):
171
- chat_history = chat_history or []
172
- # يسمح بالدردشة حتى بدون ملف مرفوع
173
- chat_history.append(("user", user_message))
174
- if sandbox.files_content:
175
- ctx = sandbox.get_context()
176
- assistant_reply = f"تم استلام طلب تعديل. (سياق المشروع مرفوع — {len(sandbox.files_content)} ملف)." # placeholder
177
- else:
178
- assistant_reply = "يمكنني الإجابة على الأسئلة العامة الآن. لطلب تعديل ملفات، ارفع ZIP/DOCX/PDF ثم اضغط 'تجهيز الملف'."
179
- chat_history.append(("assistant", assistant_reply))
180
- return chat_history
181
 
182
- def api_apply_and_package(chat_history):
183
- # يأخذ آخر رد مساعد ويستخرج كتل التعديل بصيغة [ملف: path]```...```
184
- if not sandbox.files_content:
185
- return None, "⚠️ لا ملفات داخل الـ sandbox للتعديل."
186
- if not chat_history:
187
- return None, "⚠️ المحادثة فارغة."
188
- # إيجاد آخر رد المساعد
189
- last = None
190
- for role, content in reversed(chat_history):
191
- if role == 'assistant' and content and content.strip():
192
- last = content
193
- break
194
- if not last:
195
- return None, "⚠️ لا توجد تعديلات في المحادثة."
196
- # استخراج كتل التعديل
197
- pattern = re.compile(r"\[\s*(?:ملف|file)\s*:\s*([^\]]+?)\s*\]\s*```(?:[\\w\\-+]+)?\\n(.*?)\\n```", re.IGNORECASE | re.DOTALL)
198
- matches = pattern.findall(last)
199
- if not matches:
200
- return None, "⚠️ لم يعثر على كتل تعديل بصيغة [ملف: ...]```...```"
201
- applied = 0
202
- for name, code in matches:
203
- try:
204
- sandbox.update_file(name.strip(), code)
205
- applied += 1
206
- except Exception as e:
207
- print(f"فشل تطبيق {name}: {e}")
208
- out_zip, msg = sandbox.create_zip()
209
- return out_zip, f"✅ تم تطبيق {applied} ملفات. {msg}"
210
-
211
- analyze_btn.click(api_reset, inputs=[file_input, pwd_input], outputs=[file_input, status_md])
212
  send_btn.click(api_send_message, inputs=[msg, chatbot], outputs=[chatbot])
213
- save_btn.click(api_apply_and_package, inputs=[chatbot], outputs=[download_file, status_md])
214
 
215
  if __name__ == "__main__":
216
  demo.launch()
 
 
 
 
 
 
 
 
 
1
  """
2
+ Smart Dev Sandbox - تطبيق Gradio احترافي للتعديل التفاعلي على ملفات ZIP/DOCX/PDF باستخدام AI
3
+ ملف: smart_dev_sandbox.py
4
+
5
+ المتطلبات:
6
+ - gradio
7
+ - huggingface-hub
8
+ - pyzipper
9
+ - python-docx
10
+ - PyPDF2
11
+
12
+ تشغيل:
13
+ python smart_dev_sandbox.py
14
+ """
15
+
16
  import os
17
  import re
18
  import shutil
 
23
 
24
  import pyzipper
25
  import gradio as gr
26
+ from huggingface_hub import InferenceClient
27
+ from docx import Document
28
+ import PyPDF2
29
 
30
+ # --------------------------- إعدادات عامة ---------------------------
31
+ ALLOWED_EXTENSIONS = {'.py', '.js', '.html', '.css', '.txt', '.json', '.md', '.php', '.yml', '.yaml', '.docx', '.pdf'}
32
+ MAX_FILE_SIZE_BYTES = 200 * 1024 # 200 KB لكل ملف عند القراءة لضم السياق
33
+ MAX_TOTAL_FILES = 500 # عدد الملفات الأقصى
34
+ MAX_EXTRACTED_BYTES = 20 * 1024 * 1024 # 20 MB إجمالي
 
 
 
 
 
 
 
 
 
 
 
35
  MODEL_ID = os.environ.get('HF_MODEL_ID', 'moonshotai/Kimi-K2-Instruct')
36
  HF_TOKEN = os.environ.get('HF_TOKEN')
37
 
38
  if not HF_TOKEN:
39
+ print("⚠️ تحذير: لم يتم ضبط HF_TOKEN. لتشغيل نموذج Hugging Face ضع متغير البيئة HF_TOKEN.")
40
+
41
+ # --------------------------- أدوات مساعدة ---------------------------
42
 
43
+ def safe_extract(zip_obj: pyzipper.AESZipFile, dest: str) -> Tuple[bool, str]:
44
+ total_bytes = 0
45
+ try:
46
+ for info in zip_obj.infolist():
47
+ name = info.filename
48
+ if name.startswith('__MACOSX') or name.strip() == '':
49
+ continue
50
+ if os.path.isabs(name):
51
+ return False, f"ملف غير صالح داخل الأرشيف: {name}"
52
+ dest_path = os.path.normpath(os.path.join(dest, name))
53
+ if not dest_path.startswith(os.path.normpath(dest) + os.sep):
54
+ return False, f"محاولة اختراق المسار داخل ZIP: {name}"
55
+ total_bytes += info.file_size
56
+ if total_bytes > MAX_EXTRACTED_BYTES:
57
+ return False, "حجم الملفات داخل ZIP يتجاوز الحد المسموح (20MB)"
58
+ zip_obj.extract(info, path=dest)
59
+ return True, "تم الاستخراج الآمن"
60
+ except Exception as e:
61
+ return False, str(e)
62
+
63
+ def read_allowed_files(base_dir: str) -> Dict[str, str]:
64
+ files = {}
65
+ count = 0
66
+ for path in Path(base_dir).rglob('*'):
67
+ if path.is_file():
68
+ rel = str(path.relative_to(base_dir))
69
+ ext = path.suffix.lower()
70
+ if ext not in ALLOWED_EXTENSIONS:
71
+ continue
72
+ count += 1
73
+ if count > MAX_TOTAL_FILES:
74
+ break
75
+ try:
76
+ size = path.stat().st_size
77
+ if ext == '.docx':
78
+ try:
79
+ doc = Document(path)
80
+ content = "\n".join([p.text for p in doc.paragraphs])
81
+ except Exception:
82
+ content = "/* خطأ في قراءة DOCX */"
83
+ elif ext == '.pdf':
84
+ try:
85
+ with open(path, 'rb') as f:
86
+ reader = PyPDF2.PdfReader(f)
87
+ content = "\n".join([page.extract_text() or "" for page in reader.pages])
88
+ except Exception:
89
+ content = "/* خطأ في قراءة PDF */"
90
+ else:
91
+ content = path.read_text(encoding='utf-8', errors='ignore')
92
+ if size > MAX_FILE_SIZE_BYTES:
93
+ head = content[:MAX_FILE_SIZE_BYTES//2]
94
+ tail = content[-MAX_FILE_SIZE_BYTES//2:]
95
+ content = f"/* الملف كبير جدًا ({size} بايت). عرض مقتطفات */\n{head}\n...\n{tail}"
96
+ files[rel] = content
97
+ except Exception:
98
+ files[rel] = "/* خطأ في قراءة الملف */"
99
+ return files
100
+
101
+ # --------------------------- Sandbox ---------------------------
102
  class Sandbox:
103
  def __init__(self):
104
  self.dir: Optional[str] = None
105
  self.files_content: Dict[str, str] = {}
106
+ self.original_zip_path: Optional[str] = None
107
+ self.password: Optional[str] = None
108
 
109
  def cleanup(self):
110
  if self.dir and os.path.isdir(self.dir):
111
+ try:
112
+ shutil.rmtree(self.dir)
113
+ except Exception:
114
+ pass
115
  self.dir = None
116
  self.files_content = {}
117
+ self.original_zip_path = None
118
+ self.password = None
119
 
120
+ def reset(self, zip_path: str, password: Optional[str] = None) -> Tuple[Optional[str], str]:
 
 
 
 
121
  self.cleanup()
122
+ if not zip_path:
123
+ return None, "⚠️ يرجى رفع ملف ZIP أولاً"
124
  self.dir = tempfile.mkdtemp(prefix='sandbox_')
125
+ self.files_content = {}
126
+ self.password = password
127
  try:
128
+ with pyzipper.AESZipFile(zip_path, 'r') as zf:
129
+ if password:
130
+ zf.setpassword(password.encode('utf-8'))
131
+ ok, msg = safe_extract(zf, self.dir)
132
+ if not ok:
133
+ self.cleanup()
134
+ return None, f"❌ خطأ أثناء الاستخراج: {msg}"
135
+ self.files_content = read_allowed_files(self.dir)
136
+ self.original_zip_path = zip_path
137
+ return zip_path, f"✅ تم استخراج {len(self.files_content)} ملف/ملفات"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  except Exception as e:
139
+ tb = traceback.format_exc()
140
  self.cleanup()
141
+ return None, f"❌ استثناء: {str(e)}\n{tb}"
142
 
143
  def get_context(self) -> str:
144
+ parts: List[str] = []
145
+ for name, content in self.files_content.items():
146
+ parts.append(f"--- FILE: {name} ---\n{content}")
147
+ return "\n".join(parts)
148
 
149
  def update_file(self, name: str, content: str) -> None:
150
  if not self.dir:
 
159
  def create_zip(self) -> Tuple[Optional[str], str]:
160
  if not self.dir:
161
  return None, "❌ لا توجد ملفات لحزمها"
162
+ out_fd, out_path = tempfile.mkstemp(prefix='modified_', suffix='.zip')
163
+ os.close(out_fd)
164
+ base_name = out_path[:-4]
165
+ try:
166
+ shutil.make_archive(base_name, 'zip', self.dir)
167
+ return base_name + '.zip', "✅ تم إنشاء ZIP المعدل"
168
+ except Exception as e:
169
+ return None, f"❌ خطأ في إنشاء ZIP: {e}"
170
+
171
+ # --------------------------- نموذج Hugging Face ---------------------------
172
+ client = None
173
+ if HF_TOKEN:
174
+ client = InferenceClient(api_key=HF_TOKEN)
175
+
176
+ def build_messages(user_prompt: str, sandbox_ctx: str) -> List[Dict[str, str]]:
177
+ system = (
178
+ "أنت مساعد برمجي خبير. سأزودك بمشروع (عدة ملفات).\n"
179
+ "عند طلب تعديل، قم بإرجاع التعديلات فقط في كتل واضحة وبالتنسيق التالي:\n"
180
+ "[ملف: relative/path/to/file.ext]```<language>\n<محتوى الملف الجديد>\n```\n"
181
+ "لا تكتب شروحات أو حوار داخل الكتل.")
182
+ user = f"المشروع الحالي:\n{sandbox_ctx}\n\nطلب المستخدم: {user_prompt}"
183
+ return [{"role": "system", "content": system}, {"role": "user", "content": user}]
184
+
185
+ def stream_chat_completion(model_id: str, messages: List[Dict[str, str]], max_tokens: int = 2048):
186
+ if client is None:
187
+ yield {"content": "❗️ نموذج Hugging Face غير مهيأ. ضع HF_TOKEN لتفعيل التكامل."}
188
+ return
189
+ try:
190
+ for chunk in client.chat_completion(model=model_id, messages=messages, max_tokens=max_tokens, stream=True):
191
+ try:
192
+ token = chunk.choices[0].delta.content
193
+ except Exception:
194
+ token = None
195
+ if token:
196
+ yield {"content": token}
197
+ except Exception as e:
198
+ yield {"content": f"❌ خطأ عند الاتصال بالنموذج: {str(e)}"}
199
 
200
+ CHANGE_BLOCK_RE = re.compile(
201
+ r"\[\s*(?:ملف|file)\s*:\s*(?P<name>[^\]]+?)\s*\]\s*```(?:[\w\-+]+)?\n(?P<code>.*?)\n```",
202
+ re.IGNORECASE | re.DOTALL,
203
+ )
204
+
205
+ def extract_changes_from_text(text: str) -> List[Tuple[str, str]]:
206
+ matches = CHANGE_BLOCK_RE.finditer(text)
207
+ return [(m.group('name').strip(), m.group('code')) for m in matches]
208
+
209
+ # --------------------------- واجهة Gradio ---------------------------
210
  sandbox = Sandbox()
211
 
212
+ def api_reset(zip_file, password):
213
+ if zip_file is None:
214
+ return None, "⚠️ ارفع ملف ZIP أولاً"
215
+ zip_path = zip_file.name if hasattr(zip_file, 'name') else zip_file
216
+ return sandbox.reset(zip_path, password)
217
+
218
+ def api_send_message(user_message: str, chat_history: List[dict]):
219
+ chat_history = chat_history + [("user", user_message)]
220
+ if not sandbox.files_content:
221
+ # لا توجد ملفات بعد، نرسل تأكيد للطلب فقط
222
+ chat_history = chat_history + [("assistant", f"✅ تم استلام طلبك: \"{user_message}\".\nيمكنك رفع الملفات لاحقًا لتطبيق التعديلات الفعلية.")]
223
+ return chat_history
224
+ # إذا كانت هناك ملفات، نستدعي النموذج
225
+ sandbox_ctx = sandbox.get_context()
226
+ messages = build_messages(user_message, sandbox_ctx)
227
+ full_response = ""
228
+ for chunk in stream_chat_completion(MODEL_ID, messages):
229
+ token = chunk.get('content', '')
230
+ full_response += token
231
+ if chat_history and chat_history[-1][0] == 'assistant':
232
+ chat_history[-1] = ('assistant', full_response)
233
+ else:
234
+ chat_history = chat_history + [('assistant', full_response)]
235
+ return chat_history
236
+
237
+ def api_apply_and_package(chat_history: List[tuple]):
238
+ last_assistant = None
239
+ for role, content in reversed(chat_history):
240
+ if role == 'assistant' and content and content.strip():
241
+ last_assistant = content
242
+ break
243
+ if not last_assistant:
244
+ return None, "⚠️ لا توجد تعديلات صالحة في آخر رد للمساعد"
245
+ changes = extract_changes_from_text(last_assistant)
246
+ if not changes:
247
+ return None, "⚠️ لم يتم العثور على كتل تعديل بصيغة [ملف: ...]```...```"
248
+ applied = 0
249
+ for name, code in changes:
250
+ try:
251
+ sandbox.update_file(name, code)
252
+ applied += 1
253
+ except Exception as e:
254
+ print(f"فشل تطبيق {name}: {e}")
255
+ out_zip, msg = sandbox.create_zip()
256
+ if not out_zip:
257
+ return None, msg
258
+ return out_zip, f"✅ تم تطبيق {applied} ملفات. {msg}"
259
+
260
+ # --------------------------- بناء الواجهة ---------------------------
261
+ with gr.Blocks(title="Smart Dev Sandbox — المبرمج الذكي PRO") as demo:
262
+ gr.Markdown("# 🚀 المبرمج الذكي PRO — Sandbox احترافي لتعديل مشاريع ZIP/DOCX/PDF")
263
  with gr.Row():
264
  with gr.Column(scale=3):
265
+ chatbot = gr.Chatbot(label="محادثة المساعد", elem_id="chatbot")
266
+ msg = gr.Textbox(placeholder="اطلب تعديلًا (مثال: عدل style.css لتغيّر الخلفية إلى #0f172a)", label="طلب التعديل")
267
  send_btn = gr.Button("إرسال")
268
  with gr.Column(scale=1):
269
+ zip_input = gr.File(label="1) اختر ملف المشروع (ZIP)")
270
+ pwd_input = gr.Textbox(label="كلمة مرور (اختياري)", type="password")
271
+ analyze = gr.Button("🔍 2) تحليل الملفات")
272
+ status = gr.Markdown("**الحالة:** جاهز")
273
  gr.HTML("<hr>")
274
+ save_btn = gr.Button("✨ 3) تطبيق التعديلات وإنشاء ZIP")
275
+ download_output = gr.File(label="4) حمل الملف المعدل")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
+ analyze.click(api_reset, inputs=[zip_input, pwd_input], outputs=[zip_input, status])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  send_btn.click(api_send_message, inputs=[msg, chatbot], outputs=[chatbot])
279
+ save_btn.click(api_apply_and_package, inputs=[chatbot], outputs=[download_output, status])
280
 
281
  if __name__ == "__main__":
282
  demo.launch()