mmotahari commited on
Commit
d2ef3bc
·
verified ·
1 Parent(s): a06687e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +167 -317
app.py CHANGED
@@ -1,13 +1,10 @@
1
  import gradio as gr
2
  import requests
3
- import json
4
- import time
5
 
6
- # ─── OpenRouter API ───────────────────────────────────────────────────────────
7
  OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
8
- FREE_MODEL = "meta-llama/llama-3.3-8b-instruct:free" # رایگان روی OpenRouter
9
 
10
- def call_llm(api_key: str, messages: list, max_tokens: int = 800) -> str:
11
  headers = {
12
  "Authorization": f"Bearer {api_key}",
13
  "Content-Type": "application/json",
@@ -17,7 +14,7 @@ def call_llm(api_key: str, messages: list, max_tokens: int = 800) -> str:
17
  payload = {
18
  "model": FREE_MODEL,
19
  "messages": messages,
20
- "max_tokens": max_tokens,
21
  "temperature": 0.7,
22
  }
23
  resp = requests.post(OPENROUTER_API_URL, headers=headers, json=payload, timeout=60)
@@ -25,345 +22,221 @@ def call_llm(api_key: str, messages: list, max_tokens: int = 800) -> str:
25
  return resp.json()["choices"][0]["message"]["content"].strip()
26
 
27
 
28
- # ─── MultiStep Planning Logic ─────────────────────────────────────────────────
29
-
30
- STEP_PROMPTS = {
31
- "decompose": """You are a task planning assistant.
32
- Given the user's goal, break it down into 4-6 clear sub-tasks.
33
- Output ONLY a numbered list of sub-tasks, nothing else.
34
- Goal: {goal}""",
35
-
36
- "analyze": """You are an expert analyst.
37
- For the following sub-tasks, identify dependencies, risks, and required resources.
38
- Sub-tasks:
39
- {subtasks}
40
-
41
- Output a concise analysis for each sub-task in this format:
42
- 1. [sub-task]: dependency → X, risk → Y, resource → Z""",
43
-
44
- "prioritize": """You are a project manager.
45
- Given these sub-tasks and their analysis, create an optimized execution order with time estimates.
46
- Sub-tasks and analysis:
47
- {analysis}
48
-
49
- Output an ordered execution plan with time estimates (e.g., Day 1, Hour 2, etc.)""",
50
-
51
- "action_plan": """You are an execution specialist.
52
- Convert this prioritized plan into concrete, actionable steps with clear success criteria.
53
- Plan:
54
- {prioritized}
55
-
56
- Output a final action plan with:
57
- - Step number and name
58
- - Concrete actions (2-3 bullet points)
59
- - Success criteria""",
60
-
61
- "summary": """You are a strategic advisor.
62
- Given this complete plan, write a brief executive summary (3-4 sentences) highlighting:
63
- - Main goal achievement path
64
- - Key milestones
65
- - Potential challenges and how the plan addresses them
66
-
67
- Full plan:
68
- {action_plan}"""
69
- }
70
-
71
- STEP_NAMES = {
72
- "decompose": "🔍 مرحله ۱: تجزیه هدف به زیروظایف",
73
- "analyze": "🧠 مرحله ۲: تحلیل وابستگی‌ها و ریسک‌ها",
74
- "prioritize": "📊 مرحله ۳: اولویت‌بندی و زمان‌بندی",
75
- "action_plan": "⚙️ مرحله ۴: تبدیل به اقدامات عملی",
76
- "summary": "✅ مرحله ۵: خلاصه اجرایی نهایی",
77
- }
78
-
79
-
80
- def run_planner(goal: str, api_key: str):
81
- """Generator that yields (step_name, content, all_outputs) progressively."""
 
 
 
 
 
 
 
 
 
 
 
82
  if not api_key.strip():
83
- yield "❌ خطا", "لطفاً API Key از OpenRouter وارد کنید.", {}
84
  return
85
  if not goal.strip():
86
- yield "❌ خطا", "لطفاً هدف خود را وارد کنید.", {}
87
  return
88
 
89
- outputs = {}
90
- steps = ["decompose", "analyze", "prioritize", "action_plan", "summary"]
91
-
92
  context = {"goal": goal}
93
 
94
- for i, step in enumerate(steps):
95
- step_display = STEP_NAMES[step]
96
- yield step_display, "⏳ در حال پردازش...", outputs
97
 
98
  try:
99
- prompt = STEP_PROMPTS[step].format(**context)
100
- messages = [{"role": "user", "content": prompt}]
101
- result = call_llm(api_key, messages)
102
-
103
- outputs[step_display] = result
104
- context[step] = result # feed into next step
105
-
106
- # update context keys for next prompts
107
- if step == "decompose":
108
- context["subtasks"] = result
109
- elif step == "analyze":
110
- context["analysis"] = result
111
- elif step == "prioritize":
112
- context["prioritized"] = result
113
- elif step == "action_plan":
114
- context["action_plan"] = result
115
-
116
- yield step_display, result, outputs
117
-
118
- except requests.exceptions.HTTPError as e:
119
- err = f"❌ خطای API: {e.response.status_code} - {e.response.text}"
120
- yield step_display, err, outputs
121
- return
122
  except Exception as e:
123
- yield step_display, f"❌ خطا: {str(e)}", outputs
124
- return
125
 
126
- yield "✅ تمام مراحل کامل شد!", "برنامه کامل شما آماده است.", outputs
 
127
 
 
 
 
 
128
 
129
- # ─── Gradio UI ─────────────────────────────────────────────────────────────────
130
 
131
  CSS = """
132
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;600;800&family=JetBrains+Mono:wght@400;600&display=swap');
133
-
134
- :root {
135
- --bg: #0a0e1a;
136
- --surface: #111827;
137
- --surface2: #1a2235;
138
- --border: #1e2d45;
139
- --accent: #00d4ff;
140
- --accent2: #7c3aed;
141
- --accent3: #10b981;
142
- --text: #e2e8f0;
143
- --text-muted: #64748b;
144
- --glow: 0 0 20px rgba(0, 212, 255, 0.15);
145
- }
146
 
147
  * { box-sizing: border-box; }
148
 
149
  body, .gradio-container {
150
- background: var(--bg) !important;
151
  font-family: 'Vazirmatn', sans-serif !important;
152
- color: var(--text) !important;
 
153
  }
154
 
155
- .gradio-container {
156
- max-width: 1000px !important;
157
- margin: 0 auto !important;
158
- }
159
 
160
- /* Header */
161
- .app-header {
162
  text-align: center;
163
- padding: 40px 20px 20px;
164
- background: linear-gradient(135deg, var(--surface) 0%, var(--bg) 100%);
165
- border-bottom: 1px solid var(--border);
166
- margin-bottom: 30px;
167
- border-radius: 16px;
168
- }
169
-
170
- .app-title {
171
- font-size: 2.2rem;
172
- font-weight: 800;
173
- background: linear-gradient(135deg, var(--accent), var(--accent2));
174
- -webkit-background-clip: text;
175
- -webkit-text-fill-color: transparent;
176
- margin: 0 0 8px;
177
- letter-spacing: -1px;
178
  }
179
-
180
- .app-subtitle {
181
- color: var(--text-muted);
182
- font-size: 0.95rem;
183
- font-weight: 300;
184
  }
 
185
 
186
- /* Inputs */
187
- label { color: var(--text) !important; font-weight: 600 !important; }
188
 
189
  textarea, input[type="text"], input[type="password"] {
190
- background: var(--surface2) !important;
191
- border: 1px solid var(--border) !important;
192
- border-radius: 10px !important;
193
- color: var(--text) !important;
194
  font-family: 'Vazirmatn', sans-serif !important;
195
- transition: border-color 0.2s, box-shadow 0.2s !important;
196
  }
197
-
198
  textarea:focus, input:focus {
199
- border-color: var(--accent) !important;
200
- box-shadow: var(--glow) !important;
201
  outline: none !important;
202
  }
203
 
204
- /* Buttons */
205
  button.primary {
206
- background: linear-gradient(135deg, var(--accent2), #4f46e5) !important;
207
- border: none !important;
208
- color: white !important;
209
  font-family: 'Vazirmatn', sans-serif !important;
210
- font-weight: 600 !important;
211
- font-size: 1rem !important;
212
- padding: 12px 30px !important;
213
- border-radius: 10px !important;
214
- cursor: pointer !important;
215
  transition: all 0.2s !important;
216
- box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3) !important;
217
- }
218
-
219
- button.primary:hover {
220
- transform: translateY(-2px) !important;
221
- box-shadow: 0 6px 25px rgba(124, 58, 237, 0.5) !important;
222
  }
 
223
 
224
  /* Step cards */
225
- .step-card {
226
- background: var(--surface);
227
- border: 1px solid var(--border);
228
- border-radius: 12px;
229
- padding: 20px;
230
- margin: 12px 0;
231
- transition: border-color 0.3s;
232
  }
233
-
234
- .step-card.active {
235
- border-color: var(--accent);
236
- box-shadow: var(--glow);
237
  }
238
-
239
- .step-card.done {
240
- border-color: var(--accent3);
241
- box-shadow: 0 0 15px rgba(16, 185, 129, 0.1);
 
 
 
 
242
  }
243
-
244
- /* Status badge */
245
- .status-badge {
246
- display: inline-block;
247
- padding: 4px 12px;
248
- border-radius: 20px;
249
- font-size: 0.8rem;
250
- font-weight: 600;
 
 
 
251
  }
 
 
252
 
253
- /* Accordion / Markdown output */
254
- .gr-accordion {
255
- background: var(--surface2) !important;
256
- border: 1px solid var(--border) !important;
257
- border-radius: 12px !important;
258
- margin: 8px 0 !important;
259
- }
260
 
261
- .gr-accordion > .label-wrap {
262
- background: var(--surface) !important;
263
- border-radius: 12px 12px 0 0 !important;
264
- padding: 12px 16px !important;
265
- color: var(--text) !important;
266
- font-weight: 600 !important;
267
- }
268
 
269
- /* Progress bar */
270
- #progress-bar-container {
271
- width: 100%;
272
- height: 6px;
273
- background: var(--surface2);
274
- border-radius: 3px;
275
- margin: 20px 0;
276
- overflow: hidden;
277
- }
278
-
279
- #progress-bar {
280
- height: 100%;
281
- background: linear-gradient(90deg, var(--accent2), var(--accent));
282
- border-radius: 3px;
283
- transition: width 0.5s ease;
284
- width: 0%;
285
- }
286
-
287
- /* Current step indicator */
288
- #current-step-box {
289
- background: linear-gradient(135deg, var(--surface2), var(--surface));
290
- border: 1px solid var(--accent);
291
  border-radius: 10px;
292
- padding: 14px 20px;
293
- margin: 16px 0;
294
- font-size: 1rem;
295
- font-weight: 600;
296
- color: var(--accent);
297
- box-shadow: var(--glow);
298
- min-height: 50px;
299
  }
300
-
301
- /* Outputs area */
302
- #outputs-area .markdown-text,
303
- #outputs-area p, #outputs-area li {
304
- color: var(--text) !important;
305
- font-family: 'Vazirmatn', sans-serif !important;
306
- }
307
-
308
- #outputs-area h1, #outputs-area h2, #outputs-area h3 {
309
- color: var(--accent) !important;
310
  }
311
-
312
- /* Scrollbar */
313
- ::-webkit-scrollbar { width: 6px; }
314
- ::-webkit-scrollbar-track { background: var(--bg); }
315
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
316
- ::-webkit-scrollbar-thumb:hover { background: var(--accent2); }
317
  """
318
 
319
- def format_all_outputs(outputs: dict) -> str:
320
- if not outputs:
321
- return ""
322
- md = ""
323
- for title, content in outputs.items():
324
- md += f"\n---\n## {title}\n\n{content}\n"
325
- return md
326
-
327
-
328
- def planner_stream(goal, api_key):
329
- """Stream updates to all UI components."""
330
- step_output = ""
331
- all_md = ""
332
- progress = 0
333
-
334
- total_steps = 5
335
-
336
- for current_step, content, all_outputs in run_planner(goal, api_key):
337
- # count done steps
338
- done = len([k for k, v in all_outputs.items()])
339
- progress_pct = int((done / total_steps) * 100)
340
-
341
- progress_html = f"""
342
- <div id="progress-bar-container">
343
- <div id="progress-bar" style="width:{progress_pct}%"></div>
344
- </div>
345
- """
346
- step_html = f'<div id="current-step-box">{current_step}</div>'
347
-
348
- all_md = format_all_outputs(all_outputs)
349
-
350
- yield step_html, progress_html, content, all_md
351
-
352
- # Final state
353
- final_html = '<div id="current-step-box" style="color:#10b981;border-color:#10b981;">✅ تمام مراحل با موفقیت کامل شد!</div>'
354
- progress_html = '<div id="progress-bar-container"><div id="progress-bar" style="width:100%;background:linear-gradient(90deg,#10b981,#00d4ff)"></div></div>'
355
- yield final_html, progress_html, "✅ برنامه‌ریزی کامل شد! نتایج همه مراحل در پایین قابل مشاهده است.", all_md
356
-
357
-
358
- # ─── Build Interface ──────────────────────────────────────────────────────────
359
-
360
  with gr.Blocks(css=CSS, title="MultiStep Task Planner") as demo:
361
  gr.HTML("""
362
- <div class="app-header">
363
- <div class="app-title">🧩 MultiStep Task Planner</div>
364
- <div class="app-subtitle">
365
- برنامه‌ریزی هوشمند با استدلال چندمرحله‌ای &nbsp;|&nbsp; Powered by LangChain + OpenRouter
366
- </div>
367
  </div>
368
  """)
369
 
@@ -371,7 +244,7 @@ with gr.Blocks(css=CSS, title="MultiStep Task Planner") as demo:
371
  with gr.Column(scale=2):
372
  goal_input = gr.Textbox(
373
  label="🎯 هدف شما",
374
- placeholder="مثال: یک استارتاپ SaaS راه‌اندازی کنم در حوزه آموزش آنلاین",
375
  lines=3,
376
  )
377
  with gr.Column(scale=1):
@@ -384,36 +257,13 @@ with gr.Blocks(css=CSS, title="MultiStep Task Planner") as demo:
384
 
385
  run_btn = gr.Button("🚀 شروع برنامه‌ریزی", variant="primary", size="lg")
386
 
387
- # Live progress
388
- progress_display = gr.HTML('<div id="progress-bar-container"><div id="progress-bar" style="width:0%"></div></div>')
389
- step_display = gr.HTML('<div id="current-step-box" style="color:var(--text-muted)">منتظر شروع...</div>')
390
-
391
- # Current step output (live)
392
- with gr.Group():
393
- gr.Markdown("### 📝 خروجی مرحله جاری")
394
- live_output = gr.Markdown(
395
- value="",
396
- elem_id="live-output"
397
- )
398
 
399
- # All outputs accumulated
400
- with gr.Accordion("📋 تمام مراحل (نتایج کامل)", open=False, elem_id="outputs-area"):
401
- all_outputs_display = gr.Markdown(value="")
402
-
403
- # Wire up
404
  run_btn.click(
405
- fn=planner_stream,
406
  inputs=[goal_input, api_key_input],
407
- outputs=[step_display, progress_display, live_output, all_outputs_display],
408
  )
409
 
410
- gr.HTML("""
411
- <div style="text-align:center;padding:20px;color:#475569;font-size:0.8rem;margin-top:20px;">
412
- مدل رایگان: <code style="color:#00d4ff">meta-llama/llama-3.3-8b-instruct:free</code> از OpenRouter
413
- &nbsp;|&nbsp;
414
- برای API Key رایگان: <a href="https://openrouter.ai" target="_blank" style="color:#7c3aed">openrouter.ai</a>
415
- </div>
416
- """)
417
-
418
  if __name__ == "__main__":
419
  demo.launch()
 
1
  import gradio as gr
2
  import requests
 
 
3
 
 
4
  OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
5
+ FREE_MODEL = "meta-llama/llama-3.3-8b-instruct:free"
6
 
7
+ def call_llm(api_key, messages):
8
  headers = {
9
  "Authorization": f"Bearer {api_key}",
10
  "Content-Type": "application/json",
 
14
  payload = {
15
  "model": FREE_MODEL,
16
  "messages": messages,
17
+ "max_tokens": 1000,
18
  "temperature": 0.7,
19
  }
20
  resp = requests.post(OPENROUTER_API_URL, headers=headers, json=payload, timeout=60)
 
22
  return resp.json()["choices"][0]["message"]["content"].strip()
23
 
24
 
25
+ STEPS = [
26
+ {
27
+ "id": "decompose",
28
+ "title": "مرحله ۱ — تجزیه هدف به زیروظایف",
29
+ "prompt": "You are a task planning assistant. Given the user's goal, break it down into 5 clear sub-tasks. Output ONLY a numbered list, nothing else.\n\nGoal: {goal}"
30
+ },
31
+ {
32
+ "id": "analyze",
33
+ "title": "مرحله ۲ — تحلیل وابستگی‌ها و ریسک‌ها",
34
+ "prompt": "For each sub-task below, identify: dependency, risk, required resource. Be concise.\n\nSub-tasks:\n{decompose}"
35
+ },
36
+ {
37
+ "id": "prioritize",
38
+ "title": "مرحله ۳ — اولویت‌بندی و زمان‌بندی",
39
+ "prompt": "Given these sub-tasks and analysis, create an optimized execution order with time estimates.\n\nAnalysis:\n{analyze}"
40
+ },
41
+ {
42
+ "id": "actions",
43
+ "title": "مرحله ۴ — اقدامات عملی",
44
+ "prompt": "Convert this plan into concrete actionable steps with clear success criteria for each.\n\nPlan:\n{prioritize}"
45
+ },
46
+ {
47
+ "id": "summary",
48
+ "title": "مرحله ۵ — خلاصه اجرایی نهایی",
49
+ "prompt": "Write a brief executive summary (3-4 sentences) of the complete plan below.\n\nFull plan:\n{actions}"
50
+ },
51
+ ]
52
+
53
+ def make_steps_html(done_steps, active_idx, results):
54
+ html = ""
55
+ for i, step in enumerate(STEPS):
56
+ if i < active_idx:
57
+ # done
58
+ result_text = results.get(step["id"], "")
59
+ html += f"""
60
+ <div class="step done">
61
+ <div class="step-header">
62
+ <span class="step-icon">✅</span>
63
+ <span class="step-title">{step['title']}</span>
64
+ </div>
65
+ <div class="step-body">{result_text.replace(chr(10), '<br>')}</div>
66
+ </div>"""
67
+ elif i == active_idx:
68
+ # active
69
+ html += f"""
70
+ <div class="step active">
71
+ <div class="step-header">
72
+ <span class="step-icon spin">⏳</span>
73
+ <span class="step-title">{step['title']}</span>
74
+ </div>
75
+ <div class="step-body muted">در حال پردازش...</div>
76
+ </div>"""
77
+ else:
78
+ # pending
79
+ html += f"""
80
+ <div class="step pending">
81
+ <div class="step-header">
82
+ <span class="step-icon">⬜</span>
83
+ <span class="step-title muted-title">{step['title']}</span>
84
+ </div>
85
+ </div>"""
86
+ return html
87
+
88
+
89
+ def run_planner(goal, api_key):
90
  if not api_key.strip():
91
+ yield "<div class='err'>❌ API Key وارد کنید</div>"
92
  return
93
  if not goal.strip():
94
+ yield "<div class='err'>❌ هدف را وارد کنید</div>"
95
  return
96
 
97
+ results = {}
 
 
98
  context = {"goal": goal}
99
 
100
+ for i, step in enumerate(STEPS):
101
+ # show current step as active
102
+ yield make_steps_html(results, i, results)
103
 
104
  try:
105
+ prompt = step["prompt"].format(**context)
106
+ result = call_llm(api_key, [{"role": "user", "content": prompt}])
107
+ results[step["id"]] = result
108
+ context[step["id"]] = result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  except Exception as e:
110
+ results[step["id"]] = f"❌ خطا: {str(e)}"
111
+ context[step["id"]] = results[step["id"]]
112
 
113
+ # show this step as done
114
+ yield make_steps_html(results, i + 1, results)
115
 
116
+ # all done
117
+ final = make_steps_html(results, len(STEPS), results)
118
+ final += "<div class='done-banner'>✅ برنامه‌ریزی کامل شد!</div>"
119
+ yield final
120
 
 
121
 
122
  CSS = """
123
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;600;800&display=swap');
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  * { box-sizing: border-box; }
126
 
127
  body, .gradio-container {
128
+ background: #0d1117 !important;
129
  font-family: 'Vazirmatn', sans-serif !important;
130
+ color: #e6edf3 !important;
131
+ direction: rtl;
132
  }
133
 
134
+ .gradio-container { max-width: 860px !important; margin: 0 auto !important; }
 
 
 
135
 
136
+ .header {
 
137
  text-align: center;
138
+ padding: 36px 20px 16px;
139
+ border-bottom: 1px solid #21262d;
140
+ margin-bottom: 24px;
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
142
+ .header h1 {
143
+ font-size: 2rem; font-weight: 800;
144
+ background: linear-gradient(135deg, #58a6ff, #bc8cff);
145
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
146
+ margin: 0 0 6px;
147
  }
148
+ .header p { color: #8b949e; font-size: 0.9rem; margin: 0; }
149
 
150
+ label { color: #e6edf3 !important; font-weight: 600 !important; font-size: 0.95rem !important; }
 
151
 
152
  textarea, input[type="text"], input[type="password"] {
153
+ background: #161b22 !important;
154
+ border: 1px solid #30363d !important;
155
+ border-radius: 8px !important;
156
+ color: #e6edf3 !important;
157
  font-family: 'Vazirmatn', sans-serif !important;
 
158
  }
 
159
  textarea:focus, input:focus {
160
+ border-color: #58a6ff !important;
161
+ box-shadow: 0 0 0 3px rgba(88,166,255,0.1) !important;
162
  outline: none !important;
163
  }
164
 
 
165
  button.primary {
166
+ background: linear-gradient(135deg, #7c3aed, #4f46e5) !important;
167
+ border: none !important; color: white !important;
 
168
  font-family: 'Vazirmatn', sans-serif !important;
169
+ font-weight: 700 !important; font-size: 1.05rem !important;
170
+ padding: 12px !important; border-radius: 8px !important;
 
 
 
171
  transition: all 0.2s !important;
172
+ box-shadow: 0 4px 15px rgba(124,58,237,0.35) !important;
 
 
 
 
 
173
  }
174
+ button.primary:hover { filter: brightness(1.15) !important; transform: translateY(-1px) !important; }
175
 
176
  /* Step cards */
177
+ .step {
178
+ border-radius: 10px;
179
+ margin: 10px 0;
180
+ overflow: hidden;
181
+ border: 1px solid #21262d;
182
+ transition: all 0.3s;
 
183
  }
184
+ .step.active {
185
+ border-color: #58a6ff;
186
+ box-shadow: 0 0 0 3px rgba(88,166,255,0.12);
 
187
  }
188
+ .step.done { border-color: #238636; }
189
+ .step.pending { opacity: 0.5; }
190
+
191
+ .step-header {
192
+ display: flex; align-items: center; gap: 10px;
193
+ padding: 12px 16px;
194
+ background: #161b22;
195
+ font-weight: 600; font-size: 0.95rem;
196
  }
197
+ .step.active .step-header { background: #1c2a3a; color: #58a6ff; }
198
+ .step.done .step-header { background: #0f2d16; color: #3fb950; }
199
+
200
+ .step-body {
201
+ padding: 14px 16px;
202
+ font-size: 0.88rem;
203
+ line-height: 1.8;
204
+ background: #0d1117;
205
+ color: #c9d1d9;
206
+ white-space: pre-wrap;
207
+ border-top: 1px solid #21262d;
208
  }
209
+ .step-body.muted { color: #8b949e; font-style: italic; }
210
+ .muted-title { color: #8b949e !important; }
211
 
212
+ .step-icon { font-size: 1.1rem; min-width: 22px; }
 
 
 
 
 
 
213
 
214
+ @keyframes spin { to { transform: rotate(360deg); } }
215
+ .spin { display: inline-block; animation: spin 1.2s linear infinite; }
 
 
 
 
 
216
 
217
+ .done-banner {
218
+ text-align: center;
219
+ margin-top: 20px;
220
+ padding: 16px;
221
+ background: #0f2d16;
222
+ border: 1px solid #238636;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  border-radius: 10px;
224
+ color: #3fb950;
225
+ font-weight: 700;
226
+ font-size: 1.1rem;
 
 
 
 
227
  }
228
+ .err {
229
+ background: #2d1515; border: 1px solid #f85149;
230
+ color: #f85149; padding: 14px; border-radius: 8px;
231
+ font-weight: 600;
 
 
 
 
 
 
232
  }
 
 
 
 
 
 
233
  """
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  with gr.Blocks(css=CSS, title="MultiStep Task Planner") as demo:
236
  gr.HTML("""
237
+ <div class="header">
238
+ <h1>🧩 MultiStep Task Planner</h1>
239
+ <p>برنامه‌ریزی هوشمند با استدلال چندمرحله‌ای</p>
 
 
240
  </div>
241
  """)
242
 
 
244
  with gr.Column(scale=2):
245
  goal_input = gr.Textbox(
246
  label="🎯 هدف شما",
247
+ placeholder="مثال: برنامه سفر ۳ روزه به ماسال",
248
  lines=3,
249
  )
250
  with gr.Column(scale=1):
 
257
 
258
  run_btn = gr.Button("🚀 شروع برنامه‌ریزی", variant="primary", size="lg")
259
 
260
+ output_html = gr.HTML(value="")
 
 
 
 
 
 
 
 
 
 
261
 
 
 
 
 
 
262
  run_btn.click(
263
+ fn=run_planner,
264
  inputs=[goal_input, api_key_input],
265
+ outputs=[output_html],
266
  )
267
 
 
 
 
 
 
 
 
 
268
  if __name__ == "__main__":
269
  demo.launch()