TaskPlanner / app.py
mmotahari's picture
Update app.py
75498da verified
Raw History Blame Contribute Delete
14.6 kB
import gradio as gr
HTML_APP = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;600;800&display=swap');
#planner-root {
font-family: 'Vazirmatn', sans-serif;
direction: rtl;
background: #f5f7fa;
color: #1a202c;
padding: 32px 20px 60px;
min-height: 100vh;
}
#planner-root h1 {
text-align: center; font-size: 2rem; font-weight: 800;
background: linear-gradient(135deg, #4f46e5, #7c3aed);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
margin: 0 0 6px;
}
.sub { text-align: center; color: #718096; font-size: 0.88rem; margin-bottom: 24px; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 10px; }
@media(max-width:560px){ .row { grid-template-columns: 1fr; } }
.field label { display: block; font-size: 0.85rem; font-weight: 600; color: #4a5568; margin-bottom: 5px; }
.field textarea, .field input, .field select {
width: 100%; background: #fff;
border: 1px solid #cbd5e0; border-radius: 8px;
color: #1a202c; font-family: 'Vazirmatn', sans-serif;
font-size: 0.9rem; padding: 9px 12px;
transition: border-color .2s, box-shadow .2s; resize: vertical;
}
.field textarea { min-height: 75px; }
.field input[type=password], .field select { height: 42px; }
.field textarea:focus, .field input:focus, .field select:focus {
outline: none; border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102,126,234,.15);
}
.field select { cursor: pointer; }
.field .hint { font-size: .75rem; color: #a0aec0; margin-top: 4px; }
#startBtn {
width: 100%; padding: 12px;
background: linear-gradient(135deg, #667eea, #764ba2);
border: none; border-radius: 8px; color: #fff;
font-family: 'Vazirmatn', sans-serif; font-weight: 700; font-size: 1rem;
cursor: pointer; margin-top: 10px; margin-bottom: 22px;
box-shadow: 0 4px 18px rgba(102,126,234,.3); transition: all .2s;
}
#startBtn:hover:not(:disabled) { filter: brightness(1.08); transform: translateY(-1px); }
#startBtn:disabled { opacity: .5; cursor: not-allowed; transform: none; }
.step {
border-radius: 12px; margin: 9px 0;
border: 1px solid #e2e8f0; background: #fff;
overflow: hidden; transition: border-color .3s, box-shadow .3s;
}
.step.s-pending { opacity: .38; }
.step.s-active { border-color: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,.1); }
.step.s-done { border-color: #68d391; }
.step.s-error { border-color: #fc8181; }
.step-hd {
display: flex; align-items: center; gap: 10px;
padding: 12px 16px; font-weight: 700; font-size: .9rem;
background: #f7fafc; color: #2d3748; border-bottom: 1px solid #e2e8f0;
}
.step.s-active .step-hd { background: #ebf4ff; color: #3c5fb8; border-color: #bee3f8; }
.step.s-done .step-hd { background: #f0fff4; color: #276749; border-color: #c6f6d5; }
.step.s-error .step-hd { background: #fff5f5; color: #c53030; border-color: #fed7d7; }
.step-hd .badge {
margin-right: auto; font-size: .72rem; font-weight: 600;
padding: 2px 9px; border-radius: 20px;
background: rgba(0,0,0,.06); color: inherit; white-space: nowrap;
}
.step-body {
max-height: 0; overflow: hidden;
transition: max-height .35s ease;
}
.step.s-active .step-body,
.step.s-done .step-body,
.step.s-error .step-body { max-height: 700px; }
.item {
display: flex; gap: 10px; align-items: flex-start;
padding: 8px 14px; border-bottom: 1px solid #f0f4f8;
font-size: .87rem; line-height: 1.7; color: #2d3748;
}
.item:last-child { border-bottom: none; }
.item .num {
min-width: 22px; height: 22px; border-radius: 50%;
background: #667eea; color: #fff;
font-size: .72rem; font-weight: 700;
display: flex; align-items: center; justify-content: center; margin-top: 2px; flex-shrink: 0;
}
.step.s-done .item .num { background: #48bb78; }
.step.s-error .item .num { background: #fc8181; }
.thinking {
padding: 10px 16px; color: #a0aec0;
font-style: italic; font-size: .87rem;
}
@keyframes spin { to { transform: rotate(360deg); } }
.spin-icon { display: inline-block; animation: spin 1s linear infinite; }
#doneBanner {
text-align: center; margin-top: 16px; padding: 14px;
background: #f0fff4; border: 1px solid #68d391;
border-radius: 10px; color: #276749;
font-weight: 700; font-size: .97rem; display: none;
}
#errBox {
background: #fff5f5; border: 1px solid #fc8181;
color: #c53030; padding: 11px 14px; border-radius: 8px;
font-weight: 600; margin-bottom: 12px; display: none; font-size: .88rem;
}
#modelStatus {
font-size: .75rem; color: #a0aec0; margin-top: 3px;
}
</style>
<div id="planner-root">
<h1>🧩 MultiStep Task Planner</h1>
<p class="sub">برنامه‌ریزی هوشمند با استدلال چندمرحله‌ای</p>
<div class="row">
<div class="field">
<label>🎯 هدف شما</label>
<textarea id="goal" placeholder="مثال: برنامه سفر ۳ روزه به ماسال"></textarea>
</div>
<div class="field">
<label>🔑 OpenRouter API Key</label>
<input type="password" id="apikey" placeholder="sk-or-v1-..." oninput="onKeyChange()">
<div class="hint">برای دریافت کلید رایگان: openrouter.ai</div>
</div>
</div>
<div class="field" style="margin-bottom:0">
<label>🤖 مدل هوش مصنوعی</label>
<select id="modelSelect">
<option value="">— ابتدا API Key وارد کنید —</option>
</select>
<div id="modelStatus">وارد کردن API Key مدل‌های رایگان را بارگذاری می‌کند</div>
</div>
<button id="startBtn" onclick="startPlanning()">🚀 شروع برنامه‌ریزی</button>
<div id="errBox"></div>
<div id="stepsArea"></div>
<div id="doneBanner">✅ برنامه‌ریزی کامل شد!</div>
</div>
<script>
// Known fallback free models (always valid)
const FALLBACK_MODELS = [
{ id: "deepseek/deepseek-r1:free", name: "DeepSeek R1 (رایگان)" },
{ id: "deepseek/deepseek-v3-base:free", name: "DeepSeek V3 (رایگان)" },
{ id: "nvidia/nemotron-3-ultra:free", name: "NVIDIA Nemotron 3 Ultra (رایگان)" },
{ id: "openrouter/owl-alpha:free", name: "OpenRouter Owl Alpha (رایگان)" },
{ id: "openai/gpt-oss-20b:free", name: "GPT-OSS 20B (رایگان)" },
];
let keyTimeout = null;
function onKeyChange() {
clearTimeout(keyTimeout);
keyTimeout = setTimeout(loadModels, 800);
}
async function loadModels() {
const apiKey = document.getElementById("apikey").value.trim();
const sel = document.getElementById("modelSelect");
const status = document.getElementById("modelStatus");
if (!apiKey) return;
status.textContent = "⏳ در حال بارگذاری مدل‌های رایگان...";
sel.innerHTML = '<option value="">در حال بارگذاری...</option>';
try {
const r = await fetch("https://openrouter.ai/api/v1/models", {
headers: { "Authorization": "Bearer " + apiKey }
});
if (!r.ok) throw new Error("خطا در اتصال");
const data = await r.json();
const freeModels = data.data.filter(m =>
m.id.endsWith(":free") &&
m.pricing &&
parseFloat(m.pricing.prompt) === 0
).slice(0, 20);
sel.innerHTML = "";
freeModels.forEach((m, i) => {
const opt = document.createElement("option");
opt.value = m.id;
opt.textContent = (m.name || m.id) + " (رایگان)";
if (i === 0) opt.selected = true;
sel.appendChild(opt);
});
if (freeModels.length === 0) throw new Error("مدل رایگان پیدا نشد");
status.textContent = "✅ " + freeModels.length + " مدل رایگان بارگذاری شد";
} catch(e) {
// fallback
sel.innerHTML = "";
FALLBACK_MODELS.forEach((m, i) => {
const opt = document.createElement("option");
opt.value = m.id;
opt.textContent = m.name;
if (i === 0) opt.selected = true;
sel.appendChild(opt);
});
status.textContent = "⚠️ لیست پیش‌فرض (بارگذاری خودکار ناموفق بود)";
}
}
const STEPS = [
{
id: "s1", title: "تحلیل هدف", emoji: "🎯", badge: "شناخت",
prompt: (c) =>
`هدف: "${c.goal}"
این هدف را به ۵ گام اصلی تقسیم کن.
فقط لیست شماره‌گذاری‌شده فارسی، هر مورد یک جمله.`
},
{
id: "s2", title: "منابع و ابزارها", emoji: "🛠️", badge: "آمادگی",
prompt: (c) =>
`هدف: "${c.goal}"
گام‌ها: ${c.s1}
چه منابع، ابزارها یا پیش‌نیازهایی لازم است؟
لیست مختصر فارسی.`
},
{
id: "s3", title: "زمان‌بندی", emoji: "📅", badge: "برنامه",
prompt: (c) =>
`هدف: "${c.goal}"
گام‌ها: ${c.s1}
یک زمان‌بندی واقعی برای رسیدن به این هدف بنویس.
فرمت: هفته/ماه X: فعالیت ...`
},
{
id: "s4", title: "برنامه عملیاتی", emoji: "📋", badge: "اجرا",
prompt: (c) =>
`هدف: "${c.goal}"
گام‌ها: ${c.s1} | منابع: ${c.s2} | زمان‌بندی: ${c.s3}
یک برنامه عملی و دقیق برای شروع بنویس.`
},
{
id: "s5", title: "نکات کلیدی", emoji: "✅", badge: "توصیه",
prompt: (c) =>
`هدف: "${c.goal}"
برنامه: ${c.s4}
۵ نکته مهم برای موفقیت در این مسیر بنویس.
لیست شماره‌گذاری‌شده فارسی.`
}
];
function parseLines(text) {
return text.split('\\n')
.map(l => l.replace(/^[\\d]+[\\.\\-\\)]\\.?\\s*/, '').trim())
.filter(l => l.length > 3);
}
function buildSteps() {
const area = document.getElementById("stepsArea");
area.innerHTML = "";
STEPS.forEach((s, i) => {
const d = document.createElement("div");
d.className = "step s-pending"; d.id = "step-" + i;
d.innerHTML = `
<div class="step-hd">
<span>${s.emoji}</span>
<span>مرحله ${i+1} — ${s.title}</span>
<span class="badge">${s.badge}</span>
</div>
<div class="step-body"></div>`;
area.appendChild(d);
});
}
function setStep(idx, state, lines) {
const el = document.getElementById("step-" + idx);
if (!el) return;
el.className = "step s-" + state;
const s = STEPS[idx];
const icons = { pending: s.emoji, active: '<span class="spin-icon">⏳</span>', done: "✅", error: "❌" };
el.querySelector(".step-hd").innerHTML = `
<span>${icons[state]}</span>
<span>مرحله ${idx+1} — ${s.title}</span>
<span class="badge">${s.badge}</span>`;
const body = el.querySelector(".step-body");
if (state === "active") {
body.innerHTML = '<div class="thinking">در حال پردازش...</div>';
} else if (lines && lines.length) {
body.innerHTML = lines.map((l, i) =>
`<div class="item"><span class="num">${state==="error"?"!":i+1}</span><span>${l}</span></div>`
).join("");
}
}
function showErr(msg) {
const b = document.getElementById("errBox");
b.textContent = "❌ " + msg; b.style.display = "block";
}
async function callLLM(apiKey, model, prompt) {
const controller = new AbortController();
const tid = setTimeout(() => controller.abort(), 50000);
try {
const r = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST", signal: controller.signal,
headers: {
"Authorization": "Bearer " + apiKey,
"Content-Type": "application/json",
"HTTP-Referer": "https://huggingface.co/spaces",
"X-Title": "MultiStep Task Planner"
},
body: JSON.stringify({
model: model,
messages: [{ role: "user", content: prompt }],
max_tokens: 500,
temperature: 0.6
})
});
clearTimeout(tid);
if (!r.ok) {
const errText = await r.text();
// If model not found, try next fallback
if (r.status === 404) throw new Error("MODEL_NOT_FOUND:" + errText);
throw new Error(errText.slice(0, 120));
}
const d = await r.json();
return d.choices[0].message.content.trim();
} catch(e) {
clearTimeout(tid);
throw e;
}
}
async function callWithFallback(apiKey, prompt) {
const sel = document.getElementById("modelSelect");
// Try selected model first, then fallbacks
const modelsToTry = [sel.value, ...FALLBACK_MODELS.map(m => m.id)]
.filter((v, i, a) => v && a.indexOf(v) === i); // unique, non-empty
for (const model of modelsToTry) {
try {
const result = await callLLM(apiKey, model, prompt);
// Update selector to show which model worked
if (model !== sel.value) {
document.getElementById("modelStatus").textContent = "✅ استفاده از: " + model;
}
return result;
} catch(e) {
if (e.message.startsWith("MODEL_NOT_FOUND") || e.name === "AbortError") {
continue; // try next model
}
throw e; // real error, stop
}
}
throw new Error("هیچ مدل رایگانی در دسترس نیست. لطفاً بعداً امتحان کنید.");
}
async function startPlanning() {
const goal = document.getElementById("goal").value.trim();
const apiKey = document.getElementById("apikey").value.trim();
const btn = document.getElementById("startBtn");
document.getElementById("errBox").style.display = "none";
document.getElementById("doneBanner").style.display = "none";
if (!goal) { showErr("هدف را وارد کنید"); return; }
if (!apiKey) { showErr("API Key را وارد کنید"); return; }
btn.disabled = true; btn.textContent = "⏳ در حال برنامه‌ریزی...";
buildSteps();
const ctx = { goal };
for (let i = 0; i < STEPS.length; i++) {
setStep(i, "active");
try {
const raw = await callWithFallback(apiKey, STEPS[i].prompt(ctx));
ctx[STEPS[i].id] = raw;
const lines = parseLines(raw);
setStep(i, "done", lines.length > 0 ? lines : [raw]);
} catch(e) {
ctx[STEPS[i].id] = "";
setStep(i, "error", [e.message]);
}
}
document.getElementById("doneBanner").style.display = "block";
btn.disabled = false; btn.textContent = "🔄 برنامه‌ریزی مجدد";
}
</script>
"""
with gr.Blocks(title="MultiStep Task Planner") as demo:
gr.HTML(HTML_APP)
if __name__ == "__main__":
demo.launch()