Compare commits
16 Commits
e79d2d6108
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c759851ce | |||
| 7a7542d77b | |||
| 154e06ef54 | |||
| ffba74a727 | |||
| 8896740895 | |||
| 93f7873dd1 | |||
| 255e8d25e5 | |||
| 29b5f07ebf | |||
| b721f01e14 | |||
| 33efeac98f | |||
| cfff934bdd | |||
| dd882287df | |||
| 5cc9ed976c | |||
| 97bd2a12df | |||
| a55c75b928 | |||
| fcc476e432 |
@@ -40,20 +40,13 @@ jobs:
|
|||||||
- name: Build Container
|
- name: Build Container
|
||||||
run: docker compose build
|
run: docker compose build
|
||||||
|
|
||||||
- name: Free Port 3000
|
|
||||||
run: |
|
|
||||||
# Remove any container publishing :3000 (old Next.js container,
|
|
||||||
# orphans from a previously-named compose project, etc.)
|
|
||||||
OLD=$(docker ps -a --filter publish=3000 -q)
|
|
||||||
if [ -n "$OLD" ]; then
|
|
||||||
echo "Removing containers on :3000 -> $OLD"
|
|
||||||
docker rm -f $OLD || true
|
|
||||||
fi
|
|
||||||
# Belt and suspenders: also remove by our known name.
|
|
||||||
docker rm -f soroushasadi-site 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
run: docker compose up -d --remove-orphans
|
# Compose recreates ONLY our own container (container_name:
|
||||||
|
# soroushasadi-site, project: soroushasadi). It must never touch
|
||||||
|
# other stacks. Do NOT add any step that removes containers by
|
||||||
|
# published port — port 3000 is Gitea and 5xxx/3xxx belong to
|
||||||
|
# other apps on this host.
|
||||||
|
run: docker compose up -d
|
||||||
|
|
||||||
- name: Wait For Health Check
|
- name: Wait For Health Check
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+15
-16
@@ -1,31 +1,30 @@
|
|||||||
@page "/blog"
|
@page "/blog"
|
||||||
@model SoroushAsadi.Pages.Blog.BlogIndexModel
|
@model SoroushAsadi.Pages.Blog.BlogIndexModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = Model.IsFa ? "بلاگ — سروش اسعدی" : "Blog — Soroush Asadi";
|
ViewData["Title"] = Model.IsFa ? "بلاگ - سروش اسعدی" : "Blog - Soroush Asadi";
|
||||||
var fa = Model.IsFa;
|
var fa = Model.IsFa;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="min-h-screen pt-28 pb-20 px-5 sm:px-8">
|
<div class="px-5 pt-28 pb-24 sm:px-8 sm:pt-32">
|
||||||
<div class="mx-auto max-w-7xl">
|
<div class="mx-auto max-w-4xl">
|
||||||
<div class="section-header mb-14">
|
<div class="sec-head">
|
||||||
<div class="eyebrow mb-4"><span class="chip">@(fa ? "بلاگ" : "Journal")</span></div>
|
<h1 class="@(fa ? "font-fa" : "")" style="font-size:clamp(2rem,4vw,2.75rem)">
|
||||||
<h1 class="font-display text-4xl font-extrabold text-white @(fa ? "font-fa" : "")">
|
|
||||||
@(fa ? "یادداشتهای مهندسی" : "Engineering notes")
|
@(fa ? "یادداشتهای مهندسی" : "Engineering notes")
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-4 text-slate-400">@(fa ? "یافتهها از پروژههای واقعی — نه ترجمهی مقاله، نه فهرست hype." : "Findings from real engagements — not translated articles, not hype lists.")</p>
|
<p class="lede mt-4">@(fa ? "درسهایی از پروژههای واقعی. نه ترجمهی مقاله، نه شعار توخالی." : "Findings from real engagements. Not translated articles, not hype lists.")</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div class="border-b border-zinc-200">
|
||||||
@foreach (var post in Model.Posts)
|
@foreach (var post in Model.Posts)
|
||||||
{
|
{
|
||||||
<a href="/blog/@post.Slug" class="group glass block p-6 transition-all duration-300 hover:-translate-y-1 hover:border-electric/40 reveal">
|
<a href="/blog/@post.Slug" class="group reveal grid grid-cols-1 gap-2 border-t border-zinc-200 py-6 sm:grid-cols-[8rem_1fr] sm:gap-8">
|
||||||
<span class="label-mono text-electric mb-3 block">@post.Category</span>
|
<div class="flex items-baseline justify-between sm:flex-col sm:gap-1">
|
||||||
<h2 class="font-display font-semibold leading-snug text-white group-hover:text-electric transition-colors @(fa ? "font-fa" : "")"
|
<span class="kicker">@post.Category</span>
|
||||||
style="font-size:clamp(1rem,1.4vw,1.15rem)">@post.Title</h2>
|
<span class="text-[.78rem] text-zinc-500">@post.ReadTime @(fa ? "دقیقه" : "min")</span>
|
||||||
<p class="mt-3 text-[.88rem] leading-relaxed text-slate-400 line-clamp-3">@post.Excerpt</p>
|
</div>
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div>
|
||||||
<span class="label-mono">@post.ReadTime @(fa ? "دقیقه" : "min") @(fa ? "مطالعه" : "read")</span>
|
<h2 class="text-[1.1rem] font-semibold transition-colors group-hover:text-accent @(fa ? "font-fa" : "")">@post.Title</h2>
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" class="text-electric @(fa ? "rotate-180" : "")"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
<p class="mt-1.5 text-[.9rem] leading-relaxed text-zinc-600">@post.Excerpt</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ public class BlogIndexModel : BasePageModel
|
|||||||
{
|
{
|
||||||
var fa = IsFa;
|
var fa = IsFa;
|
||||||
Posts = fa ? new BlogPost[]{
|
Posts = fa ? new BlogPost[]{
|
||||||
new("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در تولید کار میکند","چرا BLEU و ROUGE برای RAG ناکافیاند، و معیارهایی که در پروژههای واقعی تصمیم میسازند.",8),
|
new("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در عمل جواب میدهد","چرا BLEU و ROUGE برای RAG کافی نیستند، و معیارهایی که واقعاً به تصمیم کمک میکنند.",8),
|
||||||
new("agentic-n8n-patterns","Automation","الگوهای عاملمحور با n8n برای سازمان","چگونه n8n را با LangGraph ترکیب کنیم تا گردشکارهای قابل ممیزی بسازیم.",11),
|
new("agentic-n8n-patterns","Automation","الگوهای عاملمحور با n8n برای سازمان","چطور n8n را با LangGraph ترکیب کنیم تا گردشکارهای خودکار و قابلردیابی بسازیم.",11),
|
||||||
new("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه ضدالگو که در ۸۰٪ پروژههای Vertex میبینم، و چگونه ۶۰٪ هزینه را کاهش دادیم.",6),
|
new("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه اشتباه رایج که در بیشتر پروژههای Vertex میبینم، و اینکه چطور ۶۰٪ هزینه را کم کردیم.",6),
|
||||||
new("k8s-llm-inference","Infra","استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ میلیثانیه","الگوی استقرار با KEDA، GPU sharing، و request hedging برای سرویسدهی پایدار.",14),
|
new("k8s-llm-inference","Infra","اجرای LLM روی Kubernetes با تأخیر زیر ۵۰ میلیثانیه","الگوی استقرار با KEDA، اشتراک GPU و request hedging برای سرویسدهی پایدار.",14),
|
||||||
new("flutter-on-device-ai","Mobile","هوش مصنوعی on-device در Flutter","استفاده از Gemini Nano و LiteRT برای استنتاج آفلاین در اپلیکیشنهای موبایل.",9),
|
new("flutter-on-device-ai","Mobile","هوش مصنوعی روی دستگاه در Flutter","استفاده از Gemini Nano و LiteRT برای پردازش آفلاین در اپهای موبایل.",9),
|
||||||
new("enterprise-ai-roadmap","Strategy","نقشه راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای CTOها میسازم — از کشف موارد کاربری تا اولین استقرار تولید.",7),
|
new("enterprise-ai-roadmap","Strategy","نقشهی راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای مدیران فنی میچینم؛ از پیدا کردن بهترین ایده تا اولین اجرای واقعی.",7),
|
||||||
} : new BlogPost[]{
|
} : new BlogPost[]{
|
||||||
new("rag-eval-framework","LLM","A RAG evaluation framework that holds up in production","Why BLEU and ROUGE fall short for RAG, and the metrics that actually drive decisions in real projects.",8),
|
new("rag-eval-framework","LLM","A RAG evaluation framework that holds up in production","Why BLEU and ROUGE fall short for RAG, and the metrics that actually drive decisions in real projects.",8),
|
||||||
new("agentic-n8n-patterns","Automation","Agentic patterns with n8n for the enterprise","How to combine n8n with LangGraph to build auditable, debuggable autonomous workflows.",11),
|
new("agentic-n8n-patterns","Automation","Agentic patterns with n8n for the enterprise","How to combine n8n with LangGraph to build auditable, debuggable autonomous workflows.",11),
|
||||||
new("vertex-cost-control","Google Stack","Vertex AI cost control at scale","Three anti-patterns I see in 80% of Vertex projects — and how we cut 60% of monthly spend.",6),
|
new("vertex-cost-control","Google Stack","Vertex AI cost control at scale","Three anti-patterns I see in 80% of Vertex projects, and how we cut 60% of monthly spend.",6),
|
||||||
new("k8s-llm-inference","Infra","Sub-50ms LLM inference on Kubernetes","Deployment pattern with KEDA, GPU sharing, and request hedging for stable serving.",14),
|
new("k8s-llm-inference","Infra","Sub-50ms LLM inference on Kubernetes","Deployment pattern with KEDA, GPU sharing, and request hedging for stable serving.",14),
|
||||||
new("flutter-on-device-ai","Mobile","On-device AI in Flutter","Using Gemini Nano and LiteRT for offline inference inside mobile apps.",9),
|
new("flutter-on-device-ai","Mobile","On-device AI in Flutter","Using Gemini Nano and LiteRT for offline inference inside mobile apps.",9),
|
||||||
new("enterprise-ai-roadmap","Strategy","A 90-day enterprise AI roadmap","The framework I build for CTOs — from use-case discovery to first production deployment.",7),
|
new("enterprise-ai-roadmap","Strategy","A 90-day enterprise AI roadmap","The framework I build for CTOs, from use-case discovery to first production deployment.",7),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-28
@@ -1,48 +1,32 @@
|
|||||||
@page "/blog/{slug}"
|
@page "/blog/{slug}"
|
||||||
@model SoroushAsadi.Pages.Blog.PostModel
|
@model SoroushAsadi.Pages.Blog.PostModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = Model.Title + " — Soroush Asadi";
|
ViewData["Title"] = Model.Title + " - Soroush Asadi";
|
||||||
var fa = Model.IsFa;
|
var fa = Model.IsFa;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="min-h-screen pt-28 pb-20 px-5 sm:px-8">
|
<div class="px-5 pt-28 pb-24 sm:px-8 sm:pt-32">
|
||||||
<div class="mx-auto max-w-3xl">
|
<div class="mx-auto max-w-2xl">
|
||||||
<a href="/blog" class="label-mono mb-8 inline-flex items-center gap-2 text-slate-400 hover:text-white transition-colors @(fa ? "flex-row-reverse" : "")">
|
<a href="/blog" class="mb-10 inline-flex items-center gap-2 text-sm text-zinc-500 transition-colors hover:text-zinc-900 @(fa ? "flex-row-reverse" : "")">
|
||||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" class="@(fa ? "" : "rotate-180")"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" class="@(fa ? "" : "rotate-180")"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
||||||
@(fa ? "بازگشت به بلاگ" : "Back to blog")
|
@(fa ? "بازگشت به بلاگ" : "Back to blog")
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@if (Model.PostNotFound)
|
@if (Model.PostNotFound)
|
||||||
{
|
{
|
||||||
<p class="text-slate-400">@(fa ? "مقاله پیدا نشد." : "Post not found.")</p>
|
<p class="text-zinc-600">@(fa ? "مقاله پیدا نشد." : "Post not found.")</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="mb-6">
|
<header class="mb-8">
|
||||||
<span class="label-mono text-electric">@Model.Category</span>
|
<span class="kicker">@Model.Category</span>
|
||||||
<h1 class="mt-3 font-display text-3xl font-extrabold leading-tight text-white @(fa ? "font-fa" : "")">@Model.Title</h1>
|
<h1 class="mt-3 @(fa ? "font-fa" : "")" style="font-size:clamp(1.8rem,4vw,2.5rem)">@Model.Title</h1>
|
||||||
<p class="mt-2 label-mono text-slate-500">@Model.ReadTime @(fa ? "دقیقه مطالعه" : "min read")</p>
|
<p class="mt-3 text-sm text-zinc-500">@Model.ReadTime @(fa ? "دقیقه مطالعه" : "min read")</p>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<article class="prose-custom glass p-8">
|
<article class="prose-custom">
|
||||||
@Html.Raw(Model.BodyHtml)
|
@Html.Raw(Model.BodyHtml)
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<style>
|
|
||||||
.prose-custom { color:#cbd5e1; line-height:1.8; }
|
|
||||||
.prose-custom h2,.prose-custom h3 { font-family:'Syne',sans-serif; font-weight:700; color:#fff; margin:2rem 0 .75rem; }
|
|
||||||
.prose-custom p { margin-bottom:1.25rem; }
|
|
||||||
.prose-custom code { font-family:'SpaceMono',monospace; background:rgba(56,189,248,.08); border:1px solid rgba(56,189,248,.2); border-radius:.35rem; padding:.15em .45em; font-size:.85em; color:#38bdf8; }
|
|
||||||
.prose-custom pre { background:#050a1a; border:1px solid rgba(255,255,255,.06); border-radius:.75rem; padding:1.25rem; overflow-x:auto; margin:1.5rem 0; }
|
|
||||||
.prose-custom pre code { background:none; border:none; padding:0; color:#e2e8f0; }
|
|
||||||
.prose-custom ul,.prose-custom ol { padding-inline-start:1.5rem; margin-bottom:1.25rem; }
|
|
||||||
.prose-custom li { margin-bottom:.4rem; }
|
|
||||||
.prose-custom blockquote { border-inline-start:3px solid #38bdf8; padding-inline-start:1rem; color:#94a3b8; margin:1.5rem 0; }
|
|
||||||
.prose-custom strong { color:#e2e8f0; }
|
|
||||||
.prose-custom a { color:#38bdf8; text-decoration:underline; text-underline-offset:.2em; }
|
|
||||||
</style>
|
|
||||||
}
|
|
||||||
|
|||||||
+167
-38
@@ -14,24 +14,25 @@ public class PostModel(ContentService content) : BasePageModel
|
|||||||
public string BodyHtml { get; private set; } = "";
|
public string BodyHtml { get; private set; } = "";
|
||||||
public bool PostNotFound { get; private set; }
|
public bool PostNotFound { get; private set; }
|
||||||
|
|
||||||
// Default bodies (Markdown-lite, rendered server-side)
|
// Default bodies (Markdown-lite, rendered server-side). Body is locale-aware.
|
||||||
private static readonly Dictionary<string, (string Cat, string TitleEn, string TitleFa, int RT, string Body)> _defaults = new()
|
private static readonly Dictionary<string, (string Cat, string TitleEn, string TitleFa, int RT, string BodyEn, string BodyFa)> _defaults = new()
|
||||||
{
|
{
|
||||||
["rag-eval-framework"] = ("LLM", "A RAG evaluation framework that holds up in production", "چارچوب ارزیابی RAG که در تولید کار میکند", 8, DefaultBodies.RagEval),
|
["rag-eval-framework"] = ("LLM", "A RAG evaluation framework that holds up in production", "چارچوب ارزیابی RAG که در عمل جواب میدهد", 8, DefaultBodies.RagEval, DefaultBodies.RagEvalFa),
|
||||||
["agentic-n8n-patterns"] = ("Automation", "Agentic patterns with n8n for the enterprise", "الگوهای عاملمحور با n8n برای سازمان", 11, DefaultBodies.N8nPatterns),
|
["agentic-n8n-patterns"] = ("Automation", "Agentic patterns with n8n for the enterprise", "الگوهای عاملمحور با n8n برای سازمان", 11, DefaultBodies.N8nPatterns, DefaultBodies.N8nPatternsFa),
|
||||||
["vertex-cost-control"] = ("Google Stack", "Vertex AI cost control at scale", "کنترل هزینه روی Vertex AI در مقیاس بالا", 6, DefaultBodies.VertexCost),
|
["vertex-cost-control"] = ("Google Stack", "Vertex AI cost control at scale", "کنترل هزینه روی Vertex AI در مقیاس بالا", 6, DefaultBodies.VertexCost, DefaultBodies.VertexCostFa),
|
||||||
["k8s-llm-inference"] = ("Infra", "Sub-50ms LLM inference on Kubernetes", "استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ ms",14, DefaultBodies.K8sInference),
|
["k8s-llm-inference"] = ("Infra", "Sub-50ms LLM inference on Kubernetes", "اجرای LLM روی Kubernetes با تأخیر زیر ۵۰ میلیثانیه", 14, DefaultBodies.K8sInference, DefaultBodies.K8sInferenceFa),
|
||||||
["flutter-on-device-ai"] = ("Mobile", "On-device AI in Flutter", "هوش مصنوعی on-device در Flutter", 9, DefaultBodies.FlutterAI),
|
["flutter-on-device-ai"] = ("Mobile", "On-device AI in Flutter", "هوش مصنوعی روی دستگاه در Flutter", 9, DefaultBodies.FlutterAI, DefaultBodies.FlutterAIFa),
|
||||||
["enterprise-ai-roadmap"] = ("Strategy", "A 90-day enterprise AI roadmap", "نقشه راه هوش مصنوعی سازمانی در ۹۰ روز", 7, DefaultBodies.EnterpriseRoadmap),
|
["enterprise-ai-roadmap"] = ("Strategy", "A 90-day enterprise AI roadmap", "نقشهی راه هوش مصنوعی سازمانی در ۹۰ روز", 7, DefaultBodies.EnterpriseRoadmap, DefaultBodies.EnterpriseRoadmapFa),
|
||||||
};
|
};
|
||||||
|
|
||||||
public void OnGet()
|
public void OnGet()
|
||||||
{
|
{
|
||||||
if (!_defaults.TryGetValue(Slug, out var def)) { PostNotFound = true; return; }
|
if (!_defaults.TryGetValue(Slug, out var def)) { PostNotFound = true; return; }
|
||||||
|
|
||||||
|
string body = IsFa ? def.BodyFa : def.BodyEn;
|
||||||
|
|
||||||
// Check for DB override (stored under "posts" key as slug→{body,...})
|
// Check for DB override (stored under "posts" key as slug→{body,...})
|
||||||
var overrides = content.GetPostOverrides();
|
var overrides = content.GetPostOverrides();
|
||||||
string body = def.Body;
|
|
||||||
if (overrides.TryGetValue(Slug, out var node) && node["body"]?.GetValue<string>() is { } dbBody)
|
if (overrides.TryGetValue(Slug, out var node) && node["body"]?.GetValue<string>() is { } dbBody)
|
||||||
body = dbBody;
|
body = dbBody;
|
||||||
|
|
||||||
@@ -83,13 +84,13 @@ public class PostModel(ContentService content) : BasePageModel
|
|||||||
private static string Esc(string s) => s.Replace("&","&").Replace("<","<").Replace(">",">");
|
private static string Esc(string s) => s.Replace("&","&").Replace("<","<").Replace(">",">");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default article bodies (Markdown).
|
/// Default article bodies (Markdown). EN + FA per post.
|
||||||
internal static class DefaultBodies
|
internal static class DefaultBodies
|
||||||
{
|
{
|
||||||
public const string RagEval = """
|
public const string RagEval = """
|
||||||
## Why standard metrics fail for RAG
|
## Why standard metrics fail for RAG
|
||||||
|
|
||||||
BLEU and ROUGE measure n-gram overlap against a reference answer. In a RAG system, there is often no single correct reference — a question about company policy may have dozens of valid phrasings. High BLEU does not mean the system cited the right source; low BLEU does not mean it was wrong.
|
BLEU and ROUGE measure n-gram overlap against a reference answer. In a RAG system there is often no single correct reference: a question about company policy may have dozens of valid phrasings. High BLEU does not mean the system cited the right source; low BLEU does not mean it was wrong.
|
||||||
|
|
||||||
## The three metrics that actually matter
|
## The three metrics that actually matter
|
||||||
|
|
||||||
@@ -97,55 +98,117 @@ BLEU and ROUGE measure n-gram overlap against a reference answer. In a RAG syste
|
|||||||
|
|
||||||
**Context Precision** asks: of the passages retrieved, how many were actually relevant to the question? Low precision wastes context window and increases hallucination risk.
|
**Context Precision** asks: of the passages retrieved, how many were actually relevant to the question? Low precision wastes context window and increases hallucination risk.
|
||||||
|
|
||||||
**Answer Relevancy** checks whether the final response actually addresses what was asked — not just whether it sounds good.
|
**Answer Relevancy** checks whether the final response actually addresses what was asked, not just whether it sounds good.
|
||||||
|
|
||||||
## Building an eval harness
|
## Building an eval harness
|
||||||
|
|
||||||
Start with a **golden dataset**: 100–200 question/answer pairs that domain experts have verified. Run your pipeline against them nightly. Track the three metrics above over time. A drop in Faithfulness after a model upgrade is a red flag; a drop in Context Precision after a chunking change means your retrieval is degrading.
|
Start with a **golden dataset**: 100-200 question/answer pairs that domain experts have verified. Run your pipeline against them nightly. Track the three metrics above over time. A drop in Faithfulness after a model upgrade is a red flag; a drop in Context Precision after a chunking change means your retrieval is degrading.
|
||||||
|
|
||||||
The harness does not have to be complex. A spreadsheet with automatic scoring via the OpenAI or Anthropic API is enough to start catching regressions before they reach production.
|
The harness does not have to be complex. A spreadsheet with automatic scoring via the OpenAI or Anthropic API is enough to start catching regressions before they reach production.
|
||||||
|
""";
|
||||||
|
|
||||||
|
public const string RagEvalFa = """
|
||||||
|
## چرا معیارهای استاندارد برای RAG جواب نمیدهند
|
||||||
|
|
||||||
|
BLEU و ROUGE میزان همپوشانی n-gram را با یک پاسخ مرجع میسنجند. در یک سامانهی RAG معمولاً پاسخ مرجع واحدی وجود ندارد؛ یک پرسش دربارهی سیاستهای سازمان میتواند دهها بیان درست داشته باشد. BLEU بالا به این معنا نیست که سیستم به منبع درست ارجاع داده، و BLEU پایین هم به این معنا نیست که اشتباه کرده.
|
||||||
|
|
||||||
|
## سه معیاری که واقعاً مهماند
|
||||||
|
|
||||||
|
**وفاداری (Faithfulness)** میسنجد که آیا هر ادعای پاسخ تولیدشده را میتوان به یک قطعهی بازیابیشده ردیابی کرد. امتیاز وفاداری ۱.۰ یعنی مدل چیزی از خودش نساخته. ابزارهایی مثل RAGAS این را با یک داور LLM پیاده میکنند.
|
||||||
|
|
||||||
|
**دقت زمینه (Context Precision)** میپرسد: از میان قطعههای بازیابیشده، چند تا واقعاً به پرسش مربوط بودند؟ دقت پایین، پنجرهی زمینه را هدر میدهد و خطر توهم را بالا میبرد.
|
||||||
|
|
||||||
|
**مرتبطبودن پاسخ (Answer Relevancy)** بررسی میکند که پاسخ نهایی واقعاً به آنچه پرسیده شده جواب میدهد، نه اینکه فقط خوب به نظر برسد.
|
||||||
|
|
||||||
|
## ساختن یک بستر ارزیابی
|
||||||
|
|
||||||
|
با یک **دیتاست طلایی** شروع کنید: ۱۰۰ تا ۲۰۰ جفت پرسش و پاسخ که کارشناسان حوزه تأییدشان کردهاند. هر شب پایپلاین را روی آنها اجرا کنید و این سه معیار را در طول زمان دنبال کنید. افت وفاداری بعد از ارتقای مدل یک هشدار جدی است؛ افت دقت زمینه بعد از تغییر قطعهبندی یعنی بازیابیتان دارد بدتر میشود.
|
||||||
|
|
||||||
|
بستر ارزیابی لازم نیست پیچیده باشد. یک صفحهگسترده با امتیازدهی خودکار از طریق API اوپنایآی یا Anthropic، برای شروع و گرفتن افت کیفیت پیش از رسیدن به تولید کافی است.
|
||||||
""";
|
""";
|
||||||
|
|
||||||
public const string N8nPatterns = """
|
public const string N8nPatterns = """
|
||||||
## The problem with "just use n8n"
|
## The problem with "just use n8n"
|
||||||
|
|
||||||
n8n is excellent for integrating SaaS tools. It becomes fragile when you try to use it as an agent orchestrator — long-running loops, conditional retries, and LLM calls that can fail in non-obvious ways.
|
n8n is excellent for integrating SaaS tools. It becomes fragile when you try to use it as an agent orchestrator: long-running loops, conditional retries, and LLM calls that can fail in non-obvious ways.
|
||||||
|
|
||||||
## Separating orchestration from integration
|
## Separating orchestration from integration
|
||||||
|
|
||||||
The pattern that works: **n8n handles triggers and integrations; LangGraph handles agent logic**.
|
The pattern that works: **n8n handles triggers and integrations; LangGraph handles agent logic**.
|
||||||
|
|
||||||
An n8n workflow watches a Slack channel. When a message matches a pattern, it calls a LangGraph endpoint with the raw payload. LangGraph runs the multi-step reasoning loop, maintains state, and returns a structured result. n8n takes that result and routes it — posts to Jira, sends an email, updates a database row.
|
An n8n workflow watches a Slack channel. When a message matches a pattern, it calls a LangGraph endpoint with the raw payload. LangGraph runs the multi-step reasoning loop, maintains state, and returns a structured result. n8n takes that result and routes it: posts to Jira, sends an email, updates a database row.
|
||||||
|
|
||||||
## Making agents auditable
|
## Making agents auditable
|
||||||
|
|
||||||
Every LangGraph state transition should emit an event to a structured log. We use a Postgres table with columns: `run_id`, `step`, `input`, `output`, `timestamp`. This table becomes the audit trail that compliance teams and on-call engineers both need.
|
Every LangGraph state transition should emit an event to a structured log. We use a Postgres table with columns: `run_id`, `step`, `input`, `output`, `timestamp`. This table becomes the audit trail that compliance teams and on-call engineers both need.
|
||||||
|
|
||||||
Add a `human_in_the_loop` node for any action that cannot be undone — deleting records, sending external emails, approving payments. The node pauses execution and posts to Slack; a human approves or rejects; execution resumes.
|
Add a `human_in_the_loop` node for any action that cannot be undone: deleting records, sending external emails, approving payments. The node pauses execution and posts to Slack; a human approves or rejects; execution resumes.
|
||||||
|
|
||||||
## Handling failures gracefully
|
## Handling failures gracefully
|
||||||
|
|
||||||
LLM calls fail. Build **retry with exponential backoff** into every LangGraph node that calls an LLM. Set a hard limit of 3 retries, then route to a dead-letter state that pages the on-call engineer. Never silently swallow errors in agentic pipelines — a swallowed error is an invisible outage.
|
LLM calls fail. Build **retry with exponential backoff** into every LangGraph node that calls an LLM. Set a hard limit of 3 retries, then route to a dead-letter state that pages the on-call engineer. Never silently swallow errors in agentic pipelines. A swallowed error is an invisible outage.
|
||||||
|
""";
|
||||||
|
|
||||||
|
public const string N8nPatternsFa = """
|
||||||
|
## مشکلِ «فقط از n8n استفاده کن»
|
||||||
|
|
||||||
|
n8n برای اتصال ابزارهای SaaS عالی است. اما وقتی بخواهید از آن بهعنوان ارکستراتورِ عامل استفاده کنید شکننده میشود؛ حلقههای طولانی، تلاشهای مجدد شرطی، و فراخوانیهای LLM که میتوانند به شکلهای غیرمنتظره شکست بخورند.
|
||||||
|
|
||||||
|
## جدا کردن ارکستراسیون از یکپارچهسازی
|
||||||
|
|
||||||
|
الگویی که جواب میدهد: **n8n تریگرها و یکپارچهسازیها را مدیریت کند، LangGraph منطقِ عامل را**.
|
||||||
|
|
||||||
|
یک گردشکار n8n یک کانال Slack را زیر نظر میگیرد. وقتی پیامی با الگو مطابقت کرد، یک endpoint از LangGraph را با دادهی خام صدا میزند. LangGraph حلقهی استدلال چندمرحلهای را اجرا میکند، حالت را نگه میدارد و یک نتیجهی ساختارمند برمیگرداند. بعد n8n آن نتیجه را مسیردهی میکند؛ در Jira ثبت میکند، ایمیل میفرستد، یا یک ردیف پایگاهداده را بهروز میکند.
|
||||||
|
|
||||||
|
## قابلممیزی کردنِ عاملها
|
||||||
|
|
||||||
|
هر گذارِ حالت در LangGraph باید یک رویداد در یک لاگ ساختارمند ثبت کند. ما از یک جدول Postgres با ستونهای `run_id`، `step`، `input`، `output` و `timestamp` استفاده میکنیم. این جدول همان ردِ ممیزیای میشود که هم تیمهای انطباق و هم مهندسان کشیک به آن نیاز دارند.
|
||||||
|
|
||||||
|
برای هر کاری که برگشتپذیر نیست یک گرهی `human_in_the_loop` اضافه کنید؛ حذف رکورد، ارسال ایمیل بیرونی، تأیید پرداخت. این گره اجرا را متوقف میکند و در Slack پیام میگذارد؛ یک انسان تأیید یا رد میکند و اجرا ادامه پیدا میکند.
|
||||||
|
|
||||||
|
## مدیریت درستِ خطاها
|
||||||
|
|
||||||
|
فراخوانیهای LLM شکست میخورند. در هر گرهی LangGraph که LLM را صدا میزند **تلاش مجدد با backoff نمایی** بسازید. سقف سه بار تلاش بگذارید، بعد به یک حالت dead-letter مسیردهی کنید که مهندس کشیک را خبر کند. در پایپلاینهای عاملمحور هیچوقت خطا را بیصدا فرو نخورید؛ یک خطای فروخورده، یک قطعی نامرئی است.
|
||||||
""";
|
""";
|
||||||
|
|
||||||
public const string VertexCost = """
|
public const string VertexCost = """
|
||||||
## Anti-pattern 1: calling Gemini Ultra for everything
|
## Anti-pattern 1: calling Gemini Ultra for everything
|
||||||
|
|
||||||
Gemini Ultra (or GPT-4-class models) costs 10–30× more per token than smaller models. Many teams default to the most capable model because it "just works" during prototyping, then never re-evaluate.
|
Gemini Ultra (or GPT-4-class models) costs 10 to 30 times more per token than smaller models. Many teams default to the most capable model because it "just works" during prototyping, then never re-evaluate.
|
||||||
|
|
||||||
**Fix**: build a **model router**. Classify each incoming request by complexity. Simple lookups, short summaries, and classification tasks go to Gemini Flash or Haiku. Only complex reasoning, multi-step synthesis, and long-context tasks go to Pro or Ultra. In most production systems, 60–80% of requests can be served by the cheaper tier.
|
**Fix**: build a **model router**. Classify each incoming request by complexity. Simple lookups, short summaries, and classification tasks go to Gemini Flash or Haiku. Only complex reasoning, multi-step synthesis, and long-context tasks go to Pro or Ultra. In most production systems, 60-80% of requests can be served by the cheaper tier.
|
||||||
|
|
||||||
## Anti-pattern 2: no context caching
|
## Anti-pattern 2: no context caching
|
||||||
|
|
||||||
Vertex AI supports prompt caching (as does the Anthropic API). A system prompt that is 10k tokens, sent with every request at $3/M tokens, costs $30 for every million calls before the user has typed a single word.
|
Vertex AI supports prompt caching (as does the Anthropic API). A system prompt that is 10k tokens, sent with every request at $3/M tokens, costs $30 for every million calls before the user has typed a single word.
|
||||||
|
|
||||||
**Fix**: cache any context that is static or changes infrequently — system prompts, retrieved document sets, few-shot examples. Cache hits cost ~10% of full input price.
|
**Fix**: cache any context that is static or changes infrequently: system prompts, retrieved document sets, few-shot examples. Cache hits cost about 10% of full input price.
|
||||||
|
|
||||||
## Anti-pattern 3: synchronous batch jobs
|
## Anti-pattern 3: synchronous batch jobs
|
||||||
|
|
||||||
Teams run nightly document processing jobs synchronously — one document at a time, each blocked on the previous. This is slow and expensive because you pay for idle wait time between calls.
|
Teams run nightly document processing jobs synchronously, one document at a time, each blocked on the previous. This is slow and expensive because you pay for idle wait time between calls.
|
||||||
|
|
||||||
**Fix**: use the Vertex AI batch prediction API for jobs over ~1,000 documents. Batch jobs run asynchronously, are eligible for spot discounts, and typically cost 50% less per token than online serving.
|
**Fix**: use the Vertex AI batch prediction API for jobs over ~1,000 documents. Batch jobs run asynchronously, are eligible for spot discounts, and typically cost 50% less per token than online serving.
|
||||||
|
""";
|
||||||
|
|
||||||
|
public const string VertexCostFa = """
|
||||||
|
## ضدالگوی اول: صدا زدن Gemini Ultra برای همهچیز
|
||||||
|
|
||||||
|
Gemini Ultra (یا مدلهای همردهی GPT-4) به ازای هر توکن ۱۰ تا ۳۰ برابر گرانتر از مدلهای کوچکترند. خیلی از تیمها در مرحلهی نمونهسازی سراغ توانمندترین مدل میروند چون «همینجوری کار میکند» و بعد دیگر هیچوقت بازنگری نمیکنند.
|
||||||
|
|
||||||
|
**راهحل**: یک **مسیردهندهی مدل (model router)** بسازید. هر درخواست ورودی را بر اساس پیچیدگی دستهبندی کنید. جستوجوهای ساده، خلاصههای کوتاه و کارهای دستهبندی به Gemini Flash یا Haiku بروند. فقط استدلالهای پیچیده، ترکیب چندمرحلهای و کارهای با زمینهی طولانی به Pro یا Ultra. در بیشتر سامانههای تولیدی، ۶۰ تا ۸۰ درصد درخواستها را میشود با ردهی ارزانتر سرویس داد.
|
||||||
|
|
||||||
|
## ضدالگوی دوم: نبودِ کشِ زمینه
|
||||||
|
|
||||||
|
Vertex AI از کش کردن prompt پشتیبانی میکند (مثل API اَنتروپیک). یک system prompt دههزار توکنی که با هر درخواست و با نرخ ۳ دلار به ازای هر میلیون توکن فرستاده میشود، پیش از آنکه کاربر حتی یک کلمه تایپ کند، برای هر میلیون فراخوانی ۳۰ دلار خرج برمیدارد.
|
||||||
|
|
||||||
|
**راهحل**: هر زمینهای که ثابت است یا کم تغییر میکند را کش کنید؛ system promptها، مجموعهی اسناد بازیابیشده، نمونههای few-shot. هزینهی hit کش حدود ۱۰ درصد قیمت کامل ورودی است.
|
||||||
|
|
||||||
|
## ضدالگوی سوم: کارهای دستهای همگام
|
||||||
|
|
||||||
|
تیمها کارهای شبانهی پردازش سند را همگام اجرا میکنند؛ سند به سند، هرکدام منتظر قبلی. این کُند و گران است چون بابت زمان انتظارِ بیکار بین فراخوانیها هم پول میدهید.
|
||||||
|
|
||||||
|
**راهحل**: برای کارهای بالای حدود ۱۰۰۰ سند از batch prediction API در Vertex AI استفاده کنید. کارهای دستهای ناهمگام اجرا میشوند، واجد تخفیف spot هستند و معمولاً به ازای هر توکن ۵۰ درصد ارزانتر از سرویسدهی آنلاین تمام میشوند.
|
||||||
""";
|
""";
|
||||||
|
|
||||||
public const string K8sInference = """
|
public const string K8sInference = """
|
||||||
@@ -155,25 +218,47 @@ A single Kubernetes `Deployment` behind a `ClusterIP` `Service`, fronted by an I
|
|||||||
|
|
||||||
## Autoscaling with KEDA
|
## Autoscaling with KEDA
|
||||||
|
|
||||||
HPA (Horizontal Pod Autoscaler) scales on CPU and memory. LLM inference is GPU-bound and queue-depth-bound — neither maps to CPU utilization well.
|
HPA (Horizontal Pod Autoscaler) scales on CPU and memory. LLM inference is GPU-bound and queue-depth-bound, and neither maps to CPU utilization well.
|
||||||
|
|
||||||
KEDA (Kubernetes Event-Driven Autoscaling) scales on arbitrary metrics — queue depth, Pub/Sub lag, Redis list length. We publish inference request counts to a Redis stream; KEDA scales the model server pods when the stream depth exceeds a threshold. Scaling-up latency drops from minutes (cluster autoscaler cold start) to seconds (replica scale-up from 1 to N).
|
KEDA (Kubernetes Event-Driven Autoscaling) scales on arbitrary metrics: queue depth, Pub/Sub lag, Redis list length. We publish inference request counts to a Redis stream; KEDA scales the model server pods when the stream depth exceeds a threshold. Scaling-up latency drops from minutes (cluster autoscaler cold start) to seconds (replica scale-up from 1 to N).
|
||||||
|
|
||||||
## GPU sharing with time-slicing
|
## GPU sharing with time-slicing
|
||||||
|
|
||||||
For models that fit in 4–8 GB VRAM, full GPU dedication is wasteful. NVIDIA's time-slicing MIG (Multi-Instance GPU) lets multiple pods share one A100, each getting a guaranteed slice.
|
For models that fit in 4 to 8 GB VRAM, full GPU dedication is wasteful. NVIDIA's time-slicing MIG (Multi-Instance GPU) lets multiple pods share one A100, each getting a guaranteed slice.
|
||||||
|
|
||||||
Configure `nvidia.com/gpu: 1` and set the time-slice profile to `1g.10gb`. A single A100 80GB can serve 8 concurrent model instances at 10 GB each — 8× the throughput per GPU.
|
Configure `nvidia.com/gpu: 1` and set the time-slice profile to `1g.10gb`. A single A100 80GB can serve 8 concurrent model instances at 10 GB each, 8 times the throughput per GPU.
|
||||||
|
|
||||||
## Request hedging for tail latency
|
## Request hedging for tail latency
|
||||||
|
|
||||||
p50 latency is 12ms. p99 is 280ms. The tail is dominated by KV-cache misses and occasional GC pauses. **Hedged requests**: after 40ms, send a duplicate request to a second replica. Take whichever response arrives first; cancel the other. This cuts p99 from 280ms to ~45ms with only ~15% increase in total compute.
|
p50 latency is 12ms. p99 is 280ms. The tail is dominated by KV-cache misses and occasional GC pauses. **Hedged requests**: after 40ms, send a duplicate request to a second replica. Take whichever response arrives first; cancel the other. This cuts p99 from 280ms to ~45ms with only ~15% increase in total compute.
|
||||||
|
""";
|
||||||
|
|
||||||
|
public const string K8sInferenceFa = """
|
||||||
|
## معماری پایه
|
||||||
|
|
||||||
|
یک `Deployment` در Kubernetes پشتِ یک `Service` از نوع `ClusterIP` که یک Ingress جلویش قرار گرفته. تا حدود ۵۰ درخواست بر ثانیه برای یک مدل کوچک خوب کار میکند. اما وقتی ترافیک ناگهان بالا میرود، یا زمانبندی podهای GPU سه دقیقه طول میکشد، یا سرور مدل دو ثانیه cold-start دارد، از هم میپاشد.
|
||||||
|
|
||||||
|
## مقیاس خودکار با KEDA
|
||||||
|
|
||||||
|
HPA (مقیاسگذار افقی pod) بر اساس CPU و حافظه مقیاس میدهد. اما استنتاج LLM به GPU و عمق صف وابسته است و هیچکدام با مصرف CPU خوب نگاشت نمیشوند.
|
||||||
|
|
||||||
|
KEDA (مقیاس خودکار رویدادمحور Kubernetes) بر اساس هر معیار دلخواهی مقیاس میدهد؛ عمق صف، تأخیر Pub/Sub، طول لیست Redis. ما تعداد درخواستهای استنتاج را در یک stream در Redis منتشر میکنیم؛ KEDA وقتی عمق stream از یک آستانه عبور کند podهای سرور مدل را مقیاس میدهد. تأخیر مقیاسگرفتن از چند دقیقه (cold-start مقیاسگذار خوشه) به چند ثانیه (افزایش replica از ۱ به N) میرسد.
|
||||||
|
|
||||||
|
## اشتراک GPU با time-slicing
|
||||||
|
|
||||||
|
برای مدلهایی که در ۴ تا ۸ گیگابایت VRAM جا میشوند، اختصاص کاملِ GPU اسراف است. فناوری time-slicing و MIG انویدیا (GPU چنداینستنسه) اجازه میدهد چند pod یک A100 را به اشتراک بگذارند و هرکدام یک سهم تضمینشده بگیرند.
|
||||||
|
|
||||||
|
`nvidia.com/gpu: 1` را تنظیم کنید و پروفایل time-slice را روی `1g.10gb` بگذارید. یک A100 هشتادگیگابایتی میتواند ۸ اینستنس مدل را همزمان، هرکدام با ۱۰ گیگابایت، سرویس بدهد؛ یعنی ۸ برابرِ توان عبوری به ازای هر GPU.
|
||||||
|
|
||||||
|
## hedging درخواست برای تأخیر دنباله
|
||||||
|
|
||||||
|
تأخیر p50 برابر ۱۲ میلیثانیه است و p99 برابر ۲۸۰ میلیثانیه. دنباله را عمدتاً missهای KV-cache و مکثهای گاهبهگاه GC میسازند. **درخواستهای hedged**: بعد از ۴۰ میلیثانیه یک درخواست تکراری به replica دوم بفرستید. هر پاسخی زودتر رسید همان را بردارید و دیگری را لغو کنید. این کار p99 را از ۲۸۰ به حدود ۴۵ میلیثانیه میرساند، با تنها حدود ۱۵ درصد افزایش در کل محاسبات.
|
||||||
""";
|
""";
|
||||||
|
|
||||||
public const string FlutterAI = """
|
public const string FlutterAI = """
|
||||||
## Why on-device inference matters
|
## Why on-device inference matters
|
||||||
|
|
||||||
Cloud inference requires a network round-trip, exposes user data to a server, and fails in offline scenarios. For consumer apps — messaging, health, productivity — on-device inference is often a requirement, not a nice-to-have.
|
Cloud inference requires a network round-trip, exposes user data to a server, and fails in offline scenarios. For consumer apps (messaging, health, productivity) on-device inference is often a requirement, not a nice-to-have.
|
||||||
|
|
||||||
## Gemini Nano and LiteRT
|
## Gemini Nano and LiteRT
|
||||||
|
|
||||||
@@ -183,34 +268,78 @@ LiteRT (formerly TensorFlow Lite) handles vision and custom small models. For cl
|
|||||||
|
|
||||||
## Streaming UX without a network
|
## Streaming UX without a network
|
||||||
|
|
||||||
The key insight: users tolerate slightly slower responses if they can see text appearing token by token. Even on-device inference can stream — Gemini Nano's Dart SDK exposes a `generateContentStream` method. Pipe tokens directly to a Flutter `StreamBuilder` for a responsive feel regardless of total generation time.
|
The key insight: users tolerate slightly slower responses if they can see text appearing token by token. Even on-device inference can stream. Gemini Nano's Dart SDK exposes a `generateContentStream` method. Pipe tokens directly to a Flutter `StreamBuilder` for a responsive feel regardless of total generation time.
|
||||||
|
|
||||||
## Battery and thermal management
|
## Battery and thermal management
|
||||||
|
|
||||||
On-device inference heats the chip. Implement **thermal throttling**: check `DeviceInfo.thermalState` (iOS) or subscribe to the battery API on Android. Reduce `maxTokens` from 512 to 128 during sustained load. Schedule background inference tasks during charging. Users notice neither the throttling nor the scheduling — they notice when their phone gets too hot.
|
On-device inference heats the chip. Implement **thermal throttling**: check `DeviceInfo.thermalState` (iOS) or subscribe to the battery API on Android. Reduce `maxTokens` from 512 to 128 during sustained load. Schedule background inference tasks during charging. Users notice neither the throttling nor the scheduling. They notice when their phone gets too hot.
|
||||||
|
""";
|
||||||
|
|
||||||
|
public const string FlutterAIFa = """
|
||||||
|
## چرا استنتاج روی دستگاه مهم است
|
||||||
|
|
||||||
|
استنتاج ابری یک رفتوبرگشتِ شبکه میخواهد، دادهی کاربر را در معرض سرور میگذارد، و در حالت آفلاین شکست میخورد. برای اپهای مصرفی مثل پیامرسان، سلامت و بهرهوری، استنتاج روی دستگاه اغلب یک الزام است، نه یک امکانِ خوببهداشتن.
|
||||||
|
|
||||||
|
## Gemini Nano و LiteRT
|
||||||
|
|
||||||
|
Gemini Nano گوگل یک مدل ۱.۸ میلیارد پارامتری است که کوانتیزه شده تا روی NPUهای موبایل (واحدهای پردازش عصبی) اجرا شود. یکپارچهسازی با Flutter از پکیج `google_ai_dart_sdk` و `GeminiNanoModel` استفاده میکند و وقتی مدلِ روی دستگاه در دسترس نباشد به استنتاج ابری برمیگردد.
|
||||||
|
|
||||||
|
LiteRT (همان TensorFlow Lite سابق) بینایی و مدلهای کوچک سفارشی را مدیریت میکند. برای کارهای دستهبندی و embedding، یک مدل کوانتیزهی ۵۰ مگابایتی روی یک گوشی اندرویدی میانرده در کمتر از ۲۰ میلیثانیه اجرا میشود.
|
||||||
|
|
||||||
|
## تجربهی استریم بدون شبکه
|
||||||
|
|
||||||
|
نکتهی کلیدی: کاربرها پاسخ کمی کندتر را تحمل میکنند اگر ببینند متن توکنبهتوکن ظاهر میشود. حتی استنتاج روی دستگاه هم میتواند استریم کند. Dart SDK مربوط به Gemini Nano متد `generateContentStream` را در اختیار میگذارد. توکنها را مستقیم به یک `StreamBuilder` در Flutter بدهید تا فارغ از کل زمان تولید، حسی پاسخگو داشته باشید.
|
||||||
|
|
||||||
|
## مدیریت باتری و دما
|
||||||
|
|
||||||
|
استنتاج روی دستگاه تراشه را گرم میکند. **throttling حرارتی** پیاده کنید: `DeviceInfo.thermalState` را در iOS بررسی کنید یا در اندروید به API باتری گوش بدهید. زیر بار طولانی `maxTokens` را از ۵۱۲ به ۱۲۸ کم کنید. کارهای استنتاجِ پسزمینه را برای زمان شارژ زمانبندی کنید. کاربر نه throttling را میفهمد و نه زمانبندی را؛ فقط وقتی گوشیاش داغ شود متوجه میشود.
|
||||||
""";
|
""";
|
||||||
|
|
||||||
public const string EnterpriseRoadmap = """
|
public const string EnterpriseRoadmap = """
|
||||||
## Days 1–30: discovery
|
## Days 1-30: discovery
|
||||||
|
|
||||||
The most expensive mistake in enterprise AI is building the wrong thing fast. Discovery is not a formality — it is the work.
|
The most expensive mistake in enterprise AI is building the wrong thing fast. Discovery is not a formality. It is the work.
|
||||||
|
|
||||||
Interview 8–12 stakeholders across business units. For each, ask: what manual task takes more than 2 hours per week? What decision do you make with incomplete information? What report do you wish existed but is too expensive to build?
|
Interview 8 to 12 stakeholders across business units. For each, ask: what manual task takes more than 2 hours per week? What decision do you make with incomplete information? What report do you wish existed but is too expensive to build?
|
||||||
|
|
||||||
Map the candidates on a 2×2: **impact** (revenue, cost, risk) vs **feasibility** (data quality, integration complexity, regulatory constraints). The top-right quadrant is your first sprint.
|
Map the candidates on a 2x2: **impact** (revenue, cost, risk) vs **feasibility** (data quality, integration complexity, regulatory constraints). The top-right quadrant is your first sprint.
|
||||||
|
|
||||||
## Days 31–60: prototype and validate
|
## Days 31-60: prototype and validate
|
||||||
|
|
||||||
Pick one use case from the top-right. Build a prototype in 3 weeks. The prototype does not have to be production-grade — it has to be **testable by domain experts**.
|
Pick one use case from the top-right. Build a prototype in 3 weeks. The prototype does not have to be production-grade. It has to be **testable by domain experts**.
|
||||||
|
|
||||||
Run a structured eval: 100 questions, domain expert scores each answer 1–5. Set a threshold (e.g., ≥4.0 average) before the sprint begins. If the prototype clears it, proceed to production hardening. If it doesn't, investigate root cause — usually data quality or chunking strategy — before committing engineering resources.
|
Run a structured eval: 100 questions, domain expert scores each answer 1 to 5. Set a threshold (e.g. 4.0 average or higher) before the sprint begins. If the prototype clears it, proceed to production hardening. If it does not, investigate root cause (usually data quality or chunking strategy) before committing engineering resources.
|
||||||
|
|
||||||
## Days 61–90: first production deployment
|
## Days 61-90: first production deployment
|
||||||
|
|
||||||
Scope the first deployment to a single team of 10–20 people. This limits blast radius and generates real usage data fast.
|
Scope the first deployment to a single team of 10 to 20 people. This limits blast radius and generates real usage data fast.
|
||||||
|
|
||||||
Instrument everything: latency, cost per query, thumbs-up/thumbs-down from users, faithfulness score from the automated harness. Review metrics weekly with the business owner. Adjust chunking, retrieval strategy, or model tier based on what the data shows — not intuition.
|
Instrument everything: latency, cost per query, thumbs-up/thumbs-down from users, faithfulness score from the automated harness. Review metrics weekly with the business owner. Adjust chunking, retrieval strategy, or model tier based on what the data shows, not intuition.
|
||||||
|
|
||||||
At day 90, you have a live system, a tuned eval harness, and a clear picture of what the second use case should be. That is the foundation for a credible 12-month roadmap.
|
At day 90, you have a live system, a tuned eval harness, and a clear picture of what the second use case should be. That is the foundation for a credible 12-month roadmap.
|
||||||
|
""";
|
||||||
|
|
||||||
|
public const string EnterpriseRoadmapFa = """
|
||||||
|
## روز ۱ تا ۳۰: کشف
|
||||||
|
|
||||||
|
گرانترین اشتباه در هوش مصنوعی سازمانی، سریعساختنِ چیز اشتباه است. کشف یک تشریفات نیست؛ خودِ کار است.
|
||||||
|
|
||||||
|
با ۸ تا ۱۲ ذینفع در واحدهای مختلفِ کسبوکار مصاحبه کنید. از هرکدام بپرسید: کدام کار دستی بیش از دو ساعت در هفته وقت میگیرد؟ کدام تصمیم را با اطلاعات ناقص میگیرید؟ کدام گزارش را آرزو دارید داشته باشید ولی ساختنش گران است؟
|
||||||
|
|
||||||
|
نامزدها را روی یک ماتریس ۲×۲ بچینید: **اثر** (درآمد، هزینه، ریسک) در برابر **امکانپذیری** (کیفیت داده، پیچیدگی یکپارچهسازی، محدودیتهای مقرراتی). ربعِ بالا-راست، اولین sprint شماست.
|
||||||
|
|
||||||
|
## روز ۳۱ تا ۶۰: نمونهی اولیه و اعتبارسنجی
|
||||||
|
|
||||||
|
یک مورد کاربری از ربعِ بالا-راست بردارید. در سه هفته یک نمونهی اولیه بسازید. نمونه لازم نیست در سطح تولید باشد؛ باید **توسط کارشناسان حوزه قابلآزمون** باشد.
|
||||||
|
|
||||||
|
یک ارزیابی ساختارمند اجرا کنید: ۱۰۰ پرسش، کارشناس حوزه به هر پاسخ از ۱ تا ۵ امتیاز میدهد. پیش از شروع sprint یک آستانه بگذارید (مثلاً میانگین ۴.۰ یا بالاتر). اگر نمونه از آن گذشت، به سمتِ سختسازی برای تولید بروید. اگر نگذشت، ریشه را پیدا کنید (معمولاً کیفیت داده یا راهبرد قطعهبندی) و بعد منابع مهندسی را متعهد کنید.
|
||||||
|
|
||||||
|
## روز ۶۱ تا ۹۰: اولین استقرار تولید
|
||||||
|
|
||||||
|
اولین استقرار را به یک تیم ۱۰ تا ۲۰ نفره محدود کنید. این کار شعاع آسیب را کم میکند و سریع دادهی استفادهی واقعی میسازد.
|
||||||
|
|
||||||
|
همهچیز را اندازه بگیرید: تأخیر، هزینه به ازای هر پرسش، بازخورد مثبت و منفی کاربرها، امتیاز وفاداری از بسترِ خودکار. هفتگی با صاحب کسبوکار معیارها را مرور کنید. قطعهبندی، راهبرد بازیابی یا ردهی مدل را بر اساس آنچه داده نشان میدهد تنظیم کنید، نه بر اساس حدس.
|
||||||
|
|
||||||
|
در روز ۹۰ یک سامانهی زنده، یک بسترِ ارزیابیِ تنظیمشده، و تصویری روشن از اینکه دومین مورد کاربری چه باید باشد دارید. این، پایهی یک نقشهی راهِ ۱۲ماههی معتبر است.
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|||||||
+247
-398
@@ -5,162 +5,94 @@
|
|||||||
var locale = Model.Locale;
|
var locale = Model.Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- ─── HERO ─────────────────────────────────────────────────────────── -->
|
<!-- ─── HERO (bento) ─────────────────────────────────────────────────── -->
|
||||||
<section id="top" class="relative isolate overflow-hidden min-h-[100svh] pt-28 pb-20 sm:pt-32">
|
<section id="top" class="px-5 pt-24 pb-12 sm:px-8 sm:pt-28">
|
||||||
<!-- Particle canvas -->
|
<div class="mx-auto max-w-6xl">
|
||||||
<div class="pointer-events-none absolute inset-0 -z-10">
|
<div class="bento">
|
||||||
<canvas id="particle-canvas" class="h-full w-full opacity-60"></canvas>
|
|
||||||
<div aria-hidden class="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-b from-transparent to-base"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Aurora background -->
|
<!-- Name / anchor tile -->
|
||||||
<div aria-hidden class="pointer-events-none absolute inset-0 -z-20"
|
<div class="tile tile-dark span-2 row-2 reveal">
|
||||||
style="background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(56,189,248,.18),transparent 60%),
|
<span class="kicker @(fa ? "font-fa" : "")" style="color:#a1a1aa">@(fa ? "مهندس نرمافزار و هوش مصنوعی" : "Software & AI Engineer")</span>
|
||||||
radial-gradient(ellipse 60% 40% at 80% 10%,rgba(232,121,249,.10),transparent 60%),
|
<h1 class="mt-3 @(fa ? "font-fa" : "")" style="font-size:clamp(2.4rem,5.5vw,4rem)">@(fa ? "سروش اسعدی" : "Soroush Asadi")</h1>
|
||||||
radial-gradient(ellipse 60% 40% at 10% 30%,rgba(129,140,248,.10),transparent 60%)"></div>
|
<p class="t-sub mt-3 text-[1rem] @(fa ? "font-fa" : "")">@(fa ? "معمار سیستم. از ایده تا اجرا." : "Solution architect. From idea to production.")</p>
|
||||||
|
<div class="mt-auto flex flex-wrap gap-3 pt-8">
|
||||||
<div class="mx-auto flex max-w-7xl flex-col items-center px-5 text-center sm:px-8">
|
<a href="#contact" class="btn">
|
||||||
|
@(fa ? "رزرو جلسه" : "Book a call")
|
||||||
<!-- Availability chip -->
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" class="@(fa ? "rotate-180" : "")" aria-hidden="true"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
|
||||||
<div class="mb-7 reveal">
|
|
||||||
<span class="chip">
|
|
||||||
<span class="relative inline-flex h-2 w-2">
|
|
||||||
<span class="absolute inset-0 animate-pulse-dot rounded-full bg-emerald"></span>
|
|
||||||
<span class="relative inline-block h-2 w-2 rounded-full bg-emerald"></span>
|
|
||||||
</span>
|
|
||||||
@(fa ? "پذیرش پروژههای منتخب فصل سوم ۲۰۲۶" : "Available for select Q3 2026 engagements")
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Eyebrow -->
|
|
||||||
<p class="label-mono mb-6 inline-flex items-center gap-3 reveal" style="transition-delay:.08s">
|
|
||||||
<span class="h-px w-10 bg-electric/60" aria-hidden></span>
|
|
||||||
@(fa ? "مهندس هوش مصنوعی · مشاور · معمار راهکار" : "AI Engineer · Consultant · Solution Architect")
|
|
||||||
<span class="h-px w-10 bg-electric/60" aria-hidden></span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Name -->
|
|
||||||
<h1 class="font-display text-balance font-extrabold leading-[1.02] tracking-tight text-white reveal @(fa ? "font-fa" : "")"
|
|
||||||
style="font-size:clamp(2.4rem,7vw,5.4rem);transition-delay:.15s">
|
|
||||||
@(fa ? "سروش اسعدی" : "Soroush Asadi")
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Headline -->
|
|
||||||
<p class="mt-5 max-w-4xl text-balance font-medium leading-[1.25] text-slate-200 reveal"
|
|
||||||
style="font-size:clamp(1.15rem,2.2vw,1.75rem);transition-delay:.25s">
|
|
||||||
@(fa ? "طراحی سامانههای" : "Architecting")
|
|
||||||
<span class="gradient-text font-semibold">@(fa ? "هوش مصنوعی" : "production-grade AI")</span>
|
|
||||||
@(fa ? "در مقیاس سازمانی." : "for the enterprise.")
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Typewriter -->
|
|
||||||
<div class="mt-5 flex items-center gap-3 font-mono uppercase tracking-[.15em] text-slate-400 reveal"
|
|
||||||
style="font-size:clamp(.9rem,1.4vw,1.05rem);transition-delay:.35s">
|
|
||||||
<span class="h-px w-6 bg-slate-700" aria-hidden></span>
|
|
||||||
<span id="typewriter"
|
|
||||||
data-words='@(fa
|
|
||||||
? "[\"راهبرد هوش مصنوعی\",\"مهندسی LLM و RAG\",\"معماری راهکار\",\"اتوماسیون عاملمحور\",\"استک گوگل کلود\"]"
|
|
||||||
: "[\"AI Strategy\",\"LLM & RAG Engineering\",\"Solution Architecture\",\"Agentic Automation\",\"Google Cloud Stack\"]")'></span>
|
|
||||||
<span class="inline-block w-px h-[1em] bg-electric animate-caret-blink" aria-hidden></span>
|
|
||||||
<span class="h-px w-6 bg-slate-700" aria-hidden></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sub -->
|
|
||||||
<p class="mt-7 max-w-2xl text-balance leading-relaxed text-slate-400 reveal"
|
|
||||||
style="font-size:clamp(.95rem,1.4vw,1.08rem);transition-delay:.42s">
|
|
||||||
@(fa
|
|
||||||
? "از راهبرد تا تولید — ساخت پایپلاینهای LLM، عاملهای خودکار، و معماریهای ابری که در میلیونها رویداد در روز پایدار میمانند."
|
|
||||||
: "From strategy to deployment — building LLM pipelines, autonomous agents, and cloud architectures that hold up at millions of events per day.")
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- CTAs -->
|
|
||||||
<div class="mt-9 flex flex-wrap items-center justify-center gap-3 reveal" style="transition-delay:.5s">
|
|
||||||
<a href="#contact" class="btn-primary">
|
|
||||||
@(fa ? "رزرو جلسه مشاوره" : "Book a consultation")
|
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" class="@(fa ? "rotate-180" : "")" aria-hidden="true"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
|
||||||
</a>
|
</a>
|
||||||
<a href="#services" class="btn-ghost">@(fa ? "مشاهده خدمات" : "View services")</a>
|
<a href="#portfolio" class="btn-ghost btn-on-dark">@(fa ? "نمونهکارها" : "View work")</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Metrics -->
|
<!-- Value-prop tile -->
|
||||||
<div class="mt-16 grid w-full max-w-4xl grid-cols-2 gap-4 sm:grid-cols-4 reveal" style="transition-delay:.6s">
|
<div class="tile span-2 reveal" style="transition-delay:.06s">
|
||||||
@{
|
<p class="mt-auto text-balance leading-snug text-zinc-800 @(fa ? "font-fa" : "")" style="font-size:clamp(1.15rem,2vw,1.5rem)">
|
||||||
var metrics = fa
|
@(fa ? "نرمافزار، اپلیکیشنهای سازمانی و " : "I build software, enterprise apps, and ")<span class="accent-text font-semibold">@(fa ? "راهکارهای هوش مصنوعی" : "AI solutions")</span>@(fa ? " میسازم که در عمل و در مقیاس واقعی کار میکنند." : " that hold up in production, at real scale.")
|
||||||
? new[]{ ("۱۸+","مدل هوش مصنوعی مستقر","text-electric"), ("۴۰+","میکروسرویس تولید","text-violet"), ("۱۲ms","تأخیر استنتاج","text-magenta"), ("۹۹٪","پایداری SLA","text-emerald") }
|
</p>
|
||||||
: new[]{ ("18+","AI models in production","text-electric"), ("40+","microservices shipped","text-violet"), ("12ms","inference latency","text-magenta"), ("99%","SLA uptime","text-emerald") };
|
|
||||||
}
|
|
||||||
@foreach (var (val, label, color) in metrics)
|
|
||||||
{
|
|
||||||
<div class="glass relative overflow-hidden px-5 py-5 text-start">
|
|
||||||
<span aria-hidden class="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-electric/50 to-transparent"></span>
|
|
||||||
<div class="font-display font-bold leading-none @color" style="font-size:clamp(1.6rem,3vw,2.25rem)">@val</div>
|
|
||||||
<div class="mt-2 text-[.78rem] leading-snug text-slate-400">@label</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scroll cue -->
|
<!-- Availability tile -->
|
||||||
<a href="#services" class="mt-14 inline-flex flex-col items-center gap-2 text-slate-500 transition-colors hover:text-slate-200 reveal" style="transition-delay:.75s" aria-label="@(fa ? "اسکرول" : "Scroll")">
|
<div class="tile tile-accent reveal" style="transition-delay:.12s">
|
||||||
<span class="label-mono">@(fa ? "اسکرول" : "Scroll")</span>
|
<span class="inline-flex items-center gap-2 text-[.82rem]" style="color:rgba(255,255,255,.9)"><span style="width:7px;height:7px;border-radius:99px;background:#fff;display:inline-block"></span>@(fa ? "وضعیت" : "Status")</span>
|
||||||
<span class="relative block h-9 w-5 rounded-full border border-slate-700">
|
<p class="mt-auto pt-6 text-[1.05rem] font-semibold @(fa ? "font-fa" : "")" style="color:#fff">@(fa ? "پذیرای پروژههای جدید" : "Open for new projects")</p>
|
||||||
<span class="absolute left-1/2 top-1.5 inline-block h-1.5 w-0.5 -translate-x-1/2 animate-float-y rounded-full bg-electric"></span>
|
</div>
|
||||||
</span>
|
|
||||||
</a>
|
<!-- Social tile -->
|
||||||
|
<div class="tile reveal" style="transition-delay:.18s">
|
||||||
|
<span class="kicker @(fa ? "font-fa" : "")">@(fa ? "ارتباط" : "Connect")</span>
|
||||||
|
<div class="mt-auto flex items-center gap-2.5 pt-6">
|
||||||
|
<a class="social" href="https://www.linkedin.com/in/soroushdes/" target="_blank" rel="noopener" aria-label="LinkedIn"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5ZM3 9h4v12H3V9Zm6 0h3.8v1.64h.05c.53-1 1.83-2.06 3.76-2.06 4.02 0 4.76 2.65 4.76 6.1V21h-4v-5.4c0-1.29-.02-2.95-1.8-2.95-1.8 0-2.07 1.4-2.07 2.85V21H9V9Z"/></svg></a>
|
||||||
|
<a class="social" href="https://www.instagram.com/soroushasadicom/" target="_blank" rel="noopener" aria-label="Instagram"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.2" cy="6.8" r="1.1" fill="currentColor" stroke="none"/></svg></a>
|
||||||
|
<a class="social" href="mailto:code.soroush@gmail.com" aria-label="Email"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── SERVICES ─────────────────────────────────────────────────────── -->
|
<!-- ─── SERVICES (text-block grid) ───────────────────────────────────── -->
|
||||||
<section id="services" class="relative px-5 py-28 sm:px-8">
|
<section id="services" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||||
<div class="mx-auto max-w-7xl">
|
<div class="mx-auto max-w-6xl">
|
||||||
<div class="section-header">
|
<div class="sec-head">
|
||||||
<div class="eyebrow"><span class="chip">@(fa ? "خدمات" : "Services")</span></div>
|
<h2>@(fa ? "شش کاری که انجام میدهم" : "Six areas of practice")</h2>
|
||||||
<h2>@(fa ? "شش حوزه تخصصی" : "Six areas of practice")</h2>
|
<p class="lede">@(fa ? "از همان جلسهی اول تا وقتی محصول روی پای خودش میایستد، کنارتان هستم؛ در تمام مسیر مهندسی و محصول." : "From the first idea to production rollout, one engineering partner across the whole product.")</p>
|
||||||
<p>@(fa ? "از اولین جلسهی راهبرد تا استقرار تولید — یک شریک مهندسی برای کل چرخهی عمر هوش مصنوعی شما." : "From the first strategy session to production rollout — one engineering partner for the full AI lifecycle.")</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
|
<div class="bento">
|
||||||
@{
|
@{
|
||||||
var services = fa ? new[]{
|
var services = fa ? new[]{
|
||||||
("strategy","راهبرد و نقشه راه هوش مصنوعی","ارزیابی بلوغ سازمانی، شناسایی موارد کاربری با بیشترین بازده، و طراحی نقشه راه ۱۲–۱۸ ماهه با KPIهای روشن.","electric",new[]{"Discovery","ROI Mapping","Roadmap"}),
|
("apps","اپلیکیشنهای وب و سازمانی","پلتفرمهای وب و SaaS از صفر تا صد: داشبورد، چندمستاجری، صورتحساب و پنل مدیریت، ساختهشده برای رشد.",new[]{"Web","SaaS","Dashboards"}),
|
||||||
("automation","اتوماسیون هوش مصنوعی","ساخت عاملهای خودکار و گردشکارهای n8n که فرایندهای دستی را به سامانههای قابل ممیزی تبدیل میکنند.","violet",new[]{"n8n","Agents","Workflows"}),
|
("mobile","اپلیکیشنهای موبایل","اپهای بومی و چندسکویی با Flutter، Swift و Kotlin، با حسی روان و نزدیک به تجربهی بومی.",new[]{"Flutter","Swift","Kotlin"}),
|
||||||
("llm-rag","مهندسی LLM و RAG","طراحی pipelineهای RAG با پایگاههای برداری، evaluation framework، و سرویسدهی با تأخیر زیر ۵۰ میلیثانیه.","magenta",new[]{"RAG","Vector DB","Eval"}),
|
("architecture","معماری راهکار و زیرساخت ابری","سیستمهای توزیعشده روی Kubernetes؛ میکروسرویس، استریم رویداد، و پایداری زیر بار سنگین.",new[]{"K8s","Microservices","Cloud"}),
|
||||||
("architecture","معماری راهکار","طراحی سامانههای توزیعشده روی Kubernetes با میکروسرویسها، event streaming، و الگوهای پایداری در مقیاس بالا.","emerald",new[]{"K8s","Microservices","Event-Driven"}),
|
("llm-rag","راهکارهای هوش مصنوعی","قابلیتهای LLM و RAG، عاملها و اتوماسیون که داخل محصول واقعی کار میکنند، نه فقط دمو.",new[]{"LLM","RAG","Agents"}),
|
||||||
("mobile","اپلیکیشنهای موبایل هوش مصنوعی","برنامههای Flutter، Swift و Kotlin با on-device inference، استریم LLM و تجربهی کاربری بومی.","electric",new[]{"Flutter","Swift","Kotlin"}),
|
("automation","اتوماسیون و یکپارچهسازی","ابزارهایتان را به هم وصل میکنم و کارهای دستی را حذف؛ با گردشکارهای n8n، API و وبهوک.",new[]{"n8n","APIs","Webhooks"}),
|
||||||
("google-stack","تخصص استک گوگل","استقرار روی Vertex AI، GKE و Gemini با بهینهسازی هزینه و الگوهای امنیتی سطح enterprise.","cyan",new[]{"Vertex AI","GKE","Gemini"}),
|
("strategy","راهبرد و نقشهی راه","راهبرد فنی، بازبینی معماری، و نقشهی راهی روشن از ایده تا عرضه.",new[]{"Discovery","Architecture","Roadmap"}),
|
||||||
} : new[]{
|
} : new[]{
|
||||||
("strategy","AI Strategy & Roadmap","Maturity assessment, highest-ROI use-case discovery, and a 12–18 month roadmap with measurable KPIs.","electric",new[]{"Discovery","ROI Mapping","Roadmap"}),
|
("apps","Web & enterprise apps","End-to-end web platforms and SaaS: dashboards, multi-tenant, billing, and admin, built to scale.",new[]{"Web","SaaS","Dashboards"}),
|
||||||
("automation","AI Automation","Autonomous agents and n8n workflows that turn manual processes into auditable, observable systems.","violet",new[]{"n8n","Agents","Workflows"}),
|
("mobile","Mobile apps","Native and cross-platform apps with Flutter, Swift, and Kotlin, with a smooth native feel.",new[]{"Flutter","Swift","Kotlin"}),
|
||||||
("llm-rag","LLM & RAG Engineering","Production RAG pipelines with vector stores, evaluation frameworks, and sub-50ms serving.","magenta",new[]{"RAG","Vector DB","Eval"}),
|
("architecture","Solution architecture & cloud","Distributed systems on Kubernetes: microservices, event streaming, and resilience at scale.",new[]{"K8s","Microservices","Cloud"}),
|
||||||
("architecture","Solution Architecture","Distributed systems on Kubernetes — microservices, event streaming, and resilience patterns at scale.","emerald",new[]{"K8s","Microservices","Event-Driven"}),
|
("llm-rag","AI solutions","LLM and RAG features, agents, and automation built into real products, not just demos.",new[]{"LLM","RAG","Agents"}),
|
||||||
("mobile","Mobile AI Apps","Flutter, Swift, and Kotlin apps with on-device inference, streaming LLM UX, and native polish.","electric",new[]{"Flutter","Swift","Kotlin"}),
|
("automation","Automation & integrations","Connect your tools and remove manual work with n8n workflows, APIs, and webhooks.",new[]{"n8n","APIs","Webhooks"}),
|
||||||
("google-stack","Google Stack Specialist","Vertex AI, GKE, and Gemini deployments with cost optimization and enterprise security patterns.","cyan",new[]{"Vertex AI","GKE","Gemini"}),
|
("strategy","Strategy & roadmap","Technical strategy, architecture review, and a clear roadmap from idea to launch.",new[]{"Discovery","Architecture","Roadmap"}),
|
||||||
};
|
};
|
||||||
int si = 0;
|
|
||||||
}
|
}
|
||||||
@foreach (var (id, title, desc, color, tags) in services)
|
@{ int si = 0; }
|
||||||
|
@foreach (var (id, title, desc, tags) in services)
|
||||||
{
|
{
|
||||||
var (ringCls, glowCls, textCls, chipCls) = color switch {
|
var (spanCls, variant) = id switch {
|
||||||
"violet" => ("group-hover:border-violet/50", "group-hover:shadow-glow-violet", "text-violet", "border-violet/30 bg-violet/5 text-violet/90"),
|
"apps" => ("span-2", "tile-tint"),
|
||||||
"magenta" => ("group-hover:border-magenta/50", "group-hover:shadow-glow-magenta", "text-magenta", "border-magenta/30 bg-magenta/5 text-magenta/90"),
|
"llm-rag" => ("span-2", "tile-dark"),
|
||||||
"emerald" => ("group-hover:border-emerald/50", "group-hover:shadow-glow-emerald", "text-emerald", "border-emerald/30 bg-emerald/5 text-emerald/90"),
|
_ => ("", ""),
|
||||||
"cyan" => ("group-hover:border-cyan/50", "group-hover:shadow-glow-electric","text-cyan", "border-cyan/30 bg-cyan/5 text-cyan/90"),
|
|
||||||
_ => ("group-hover:border-electric/50","group-hover:shadow-glow-electric","text-electric","border-electric/30 bg-electric/5 text-electric/90"),
|
|
||||||
};
|
};
|
||||||
<article class="group relative isolate overflow-hidden p-6 sm:p-7 glass service-card reveal @ringCls @glowCls" style="transition-delay:@(si * 50)ms">
|
var descCls = variant == "tile-dark" ? "t-sub" : "text-zinc-600";
|
||||||
<div class="flex items-start justify-between">
|
<article class="tile reveal @spanCls @variant" style="transition-delay:@(si * 60)ms">
|
||||||
<span class="label-mono">@((si + 1).ToString("D2"))</span>
|
<span class="tile-icon" aria-hidden="true">@Html.Raw(ServiceIcon(id))</span>
|
||||||
<span class="@textCls">
|
<h3 class="mt-4 text-lg font-semibold @(fa ? "font-fa" : "")">@title</h3>
|
||||||
@Html.Raw(ServiceIcon(id))
|
<p class="mt-2.5 text-[.93rem] leading-relaxed @descCls">@desc</p>
|
||||||
</span>
|
<div class="mt-auto flex flex-wrap gap-1.5 pt-5">
|
||||||
|
@foreach (var tag in tags) { <span class="chip">@tag</span> }
|
||||||
</div>
|
</div>
|
||||||
<h3 class="mt-6 font-display font-semibold leading-snug text-white @(fa ? "font-fa" : "")" style="font-size:clamp(1.15rem,1.8vw,1.4rem)">@title</h3>
|
|
||||||
<p class="mt-3 text-[.94rem] leading-relaxed text-slate-400">@desc</p>
|
|
||||||
<div class="mt-5 flex flex-wrap gap-1.5">
|
|
||||||
@foreach (var tag in tags)
|
|
||||||
{
|
|
||||||
<span class="rounded-full border px-2.5 py-0.5 font-mono text-[.65rem] uppercase tracking-wider @chipCls">@tag</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<span aria-hidden class="absolute inset-x-6 bottom-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"></span>
|
|
||||||
</article>
|
</article>
|
||||||
si++;
|
si++;
|
||||||
}
|
}
|
||||||
@@ -168,76 +100,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── DATA FLOW ────────────────────────────────────────────────────── -->
|
<!-- ─── PIPELINE (horizontal stepper) ────────────────────────────────── -->
|
||||||
<section id="dataflow" class="relative overflow-hidden px-5 py-28 sm:px-8">
|
<section id="dataflow" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||||
<div class="mx-auto max-w-5xl">
|
<div class="mx-auto max-w-6xl">
|
||||||
<div class="section-header">
|
<div class="sec-head">
|
||||||
<div class="eyebrow"><span class="chip">@(fa ? "پایپلاین" : "Pipeline")</span></div>
|
<h2>@(fa ? "از سند خام تا پاسخ قابل اتکا" : "From raw document to a trustworthy answer")</h2>
|
||||||
<h2>@(fa ? "از سند خام تا پاسخ قابل اتکا" : "From raw document to trustworthy answer")</h2>
|
<p class="lede">@(fa ? "مسیری که هر پرسش در یک سامانهی RAG واقعی طی میکند. هر مرحله را میشود اندازه گرفت، دنبال کرد و برای سرعت بهتر کرد." : "The path every query takes through a production RAG system. Each stage is measurable, auditable, and tuned for latency.")</p>
|
||||||
<p>@(fa ? "مسیری که هر پرسش در یک سامانهی RAG تولیدی طی میکند — هر مرحله قابل اندازهگیری، قابل ممیزی و بهینهشده برای تأخیر." : "The path every query takes through a production RAG system — each stage measurable, auditable, and tuned for latency.")</p>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Flow diagram -->
|
|
||||||
<div class="reveal mt-10 overflow-x-auto">
|
<ol class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-3 lg:grid-cols-5">
|
||||||
<div class="flex min-w-max items-center gap-0 mx-auto w-fit">
|
|
||||||
@{
|
@{
|
||||||
var nodes = fa ? new[]{
|
var nodes = fa ? new[]{
|
||||||
("ingest","دریافت","نرمالسازی، قطعهبندی و پاکسازی اسناد منبع","electric"),
|
("دریافت","نرمالسازی، تکهتکهکردن و پاکسازی سندهای منبع"),
|
||||||
("embed","برداریسازی","تولید embedding و نمایهسازی در پایگاه برداری","violet"),
|
("برداریسازی","ساخت embedding و نمایهکردن در پایگاه برداری"),
|
||||||
("retrieve","بازیابی","جستجوی ترکیبی معنایی و کلیدواژهای","cyan"),
|
("بازیابی","جستجوی ترکیبی معنایی و کلیدواژهای"),
|
||||||
("rerank","بازرتبهبندی","مرتبسازی مجدد نامزدها با cross-encoder","magenta"),
|
("بازرتبهبندی","چیدن دوبارهی نتایج با cross-encoder"),
|
||||||
("generate","تولید","پاسخ مستند با ارجاع به منبع","emerald"),
|
("تولید","پاسخ مستند همراه با ذکر منبع"),
|
||||||
} : new[]{
|
} : new[]{
|
||||||
("ingest","Ingest","Normalize, chunk, and clean source documents","electric"),
|
("Ingest","Normalize, chunk, and clean source documents"),
|
||||||
("embed","Embed","Generate embeddings and index in vector store","violet"),
|
("Embed","Generate embeddings and index in the vector store"),
|
||||||
("retrieve","Retrieve","Hybrid semantic + keyword search","cyan"),
|
("Retrieve","Hybrid semantic and keyword search"),
|
||||||
("rerank","Rerank","Re-order candidates with a cross-encoder","magenta"),
|
("Rerank","Re-order candidates with a cross-encoder"),
|
||||||
("generate","Generate","Grounded answer with source citations","emerald"),
|
("Generate","Grounded answer with source citations"),
|
||||||
};
|
|
||||||
var colorMap2 = new Dictionary<string,(string border, string text, string bg)>{
|
|
||||||
["electric"] = ("border-electric/40","text-electric","bg-electric/10"),
|
|
||||||
["violet"] = ("border-violet/40", "text-violet", "bg-violet/10"),
|
|
||||||
["cyan"] = ("border-cyan/40", "text-cyan", "bg-cyan/10"),
|
|
||||||
["magenta"] = ("border-magenta/40", "text-magenta", "bg-magenta/10"),
|
|
||||||
["emerald"] = ("border-emerald/40", "text-emerald", "bg-emerald/10"),
|
|
||||||
};
|
};
|
||||||
|
int stepN = 0;
|
||||||
}
|
}
|
||||||
@for (int ni = 0; ni < nodes.Length; ni++)
|
@foreach (var (nlabel, ndesc) in nodes)
|
||||||
{
|
{
|
||||||
var (nid, nlabel, ndesc, naccent) = nodes[ni];
|
stepN++;
|
||||||
var (nborder, ntext, nbg) = colorMap2[naccent];
|
<li class="reveal border-t border-zinc-200 pt-4" style="transition-delay:@((stepN-1) * 40)ms">
|
||||||
<div class="flex flex-col items-center">
|
<span class="font-display text-sm text-zinc-500">@stepN.ToString("D2")</span>
|
||||||
<div class="glass @nborder @nbg flex flex-col items-center gap-2 rounded-2xl border p-5 text-center w-40">
|
<h3 class="mt-2 text-base font-semibold @(fa ? "font-fa" : "")">@nlabel</h3>
|
||||||
<span class="label-mono @ntext">@nlabel</span>
|
<p class="mt-1.5 text-[.85rem] leading-relaxed text-zinc-600">@ndesc</p>
|
||||||
<p class="text-[.72rem] leading-snug text-slate-400">@ndesc</p>
|
</li>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
if (ni < nodes.Length - 1)
|
|
||||||
{
|
|
||||||
<div class="flex items-center px-2">
|
|
||||||
<svg width="40" height="16" viewBox="0 0 40 16" fill="none" aria-hidden="true">
|
|
||||||
<line x1="0" y1="8" x2="32" y2="8" stroke="rgba(56,189,248,0.4)" stroke-width="1.5" stroke-dasharray="4 3">
|
|
||||||
<animate attributeName="stroke-dashoffset" values="0;-14" dur="1.1s" repeatCount="indefinite"/>
|
|
||||||
</line>
|
|
||||||
<polygon points="32,4 40,8 32,12" fill="rgba(56,189,248,0.6)"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
</ol>
|
||||||
</div>
|
<p class="mt-8 text-sm text-zinc-500">@(fa ? "تأخیر کل زیر ۵۰ میلیثانیه؛ هر مرحله قابل مشاهده." : "Sub-50ms end-to-end, every stage observable.")</p>
|
||||||
</div>
|
|
||||||
<p class="mt-8 text-center label-mono text-slate-500 reveal">@(fa ? "تأخیر سرتاسری زیر ۵۰ میلیثانیه · هر مرحله مشاهدهپذیر" : "Sub-50ms end-to-end · every stage observable")</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── STACK ─────────────────────────────────────────────────────────── -->
|
<!-- ─── STACK (grouped tag clusters) ─────────────────────────────────── -->
|
||||||
<section id="stack" class="relative px-5 py-28 sm:px-8">
|
<section id="stack" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||||
<div class="mx-auto max-w-7xl">
|
<div class="mx-auto max-w-6xl">
|
||||||
<div class="section-header">
|
<div class="sec-head">
|
||||||
<div class="eyebrow"><span class="chip">@(fa ? "استک" : "Stack")</span></div>
|
<h2>@(fa ? "ابزار روزمره" : "Daily tooling")</h2>
|
||||||
<h2>@(fa ? "ابزارهای روزانه" : "Daily tooling")</h2>
|
<p class="lede">@(fa ? "هر چیزی که میسازم روی اینها بنا میشود؛ انتخابشان کردهام چون میمانند، نه چون مد روزند." : "Everything I ship sits on this foundation, chosen for longevity, not hype cycles.")</p>
|
||||||
<p>@(fa ? "هر چه ساخته میشود از این پایهها بیرون میآید — انتخابشده برای عمر طولانی، نه ترند روز." : "Everything I ship sits on this foundation — chosen for longevity, not hype cycles.")</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4 reveal">
|
|
||||||
|
<div class="bento">
|
||||||
@{
|
@{
|
||||||
var cats = fa ? new[]{
|
var cats = fa ? new[]{
|
||||||
("زبانها", new[]{"Python","TypeScript","Go","Rust","SQL"}),
|
("زبانها", new[]{"Python","TypeScript","Go","Rust","SQL"}),
|
||||||
@@ -250,201 +160,153 @@
|
|||||||
("Infrastructure",new[]{"Kubernetes","Terraform","Postgres","Redis","Kafka","NATS"}),
|
("Infrastructure",new[]{"Kubernetes","Terraform","Postgres","Redis","Kafka","NATS"}),
|
||||||
("AI / ML", new[]{"Vertex AI","Gemini","OpenAI","Anthropic","LangGraph","Pinecone","pgvector"}),
|
("AI / ML", new[]{"Vertex AI","Gemini","OpenAI","Anthropic","LangGraph","Pinecone","pgvector"}),
|
||||||
};
|
};
|
||||||
string[] catColors = ["text-electric","text-violet","text-emerald","text-magenta"];
|
|
||||||
int ci2 = 0;
|
|
||||||
}
|
}
|
||||||
|
@{ int ci = 0; }
|
||||||
@foreach (var (catLabel, items) in cats)
|
@foreach (var (catLabel, items) in cats)
|
||||||
{
|
{
|
||||||
<div class="glass p-6">
|
<div class="tile reveal @(ci == 3 ? "tile-tint" : "")" style="transition-delay:@(ci * 60)ms">
|
||||||
<h3 class="font-display font-semibold text-white mb-4 @catColors[ci2]">@catLabel</h3>
|
<h3 class="text-sm font-semibold @(fa ? "font-fa" : "")">@catLabel</h3>
|
||||||
<ul class="space-y-2">
|
<div class="mt-4 flex flex-wrap gap-1.5">
|
||||||
@foreach (var item in items)
|
@foreach (var item in items) { <span class="chip">@item</span> }
|
||||||
{
|
|
||||||
<li class="flex items-center gap-2 text-[.9rem] text-slate-300">
|
|
||||||
<span class="h-1 w-1 rounded-full @catColors[ci2].Replace("text-","bg-")" aria-hidden></span>
|
|
||||||
@item
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
ci2++;
|
</div>
|
||||||
|
ci++;
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── EXPERTISE ────────────────────────────────────────────────────── -->
|
<!-- ─── EXPERTISE (definition list) ──────────────────────────────────── -->
|
||||||
<section id="expertise" class="relative px-5 py-28 sm:px-8">
|
<section id="expertise" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||||
<div class="mx-auto max-w-3xl">
|
<div class="mx-auto max-w-4xl">
|
||||||
<div class="section-header">
|
<div class="sec-head">
|
||||||
<div class="eyebrow"><span class="chip">@(fa ? "تخصص" : "Expertise")</span></div>
|
<h2>@(fa ? "جاهایی که عمیق شدهام" : "What I go deep on")</h2>
|
||||||
<h2>@(fa ? "اعدادی که اهمیت دارند" : "The numbers that matter")</h2>
|
<p class="lede">@(fa ? "سامانههایی که روزانه میلیونها رویداد را تاب میآورند. اینها همان چیزهاییاند که سالها رویشان کار کردهام." : "Systems that survive millions of events per day. These are the areas I optimize for.")</p>
|
||||||
<p>@(fa ? "سامانههایی که در میلیونها رویداد در روز پایدار میمانند — اینها معیارهایی هستند که اندازه میگیریم." : "Systems that survive millions of events per day — these are the metrics I optimize for.")</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-6 reveal">
|
|
||||||
|
<dl>
|
||||||
@{
|
@{
|
||||||
var bars = fa ? new[]{
|
var areas = fa ? new[]{
|
||||||
("مهندسی LLM و RAG", 95),
|
("معماری نرمافزار و سیستمهای توزیعشده","میکروسرویس، استریم رویداد و الگوهای پایداری زیر بار سنگین."),
|
||||||
("معماری ابری و Kubernetes", 92),
|
("اپلیکیشنهای وب و سازمانی","پلتفرمهای چندمستاجری، داشبورد و سیستمهای پرترافیک."),
|
||||||
("سیستمهای عاملمحور و اتوماسیون", 90),
|
("راهکارهای هوش مصنوعی (LLM و RAG)","بازیابی، ارزیابی و تولید پاسخ مستند، داخل محصول واقعی."),
|
||||||
("استک گوگل کلود (Vertex / GKE)", 88),
|
("زیرساخت ابری و Kubernetes","استقرار، مقیاسپذیری خودکار و حواسجمعی روی هزینه."),
|
||||||
("موبایل بومی و cross-platform", 82),
|
("موبایل بومی و چندسکویی","Flutter، Swift و Kotlin برای اپهای روان و سریع."),
|
||||||
} : new[]{
|
} : new[]{
|
||||||
("LLM & RAG engineering", 95),
|
("Software architecture & distributed systems","Microservices, event streaming, and resilience patterns at scale."),
|
||||||
("Cloud architecture & Kubernetes", 92),
|
("Web & enterprise applications","Multi-tenant platforms, dashboards, and high-traffic systems."),
|
||||||
("Agentic systems & automation", 90),
|
("AI solutions (LLM & RAG)","Retrieval, evals, and grounded generation, inside real products."),
|
||||||
("Google Cloud stack (Vertex / GKE)", 88),
|
("Cloud infrastructure & Kubernetes","Deployment, autoscaling, and real cost discipline."),
|
||||||
("Native + cross-platform mobile", 82),
|
("Native & cross-platform mobile","Flutter, Swift, and Kotlin for smooth, fast apps."),
|
||||||
};
|
};
|
||||||
string[] barColors = ["bg-electric","bg-violet","bg-cyan","bg-magenta","bg-emerald"];
|
|
||||||
int bi = 0;
|
|
||||||
}
|
}
|
||||||
@foreach (var (blabel, bval) in bars)
|
@foreach (var (alabel, adesc) in areas)
|
||||||
{
|
{
|
||||||
<div>
|
<div class="reveal grid grid-cols-1 gap-1 border-t border-zinc-200 py-5 sm:grid-cols-[1fr_1.5fr] sm:gap-8">
|
||||||
<div class="mb-2 flex items-center justify-between">
|
<dt class="text-base font-semibold @(fa ? "font-fa" : "")">@alabel</dt>
|
||||||
<span class="text-[.9rem] text-slate-300">@blabel</span>
|
<dd class="text-[.95rem] leading-relaxed text-zinc-600">@adesc</dd>
|
||||||
<span class="label-mono text-slate-400">@bval%</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bar-track">
|
|
||||||
<div class="bar-fill @barColors[bi]" data-w="@bval%" style="width:0%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
bi++;
|
|
||||||
}
|
}
|
||||||
</div>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── PORTFOLIO ────────────────────────────────────────────────────── -->
|
<!-- ─── PORTFOLIO (card grid, typographic covers) ────────────────────── -->
|
||||||
<section id="portfolio" class="relative px-5 py-28 sm:px-8">
|
<section id="portfolio" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||||
<div class="mx-auto max-w-7xl">
|
<div class="mx-auto max-w-6xl">
|
||||||
<div class="section-header">
|
<div class="sec-head">
|
||||||
<div class="eyebrow"><span class="chip">@(fa ? "نمونهکارها" : "Selected work")</span></div>
|
<h2>@(fa ? "نمونهکارهای منتخب" : "Selected work")</h2>
|
||||||
<h2>@(fa ? "سامانههایی که در تولید کار میکنند" : "Systems that run in production")</h2>
|
<p class="lede">@(fa ? "محصولاتی که خودم طراحی و ساختهام. روی هر کارت بزنید تا خودِ سایت را ببینید." : "Products I have designed and built. Tap any card to open the live site.")</p>
|
||||||
<p>@(fa ? "گزیدهای از پروژههای واقعی. روی هر کارت بزنید تا جزئیات معماری را ببینید." : "A selection of real engagements. Tap any card for the gallery and architecture details.")</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
<div class="bento">
|
||||||
@{
|
@{
|
||||||
var projects = fa ? new[]{
|
var projects = fa ? new[]{
|
||||||
("atlas-rag","اطلس — پلتفرم RAG سازمانی","بانک ردیفاول","مهندس ارشد هوش مصنوعی","۲۰۲۵","دستیار دانش روی بیش از ۴ میلیون سند داخلی؛ بازیابی ترکیبی با pgvector و reranker.","electric",new[]{"RAG","pgvector","Vertex AI","Eval"},new[]{("۴M+","سند نمایهشده"),("۳۸ms","تأخیر p95"),("۹۲٪","دقت پاسخ")},"/portfolio/atlas-rag/cover.svg",new[]{"/portfolio/atlas-rag/01.svg","/portfolio/atlas-rag/02.svg","/portfolio/atlas-rag/03.svg"}),
|
("hamkadr","همکادر","hamkadr.ir","بازاری که کادر درمان را برای شیفت و استخدام به بیمارستانها و کلینیکها وصل میکند؛ با پروفایل، فیلتر، تقویم هفتگی و اپ موبایل.",new[]{"Marketplace","Healthcare","Mobile"}),
|
||||||
("sentinel-agents","Sentinel — اتوماسیون Ops عاملمحور","SaaS scale-up","معمار راهکار","۲۰۲۵","پاسخ خودکار به حوادث با ترکیب n8n و LangGraph — عاملهای قابل ممیزی که alert تریاژ میکنند.","violet",new[]{"n8n","LangGraph","Agents"},new[]{("۷۰٪","کاهش MTTR"),("۲۴/۷","پوشش on-call"),("۱۵۰+","جریان خودکار")},"/portfolio/sentinel-agents/cover.svg",new[]{"/portfolio/sentinel-agents/01.svg","/portfolio/sentinel-agents/02.svg","/portfolio/sentinel-agents/03.svg"}),
|
("meezi","میزی","meezi.ir","سامانهی یکپارچه برای کافه و رستوران: سفارش با QR، صندوق فروش، انبار، کارکنان و تحلیل فروش، روی زیرساخت داخلی.",new[]{"SaaS","POS","Analytics"}),
|
||||||
("vertex-vision","Vertex Vision — استنتاج بینایی بلادرنگ","زنجیره خردهفروشی","مهندس هوش مصنوعی","۲۰۲۴","استنتاج بینایی بلادرنگ روی GKE با Triton و Vertex AI برای تحلیل قفسه و جریان مشتری.","cyan",new[]{"Vertex AI","GKE","Triton"},new[]{("۱.۲B","استنتاج ماهانه"),("۳۰۰+","فروشگاه"),("۶۰٪","کاهش هزینه GPU")},"/portfolio/vertex-vision/cover.svg",new[]{"/portfolio/vertex-vision/01.svg","/portfolio/vertex-vision/02.svg","/portfolio/vertex-vision/03.svg"}),
|
("bargevasat","برگ وسط","bargevasat.ir","بازی آنلاین حکم بهصورت بلادرنگ مقابل بازیکنان واقعی یا رباتهای هوشمند؛ با لیگ، رتبهبندی، جایزهی روزانه و همگامسازی چنددستگاهه.",new[]{"Realtime","Multiplayer","Game"}),
|
||||||
("mirage-mobile","Mirage — مجموعه هوش مصنوعی on-device","محصول مصرفی","رهبر موبایل + هوش مصنوعی","۲۰۲۴","اپلیکیشن Flutter با استنتاج کاملاً آفلاین با Gemini Nano و LiteRT.","magenta",new[]{"Flutter","Gemini Nano","LiteRT"},new[]{("۰","وابستگی شبکه"),("<80ms","پاسخ"),("۴.۸★","امتیاز کاربران")},"/portfolio/mirage-mobile/cover.svg",new[]{"/portfolio/mirage-mobile/01.svg","/portfolio/mirage-mobile/02.svg","/portfolio/mirage-mobile/03.svg"}),
|
("flatrender","فلترندر","flatrender.ir","استودیوی هوش مصنوعی که بیش از ۱۲۰۰ قالب را در چند دقیقه به ویدیو و تصویر آمادهی انتشار تبدیل میکند؛ بر پایهی ثانیهی رندر. در نسخهی بتا.",new[]{"AI","Video","SaaS"}),
|
||||||
("flux-stream","Flux — مش داده رویدادمحور","پلتفرم لجستیک","معمار پلتفرم","۲۰۲۳","ستون استریمینگ روی Kafka و NATS روی Kubernetes — ۴۰+ میکروسرویس با الگوهای پایداری.","emerald",new[]{"Kafka","NATS","Kubernetes","Go"},new[]{("۴۰+","میکروسرویس"),("۲M/s","رویداد در ثانیه"),("۹۹.۹٪","uptime")},"/portfolio/flux-stream/cover.svg",new[]{"/portfolio/flux-stream/01.svg","/portfolio/flux-stream/02.svg","/portfolio/flux-stream/03.svg"}),
|
|
||||||
("oracle-forecast","Oracle — موتور پیشبینی تقاضا","زنجیره تامین","مهندس ML","۲۰۲۳","پایپلاین پیشبینی سری زمانی روی BigQuery و dbt با بازآموزی خودکار.","electric",new[]{"Forecasting","BigQuery","dbt","MLOps"},new[]{("۲۳٪","کاهش ضایعات"),("۸۹٪","دقت پیشبینی"),("روزانه","بازآموزی")},"/portfolio/oracle-forecast/cover.svg",new[]{"/portfolio/oracle-forecast/01.svg","/portfolio/oracle-forecast/02.svg","/portfolio/oracle-forecast/03.svg"}),
|
|
||||||
} : new[]{
|
} : new[]{
|
||||||
("atlas-rag","Atlas — Enterprise RAG Platform","Tier-1 bank","Lead AI Engineer","2025","A knowledge assistant over 4M+ internal documents — hybrid retrieval with pgvector and a reranker, sub-40ms serving on Vertex AI.","electric",new[]{"RAG","pgvector","Vertex AI","Eval"},new[]{("4M+","docs indexed"),("38ms","p95 latency"),("92%","answer accuracy")},"/portfolio/atlas-rag/cover.svg",new[]{"/portfolio/atlas-rag/01.svg","/portfolio/atlas-rag/02.svg","/portfolio/atlas-rag/03.svg"}),
|
("hamkadr","Hamkadr","hamkadr.ir","A marketplace connecting healthcare staff with hospitals and clinics for shifts and hiring, with profiles, filters, weekly scheduling, and a mobile app.",new[]{"Marketplace","Healthcare","Mobile"}),
|
||||||
("sentinel-agents","Sentinel — Agentic Ops Automation","SaaS scale-up","Solution Architect","2025","Autonomous incident response combining n8n and LangGraph — auditable agents that triage alerts and self-heal.","violet",new[]{"n8n","LangGraph","Agents"},new[]{("70%","MTTR reduction"),("24/7","on-call coverage"),("150+","automated flows")},"/portfolio/sentinel-agents/cover.svg",new[]{"/portfolio/sentinel-agents/01.svg","/portfolio/sentinel-agents/02.svg","/portfolio/sentinel-agents/03.svg"}),
|
("meezi","Meezi","meezi.ir","An all-in-one SaaS for cafes and restaurants: QR ordering, POS, inventory, staff, and sales analytics, hosted in Iran.",new[]{"SaaS","POS","Analytics"}),
|
||||||
("vertex-vision","Vertex Vision — Realtime Vision Inference","Retail chain","AI Engineer","2024","Real-time vision inference on GKE with Triton and Vertex AI for shelf analytics and customer flow across 300+ stores.","cyan",new[]{"Vertex AI","GKE","Triton"},new[]{("1.2B","inferences / mo"),("300+","stores"),("60%","GPU cost cut")},"/portfolio/vertex-vision/cover.svg",new[]{"/portfolio/vertex-vision/01.svg","/portfolio/vertex-vision/02.svg","/portfolio/vertex-vision/03.svg"}),
|
("bargevasat","Barge Vasat","bargevasat.ir","A real-time multiplayer Hokm card game against people or AI bots, with leagues, rankings, daily rewards, and cross-device play.",new[]{"Realtime","Multiplayer","Game"}),
|
||||||
("mirage-mobile","Mirage — On-device AI Suite","Consumer product","Mobile + AI Lead","2024","A Flutter app with fully offline inference via Gemini Nano and LiteRT — streaming response UX with zero network dependency.","magenta",new[]{"Flutter","Gemini Nano","LiteRT"},new[]{("0","network deps"),("<80ms","response"),("4.8★","user rating")},"/portfolio/mirage-mobile/cover.svg",new[]{"/portfolio/mirage-mobile/01.svg","/portfolio/mirage-mobile/02.svg","/portfolio/mirage-mobile/03.svg"}),
|
("flatrender","Flatrender","flatrender.ir","An AI studio that turns 1,200+ templates into platform-ready videos and images in minutes, billed by render-seconds. In beta.",new[]{"AI","Video","SaaS"}),
|
||||||
("flux-stream","Flux — Event-Driven Data Mesh","Logistics platform","Platform Architect","2023","Streaming backbone on Kafka and NATS over Kubernetes — 40+ microservices with resilience patterns and exactly-once delivery.","emerald",new[]{"Kafka","NATS","Kubernetes","Go"},new[]{("40+","microservices"),("2M/s","events / sec"),("99.9%","uptime")},"/portfolio/flux-stream/cover.svg",new[]{"/portfolio/flux-stream/01.svg","/portfolio/flux-stream/02.svg","/portfolio/flux-stream/03.svg"}),
|
|
||||||
("oracle-forecast","Oracle — Demand Forecasting Engine","Supply chain","ML Engineer","2023","Time-series forecasting pipeline on BigQuery and dbt with automated retraining — reduced inventory waste significantly.","electric",new[]{"Forecasting","BigQuery","dbt","MLOps"},new[]{("23%","waste reduction"),("89%","forecast accuracy"),("daily","retraining")},"/portfolio/oracle-forecast/cover.svg",new[]{"/portfolio/oracle-forecast/01.svg","/portfolio/oracle-forecast/02.svg","/portfolio/oracle-forecast/03.svg"}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@foreach (var (pid, ptitle, pclient, prole, pyear, psummary, paccent, ptags, pmetrics, pcover, pgallery) in projects)
|
@{ int pi = 0; }
|
||||||
|
@foreach (var (pid, pname, pdomain, pdesc, ptags) in projects)
|
||||||
{
|
{
|
||||||
var (pborder, ptext) = paccent switch {
|
var initial = char.ToUpperInvariant(pid[0]);
|
||||||
"violet" => ("border-violet/30", "text-violet"),
|
var (spanCls, coverBg, coverFg) = pi switch {
|
||||||
"cyan" => ("border-cyan/30", "text-cyan"),
|
0 => ("span-2 row-2", "#18181b", "#fafafa"),
|
||||||
"magenta" => ("border-magenta/30", "text-magenta"),
|
1 => ("span-2", "#2563eb", "#ffffff"),
|
||||||
"emerald" => ("border-emerald/30", "text-emerald"),
|
2 => ("", "#eff4ff", "#2563eb"),
|
||||||
_ => ("border-electric/30", "text-electric"),
|
_ => ("", "#f4f4f5", "#a1a1aa"),
|
||||||
};
|
};
|
||||||
var galleryJson = System.Text.Json.JsonSerializer.Serialize(pgallery);
|
<a href="https://@pdomain" target="_blank" rel="noopener" aria-label="@pname"
|
||||||
<div class="glass cursor-pointer select-none p-6 transition-all duration-300 hover:-translate-y-1 reveal @pborder"
|
class="group tile tile-link reveal @spanCls" style="padding:0;transition-delay:@(pi * 60)ms">
|
||||||
data-portfolio-card
|
<div class="pcover" style="background:@coverBg;@(pi == 0 ? "min-height:210px" : "min-height:104px")">
|
||||||
tabindex="0"
|
<span class="font-display font-bold" style="font-size:@(pi == 0 ? "5rem" : "2.6rem");color:@coverFg">@initial</span>
|
||||||
data-title="@ptitle"
|
|
||||||
data-summary="@psummary"
|
|
||||||
data-gallery="@galleryJson"
|
|
||||||
role="button"
|
|
||||||
aria-label="@ptitle">
|
|
||||||
<div class="mb-4 aspect-video w-full overflow-hidden rounded-xl bg-base-700">
|
|
||||||
<img src="@pcover" alt="@ptitle" class="h-full w-full object-cover" loading="lazy" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-1 flex-col p-5">
|
||||||
<div class="mb-3 flex flex-wrap gap-1.5">
|
<div class="mb-3 flex flex-wrap gap-1.5">
|
||||||
@foreach (var tag in ptags)
|
@foreach (var tag in ptags) { <span class="chip">@tag</span> }
|
||||||
{
|
|
||||||
<span class="rounded-full border @pborder px-2 py-0.5 font-mono text-[.62rem] uppercase tracking-wider @ptext/80">@tag</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<h3 class="font-display font-semibold text-white @(fa ? "font-fa" : "")" style="font-size:clamp(1rem,1.5vw,1.2rem)">@ptitle</h3>
|
|
||||||
<p class="mt-1 text-[.82rem] text-slate-400">@pclient · @pyear</p>
|
|
||||||
<div class="mt-4 grid grid-cols-3 gap-3 border-t border-white/5 pt-4">
|
|
||||||
@foreach (var (mv, ml) in pmetrics)
|
|
||||||
{
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="font-display font-bold @ptext text-lg">@mv</div>
|
|
||||||
<div class="text-[.68rem] text-slate-500">@ml</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
<h3 class="text-[1.1rem] font-semibold @(fa ? "font-fa" : "")">@pname</h3>
|
||||||
|
<p class="mt-1 text-[.78rem] text-zinc-500" dir="ltr">@pdomain</p>
|
||||||
|
<p class="mt-2.5 text-[.88rem] leading-relaxed text-zinc-600 @(pi == 0 ? "" : "line-clamp-3")">@pdesc</p>
|
||||||
|
<span class="arrow-link mt-auto pt-4">
|
||||||
|
@(fa ? "مشاهدهی سایت" : "Visit site")
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M8 7h9v9"/></svg>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
pi++;
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Portfolio modal -->
|
<!-- ─── BLOG (editorial list) ────────────────────────────────────────── -->
|
||||||
<div id="portfolio-modal" class="fixed inset-0 z-[100] hidden" role="dialog" aria-modal="true">
|
<section id="blog" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||||
<div id="modal-overlay" class="absolute inset-0 bg-black/80 backdrop-blur-sm"></div>
|
<div class="mx-auto max-w-4xl">
|
||||||
<div class="relative z-10 flex h-full items-center justify-center p-4">
|
<div class="sec-head">
|
||||||
<div class="glass w-full max-w-3xl max-h-[90vh] overflow-auto rounded-2xl p-6">
|
|
||||||
<div class="mb-4 flex items-start justify-between gap-4">
|
|
||||||
<h2 id="modal-title" class="font-display text-xl font-bold text-white"></h2>
|
|
||||||
<button id="modal-close" class="text-slate-400 hover:text-white transition-colors p-1" aria-label="Close">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4 aspect-video overflow-hidden rounded-xl bg-base-700">
|
|
||||||
<img id="modal-img" src="" alt="" class="h-full w-full object-contain" />
|
|
||||||
</div>
|
|
||||||
<div class="mb-4 flex justify-between gap-2">
|
|
||||||
<button id="modal-prev" class="btn-ghost text-sm py-2 px-4 disabled:opacity-30">@(fa ? "قبلی" : "Previous")</button>
|
|
||||||
<button id="modal-next" class="btn-ghost text-sm py-2 px-4 disabled:opacity-30">@(fa ? "بعدی" : "Next")</button>
|
|
||||||
</div>
|
|
||||||
<p id="modal-body" class="text-sm leading-relaxed text-slate-400"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ─── BLOG ──────────────────────────────────────────────────────────── -->
|
|
||||||
<section id="blog" class="relative px-5 py-28 sm:px-8">
|
|
||||||
<div class="mx-auto max-w-7xl">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="eyebrow"><span class="chip">@(fa ? "بلاگ" : "Journal")</span></div>
|
|
||||||
<h2>@(fa ? "یادداشتهای مهندسی" : "Engineering notes")</h2>
|
<h2>@(fa ? "یادداشتهای مهندسی" : "Engineering notes")</h2>
|
||||||
<p>@(fa ? "یافتهها از پروژههای واقعی — نه ترجمهی مقاله، نه فهرست hype." : "Findings from real engagements — not translated articles, not hype lists.")</p>
|
<p class="lede">@(fa ? "درسهایی از پروژههای واقعی. نه ترجمهی مقاله، نه شعار توخالی." : "Findings from real engagements. Not translated articles, not hype lists.")</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
|
<div class="border-b border-zinc-200">
|
||||||
@{
|
@{
|
||||||
var posts = fa ? new[]{
|
var posts = fa ? new[]{
|
||||||
("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در تولید کار میکند","چرا BLEU و ROUGE برای RAG ناکافیاند، و معیارهایی که در پروژههای واقعی تصمیم میسازند.",8),
|
("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در عمل جواب میدهد","چرا BLEU و ROUGE برای RAG کافی نیستند، و معیارهایی که واقعاً به تصمیم کمک میکنند.",8),
|
||||||
("agentic-n8n-patterns","Automation","الگوهای عاملمحور با n8n برای سازمان","چگونه n8n را با LangGraph ترکیب کنیم تا گردشکارهای قابل ممیزی بسازیم.",11),
|
("agentic-n8n-patterns","Automation","الگوهای عاملمحور با n8n برای سازمان","چطور n8n را با LangGraph ترکیب کنیم تا گردشکارهای خودکار و قابلردیابی بسازیم.",11),
|
||||||
("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه ضدالگو که در ۸۰٪ پروژههای Vertex میبینم، و چگونه ۶۰٪ هزینه را کاهش دادیم.",6),
|
("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه اشتباه رایج که در بیشتر پروژههای Vertex میبینم، و اینکه چطور ۶۰٪ هزینه را کم کردیم.",6),
|
||||||
("k8s-llm-inference","Infra","استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ میلیثانیه","الگوی استقرار با KEDA، GPU sharing، و request hedging برای سرویسدهی پایدار.",14),
|
("k8s-llm-inference","Infra","اجرای LLM روی Kubernetes با تأخیر زیر ۵۰ میلیثانیه","الگوی استقرار با KEDA، اشتراک GPU و request hedging برای سرویسدهی پایدار.",14),
|
||||||
("flutter-on-device-ai","Mobile","هوش مصنوعی on-device در Flutter","استفاده از Gemini Nano و LiteRT برای استنتاج آفلاین در اپلیکیشنهای موبایل.",9),
|
("flutter-on-device-ai","Mobile","هوش مصنوعی روی دستگاه در Flutter","استفاده از Gemini Nano و LiteRT برای پردازش آفلاین در اپهای موبایل.",9),
|
||||||
("enterprise-ai-roadmap","Strategy","نقشه راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای CTOها میسازم — از کشف موارد کاربری تا اولین استقرار تولید.",7),
|
("enterprise-ai-roadmap","Strategy","نقشهی راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای مدیران فنی میچینم؛ از پیدا کردن بهترین ایده تا اولین اجرای واقعی.",7),
|
||||||
} : new[]{
|
} : new[]{
|
||||||
("rag-eval-framework","LLM","A RAG evaluation framework that holds up in production","Why BLEU and ROUGE fall short for RAG, and the metrics that actually drive decisions in real projects.",8),
|
("rag-eval-framework","LLM","A RAG evaluation framework that holds up in production","Why BLEU and ROUGE fall short for RAG, and the metrics that actually drive decisions in real projects.",8),
|
||||||
("agentic-n8n-patterns","Automation","Agentic patterns with n8n for the enterprise","How to combine n8n with LangGraph to build auditable, debuggable autonomous workflows.",11),
|
("agentic-n8n-patterns","Automation","Agentic patterns with n8n for the enterprise","How to combine n8n with LangGraph to build auditable, debuggable autonomous workflows.",11),
|
||||||
("vertex-cost-control","Google Stack","Vertex AI cost control at scale","Three anti-patterns I see in 80% of Vertex projects — and how we cut 60% of monthly spend.",6),
|
("vertex-cost-control","Google Stack","Vertex AI cost control at scale","Three anti-patterns I see in 80% of Vertex projects, and how we cut 60% of monthly spend.",6),
|
||||||
("k8s-llm-inference","Infra","Sub-50ms LLM inference on Kubernetes","Deployment pattern with KEDA, GPU sharing, and request hedging for stable serving.",14),
|
("k8s-llm-inference","Infra","Sub-50ms LLM inference on Kubernetes","Deployment pattern with KEDA, GPU sharing, and request hedging for stable serving.",14),
|
||||||
("flutter-on-device-ai","Mobile","On-device AI in Flutter","Using Gemini Nano and LiteRT for offline inference inside mobile apps.",9),
|
("flutter-on-device-ai","Mobile","On-device AI in Flutter","Using Gemini Nano and LiteRT for offline inference inside mobile apps.",9),
|
||||||
("enterprise-ai-roadmap","Strategy","A 90-day enterprise AI roadmap","The framework I build for CTOs — from use-case discovery to first production deployment.",7),
|
("enterprise-ai-roadmap","Strategy","A 90-day enterprise AI roadmap","The framework I build for CTOs, from use-case discovery to first production deployment.",7),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@foreach (var (slug, cat, btitle, excerpt, readTime) in posts)
|
@foreach (var (slug, cat, btitle, excerpt, readTime) in posts)
|
||||||
{
|
{
|
||||||
<a href="/blog/@slug" class="group glass block p-6 transition-all duration-300 hover:-translate-y-1 hover:border-electric/40 reveal">
|
<a href="/blog/@slug" class="group reveal grid grid-cols-1 gap-2 border-t border-zinc-200 py-6 sm:grid-cols-[8rem_1fr] sm:gap-8">
|
||||||
<span class="label-mono text-electric mb-3 block">@cat</span>
|
<div class="flex items-baseline justify-between sm:flex-col sm:gap-1">
|
||||||
<h3 class="font-display font-semibold leading-snug text-white group-hover:text-electric transition-colors @(fa ? "font-fa" : "")" style="font-size:clamp(1rem,1.4vw,1.15rem)">@btitle</h3>
|
<span class="kicker">@cat</span>
|
||||||
<p class="mt-3 text-[.88rem] leading-relaxed text-slate-400 line-clamp-3">@excerpt</p>
|
<span class="text-[.78rem] text-zinc-500">@readTime @(fa ? "دقیقه" : "min")</span>
|
||||||
<div class="mt-4 flex items-center justify-between">
|
</div>
|
||||||
<span class="label-mono">@readTime @(fa ? "دقیقه" : "min") @(fa ? "ادامه" : "read")</span>
|
<div>
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="text-electric @(fa ? "rotate-180" : "")"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
<h3 class="text-[1.1rem] font-semibold transition-colors group-hover:text-accent @(fa ? "font-fa" : "")">@btitle</h3>
|
||||||
|
<p class="mt-1.5 text-[.9rem] leading-relaxed text-zinc-600">@excerpt</p>
|
||||||
|
<span class="arrow-link mt-3">
|
||||||
|
@(fa ? "خواندن" : "Read")
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="@(fa ? "rotate-180" : "")" aria-hidden="true"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -452,99 +314,86 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── CONTACT ───────────────────────────────────────────────────────── -->
|
<!-- ─── CONTACT ──────────────────────────────────────────────────────── -->
|
||||||
<section id="contact" class="relative px-5 py-28 sm:px-8">
|
<section id="contact" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||||
<div class="mx-auto max-w-2xl">
|
<div class="mx-auto max-w-2xl">
|
||||||
<div class="section-header">
|
<div class="sec-head">
|
||||||
<div class="eyebrow"><span class="chip">@(fa ? "تماس" : "Contact")</span></div>
|
<h2>@(fa ? "رزرو یک جلسهی ۳۰ دقیقهای" : "Book a 30-minute call")</h2>
|
||||||
<h2>@(fa ? "رزرو یک جلسه ۳۰ دقیقهای" : "Book a 30-minute call")</h2>
|
<p class="lede">@(fa ? "بدون هزینه، بدون تعهد. با هم میبینیم چه میخواهید، چه محدودیتهایی هست، و قدم بعد چیست." : "No cost, no commitment. We map the use case, the constraints, and the next step together.")</p>
|
||||||
<p>@(fa ? "بدون هزینه، بدون تعهد. موارد کاربردی، محدودیتها و گام بعدی را با هم بررسی میکنیم." : "No cost, no commitment. We map the use case, the constraints, and the next step together.")</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="contact-form"
|
<form id="contact-form" class="card space-y-5 p-6 sm:p-8"
|
||||||
class="glass p-8 space-y-5"
|
data-success-msg="@(fa ? "پیام رسید! معمولاً ظرف ۲۴ ساعت کاری جواب میدهم." : "Sent. Typical reply within 24 working hours.")"
|
||||||
data-success-msg="@(fa ? "پیام ارسال شد. معمولاً ظرف ۲۴ ساعت کاری پاسخ میدهم." : "Sent! Typical reply within 24 working hours.")"
|
data-error-msg="@(fa ? "یک مشکلی پیش آمد. لطفاً دوباره تلاش کنید." : "Something went wrong. Please try again.")">
|
||||||
data-error-msg="@(fa ? "خطایی رخ داد. لطفاً دوباره امتحان کنید." : "Something went wrong. Please try again.")">
|
|
||||||
<input type="hidden" name="locale" value="@locale" />
|
<input type="hidden" name="locale" value="@locale" />
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="label-mono mb-2 block" for="name">@(fa ? "نام" : "Name")</label>
|
<label class="flabel" for="name">@(fa ? "نام" : "Name")<span class="text-red-600" aria-hidden="true"> *</span></label>
|
||||||
<input id="name" name="name" type="text" required placeholder="@(fa ? "نام و نام خانوادگی" : "Full name")" class="w-full rounded-xl border border-white/10 bg-white/[.03] px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-electric/60 transition-colors" />
|
<input id="name" name="name" type="text" required autocomplete="name" placeholder="@(fa ? "نام و نام خانوادگی" : "Full name")" class="field" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="label-mono mb-2 block" for="company">@(fa ? "سازمان" : "Company")</label>
|
<label class="flabel" for="company">@(fa ? "سازمان" : "Company")</label>
|
||||||
<input id="company" name="company" type="text" placeholder="@(fa ? "نام سازمان" : "Organization")" class="w-full rounded-xl border border-white/10 bg-white/[.03] px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-electric/60 transition-colors" />
|
<input id="company" name="company" type="text" autocomplete="organization" placeholder="@(fa ? "نام سازمان" : "Organization")" class="field" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="label-mono mb-2 block" for="service">@(fa ? "خدمت" : "Service")</label>
|
<label class="flabel" for="service">@(fa ? "خدمت" : "Service")<span class="text-red-600" aria-hidden="true"> *</span></label>
|
||||||
<select id="service" name="service" required class="w-full rounded-xl border border-white/10 bg-base-800 px-4 py-3 text-sm text-white outline-none focus:border-electric/60 transition-colors">
|
<select id="service" name="service" required class="field">
|
||||||
<option value="" disabled selected>—</option>
|
<option value="" disabled selected>@(fa ? "انتخاب کنید" : "Select…")</option>
|
||||||
@if (fa)
|
@if (fa)
|
||||||
{
|
{
|
||||||
<option value="strategy">راهبرد و نقشه راه</option>
|
<option value="apps">اپلیکیشن وب و سازمانی</option>
|
||||||
<option value="automation">اتوماسیون هوش مصنوعی</option>
|
<option value="mobile">اپلیکیشن موبایل</option>
|
||||||
<option value="llm-rag">مهندسی LLM و RAG</option>
|
<option value="architecture">معماری و زیرساخت ابری</option>
|
||||||
<option value="architecture">معماری راهکار</option>
|
<option value="ai">راهکار هوش مصنوعی</option>
|
||||||
<option value="mobile">موبایل</option>
|
<option value="automation">اتوماسیون و یکپارچهسازی</option>
|
||||||
<option value="google-stack">استک گوگل</option>
|
<option value="strategy">راهبرد و نقشهی راه</option>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<option value="strategy">AI Strategy & Roadmap</option>
|
<option value="apps">Web & enterprise apps</option>
|
||||||
<option value="automation">AI Automation</option>
|
<option value="mobile">Mobile apps</option>
|
||||||
<option value="llm-rag">LLM & RAG Engineering</option>
|
<option value="architecture">Solution architecture & cloud</option>
|
||||||
<option value="architecture">Solution Architecture</option>
|
<option value="ai">AI solutions</option>
|
||||||
<option value="mobile">Mobile AI Apps</option>
|
<option value="automation">Automation & integrations</option>
|
||||||
<option value="google-stack">Google Stack</option>
|
<option value="strategy">Strategy & roadmap</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="label-mono mb-2 block" for="budget">@(fa ? "بودجه (تقریبی)" : "Budget (rough)")</label>
|
<label class="flabel" for="budget">@(fa ? "بودجه (تقریبی)" : "Budget (rough)")<span class="text-red-600" aria-hidden="true"> *</span></label>
|
||||||
<select id="budget" name="budget" required class="w-full rounded-xl border border-white/10 bg-base-800 px-4 py-3 text-sm text-white outline-none focus:border-electric/60 transition-colors">
|
<select id="budget" name="budget" required class="field">
|
||||||
<option value="" disabled selected>—</option>
|
<option value="" disabled selected>@(fa ? "انتخاب کنید" : "Select…")</option>
|
||||||
<option>Under $10k</option>
|
<option value="under-10k">@(fa ? "زیر ۱۰ هزار دلار" : "Under $10k")</option>
|
||||||
<option>$10k–$50k</option>
|
<option value="10-50k">@(fa ? "۱۰ تا ۵۰ هزار دلار" : "$10k - $50k")</option>
|
||||||
<option>$50k–$200k</option>
|
<option value="50-200k">@(fa ? "۵۰ تا ۲۰۰ هزار دلار" : "$50k - $200k")</option>
|
||||||
<option>$200k+</option>
|
<option value="200k-plus">@(fa ? "بیش از ۲۰۰ هزار دلار" : "$200k+")</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label-mono mb-2 block" for="message">@(fa ? "پیام" : "Message")</label>
|
<label class="flabel" for="message">@(fa ? "پیام" : "Message")<span class="text-red-600" aria-hidden="true"> *</span></label>
|
||||||
<textarea id="message" name="message" required rows="4" placeholder="@(fa ? "هدف، بازه زمانی، موانع فعلی…" : "Goal, timeline, current blockers…")" class="w-full rounded-xl border border-white/10 bg-white/[.03] px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-electric/60 transition-colors resize-none"></textarea>
|
<textarea id="message" name="message" required rows="4" placeholder="@(fa ? "هدف، بازهی زمانی، و چیزی که الان گیرتان انداخته…" : "Goal, timeline, current blockers…")" class="field resize-none"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn-primary w-full justify-center">
|
<button type="submit" class="btn w-full">@(fa ? "ارسال پیام" : "Send request")</button>
|
||||||
@(fa ? "ارسال درخواست" : "Send request")
|
<p id="contact-status" role="status" aria-live="polite" class="mt-1 text-sm text-zinc-500">@(fa ? "معمولاً ظرف ۲۴ ساعت کاری جواب میدهم." : "Typical reply within 24 working hours.")</p>
|
||||||
</button>
|
|
||||||
<p id="contact-status" class="text-center text-sm text-slate-500">@(fa ? "معمولاً ظرف ۲۴ ساعت کاری پاسخ میدهم." : "Typical reply within 24 working hours.")</p>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── FOOTER ────────────────────────────────────────────────────────── -->
|
|
||||||
<footer class="border-t border-white/5 px-5 py-10 sm:px-8">
|
|
||||||
<div class="mx-auto flex max-w-7xl flex-col items-center gap-3 text-center">
|
|
||||||
<img src="/logo-mark.svg" alt="" width="24" height="24" />
|
|
||||||
<p class="label-mono">@(fa ? "طراحیشده در تهران · ساختهشده برای سازمانها" : "Designed in Tehran · Built for the enterprise")</p>
|
|
||||||
<p class="text-[.78rem] text-slate-600">© 2026 Soroush Asadi. @(fa ? "تمام حقوق محفوظ است." : "All rights reserved.")</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
static string ServiceIcon(string id) => id switch {
|
static string ServiceIcon(string id) => id switch {
|
||||||
"strategy" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M9 20H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4"/><polyline points="9,9 4,9"/><polyline points="9,12 4,12"/><polyline points="9,15 4,15"/><rect x="9" y="2" width="6" height="6"/><path d="M15 8h4a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-4"/></svg>""",
|
"strategy" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M9 20H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4"/><polyline points="9,9 4,9"/><polyline points="9,12 4,12"/><polyline points="9,15 4,15"/><rect x="9" y="2" width="6" height="6"/><path d="M15 8h4a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-4"/></svg>""",
|
||||||
"automation" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.22 4.22l2.12 2.12M17.66 17.66l2.12 2.12M2 12h3M19 12h3M4.22 19.78l2.12-2.12M17.66 6.34l2.12-2.12"/></svg>""",
|
"automation" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.22 4.22l2.12 2.12M17.66 17.66l2.12 2.12M2 12h3M19 12h3M4.22 19.78l2.12-2.12M17.66 6.34l2.12-2.12"/></svg>""",
|
||||||
"llm-rag" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>""",
|
"llm-rag" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>""",
|
||||||
"architecture" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="2" y="3" width="6" height="6"/><rect x="16" y="3" width="6" height="6"/><rect x="9" y="15" width="6" height="6"/><path d="M5 9v3h14V9M12 12v3"/></svg>""",
|
"architecture" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="2" y="3" width="6" height="6"/><rect x="16" y="3" width="6" height="6"/><rect x="9" y="15" width="6" height="6"/><path d="M5 9v3h14V9M12 12v3"/></svg>""",
|
||||||
"mobile" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="5" y="2" width="14" height="20" rx="2"/><path d="M12 18h.01"/></svg>""",
|
"mobile" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="5" y="2" width="14" height="20" rx="2"/><path d="M12 18h.01"/></svg>""",
|
||||||
"google-stack" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>""",
|
"apps" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18"/><path d="M7 6.5h.01"/></svg>""",
|
||||||
_ => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="10"/></svg>""",
|
_ => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>""",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,9 @@
|
|||||||
@@font-face { font-family:'Syne'; src:url('/fonts/Syne-Variable.woff2') format('woff2'); font-weight:100 900; font-display:swap; }
|
@@font-face { font-family:'Syne'; src:url('/fonts/Syne-Variable.woff2') format('woff2'); font-weight:100 900; font-display:swap; }
|
||||||
@@font-face { font-family:'SpaceMono'; src:url('/fonts/SpaceMono-Regular.woff2') format('woff2'); font-display:swap; }
|
@@font-face { font-family:'SpaceMono'; src:url('/fonts/SpaceMono-Regular.woff2') format('woff2'); font-display:swap; }
|
||||||
</style>
|
</style>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<!-- Tailwind: prebuilt + purged admin stylesheet (`npm run build`). No runtime CDN. -->
|
||||||
<script>
|
<link rel="stylesheet" href="/css/tailwind-admin.css" asp-append-version="true" />
|
||||||
tailwind.config = {
|
<link rel="stylesheet" href="/css/site.css" asp-append-version="true" />
|
||||||
theme: { extend: {
|
|
||||||
colors: { base:{DEFAULT:'#020510',800:'#050a1a'}, electric:'#38bdf8', violet:'#818cf8', magenta:'#e879f9', emerald:'#34d399' },
|
|
||||||
fontFamily: { sans:['Syne','system-ui','sans-serif'], mono:['SpaceMono','monospace'] }
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<link rel="stylesheet" href="/css/site.css" />
|
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-base text-slate-200 antialiased">
|
<body class="min-h-screen bg-base text-slate-200 antialiased">
|
||||||
<div class="flex min-h-screen">
|
<div class="flex min-h-screen">
|
||||||
|
|||||||
+69
-87
@@ -1,11 +1,12 @@
|
|||||||
@{
|
@{
|
||||||
var locale = (string)(ViewData["Locale"] ?? "fa");
|
var locale = (string)(ViewData["Locale"] ?? "fa");
|
||||||
|
var fa = locale == "fa";
|
||||||
var isRtl = locale == "fa";
|
var isRtl = locale == "fa";
|
||||||
var dir = isRtl ? "rtl" : "ltr";
|
var dir = isRtl ? "rtl" : "ltr";
|
||||||
var lang = locale == "fa" ? "fa" : "en";
|
var lang = locale == "fa" ? "fa" : "en";
|
||||||
var title = (string?)ViewData["Title"] ?? (locale == "fa"
|
var title = (string?)ViewData["Title"] ?? (locale == "fa"
|
||||||
? "سروش اسعدی — مهندس هوش مصنوعی، مشاور، معمار راهکار"
|
? "سروش اسعدی - مهندس نرمافزار و هوش مصنوعی"
|
||||||
: "Soroush Asadi — AI Engineer, Consultant, Solution Architect");
|
: "Soroush Asadi - Software & AI Engineer");
|
||||||
}
|
}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="@lang" dir="@dir">
|
<html lang="@lang" dir="@dir">
|
||||||
@@ -14,89 +15,42 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>@title</title>
|
<title>@title</title>
|
||||||
<meta name="description" content="@(locale == "fa"
|
<meta name="description" content="@(locale == "fa"
|
||||||
? "طراحی و پیادهسازی سامانههای هوش مصنوعی در مقیاس سازمانی — راهبرد، LLM و RAG، اتوماسیون عاملمحور، زیرساخت ابری و استک گوگل."
|
? "نرمافزار، اپلیکیشنهای سازمانی و راهکارهای هوش مصنوعی میسازم: پلتفرمهای وب و موبایل، سیستمهای توزیعشده، زیرساخت ابری و قابلیتهای LLM و RAG که به تولید میرسند."
|
||||||
: "Designing and deploying enterprise-grade AI systems — strategy, LLM & RAG, agentic automation, cloud infrastructure, and Google Stack.")" />
|
: "I design and build software, enterprise apps, and AI solutions: web and mobile platforms, distributed systems, cloud, and LLM/RAG features that ship to production.")" />
|
||||||
|
<meta name="theme-color" content="#fafafa" />
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts: Syne (display) + Vazirmatn (Persian). Body is system sans. -->
|
||||||
<style>
|
<style>
|
||||||
@@font-face { font-family:'Syne'; src:url('/fonts/Syne-Variable.woff2') format('woff2'); font-weight:100 900; font-display:swap; }
|
@@font-face { font-family:'Syne'; src:url('/fonts/Syne-Variable.woff2') format('woff2'); font-weight:100 900; font-display:swap; }
|
||||||
@@font-face { font-family:'Vazirmatn'; src:url('/fonts/Vazirmatn-Arabic.woff2') format('woff2'); font-display:swap; }
|
@@font-face { font-family:'Vazirmatn'; src:url('/fonts/Vazirmatn-Arabic.woff2') format('woff2'); font-display:swap; }
|
||||||
@@font-face { font-family:'VazirmatnLat'; src:url('/fonts/Vazirmatn-Latin.woff2') format('woff2'); font-display:swap; }
|
|
||||||
@@font-face { font-family:'SpaceMono'; src:url('/fonts/SpaceMono-Regular.woff2') format('woff2'); font-weight:400; font-display:swap; }
|
|
||||||
@@font-face { font-family:'SpaceMono'; src:url('/fonts/SpaceMono-Bold.woff2') format('woff2'); font-weight:700; font-display:swap; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Tailwind CDN (play) + custom config -->
|
<!-- Tailwind: prebuilt + purged stylesheet (`npm run build`). No runtime CDN. -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<link rel="stylesheet" href="/css/tailwind.css" asp-append-version="true" />
|
||||||
<script>
|
<link rel="stylesheet" href="/css/site.css" asp-append-version="true" />
|
||||||
tailwind.config = {
|
<link rel="icon" href="/logo-mark.svg" type="image/svg+xml" asp-append-version="true" />
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
base: { DEFAULT:'#020510', 800:'#050a1a', 700:'#0a1224', 600:'#0f1b33' },
|
|
||||||
electric:'#38bdf8',
|
|
||||||
violet: '#818cf8',
|
|
||||||
magenta: '#e879f9',
|
|
||||||
emerald: '#34d399',
|
|
||||||
cyan: '#22d3ee',
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
sans: ['Syne','Vazirmatn','VazirmatnLat','system-ui','sans-serif'],
|
|
||||||
display: ['Syne','Vazirmatn','sans-serif'],
|
|
||||||
fa: ['Vazirmatn','VazirmatnLat','sans-serif'],
|
|
||||||
mono: ['SpaceMono','ui-monospace','monospace'],
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
'pulse-dot': {'0%,100%':{opacity:'1',transform:'scale(1)'},'50%':{opacity:'.6',transform:'scale(1.4)'}},
|
|
||||||
'gradient-pan': {'0%,100%':{backgroundPosition:'0% 50%'},'50%':{backgroundPosition:'100% 50%'}},
|
|
||||||
'caret-blink': {'0%,49%':{opacity:'1'},'50%,100%':{opacity:'0'}},
|
|
||||||
'float-y': {'0%,100%':{transform:'translateY(0)'},'50%':{transform:'translateY(-6px)'}},
|
|
||||||
'flow-dash': {'0%':{strokeDashoffset:'0'},'100%':{strokeDashoffset:'-66'}},
|
|
||||||
'fade-up': {'0%':{opacity:'0',transform:'translateY(24px)'},'100%':{opacity:'1',transform:'translateY(0)'}},
|
|
||||||
'bar-grow': {'0%':{width:'0%'},'100%':{width:'var(--bar-w)'}},
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
'pulse-dot': 'pulse-dot 1.8s ease-in-out infinite',
|
|
||||||
'caret-blink':'caret-blink 1s steps(2) infinite',
|
|
||||||
'float-y': 'float-y 4s ease-in-out infinite',
|
|
||||||
'flow-dash': 'flow-dash 1.1s linear infinite',
|
|
||||||
'fade-up': 'fade-up .7s cubic-bezier(.22,1,.36,1) forwards',
|
|
||||||
'bar-grow': 'bar-grow 1.2s cubic-bezier(.22,1,.36,1) forwards',
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
'glow-electric':'0 0 40px -8px rgba(56,189,248,.55)',
|
|
||||||
'glow-magenta': '0 0 40px -8px rgba(232,121,249,.55)',
|
|
||||||
'glow-violet': '0 0 40px -8px rgba(129,140,248,.55)',
|
|
||||||
'glow-emerald': '0 0 40px -8px rgba(52,211,153,.55)',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Site CSS -->
|
|
||||||
<link rel="stylesheet" href="/css/site.css" />
|
|
||||||
<link rel="icon" href="/logo-mark.svg" type="image/svg+xml" />
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-base text-slate-200 antialiased">
|
<body class="site antialiased">
|
||||||
|
|
||||||
<!-- Custom cursor (desktop only) -->
|
<!-- Reading progress (CSS scroll-driven) -->
|
||||||
<div id="cursor" class="pointer-events-none fixed z-[9999] hidden h-5 w-5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-electric/70 transition-transform duration-100 lg:block" aria-hidden="true"></div>
|
<div class="scroll-progress" aria-hidden="true"></div>
|
||||||
<div id="cursor-dot" class="pointer-events-none fixed z-[9999] hidden h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-electric lg:block" aria-hidden="true"></div>
|
|
||||||
|
<!-- Sentinel for the navbar border (observed by IntersectionObserver) -->
|
||||||
|
<div id="nav-sentinel" aria-hidden="true"></div>
|
||||||
|
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<header id="navbar" class="fixed inset-x-0 top-0 z-50 transition-all duration-300">
|
<header id="navbar" class="fixed inset-x-0 top-0 z-50">
|
||||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-5 py-4 sm:px-8">
|
<div class="mx-auto flex max-w-6xl items-center justify-between px-5 py-3.5 sm:px-8">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<a href="/#top" class="flex items-center gap-2.5" aria-label="Home">
|
<a href="/#top" class="flex items-center gap-2.5" aria-label="@(locale == "fa" ? "خانه" : "Home")">
|
||||||
<img src="/logo-mark.svg" alt="" width="28" height="28" class="h-7 w-7" />
|
<img src="/logo-mark.svg" alt="" width="26" height="26" class="h-[26px] w-[26px]" />
|
||||||
<span class="font-display font-bold text-white @(isRtl ? "font-fa" : "")">
|
<span class="font-display text-[15px] font-bold text-zinc-900 @(isRtl ? "font-fa" : "")">
|
||||||
@(locale == "fa" ? "سروش اسعدی" : "Soroush Asadi")
|
@(locale == "fa" ? "سروش اسعدی" : "Soroush Asadi")
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Desktop nav -->
|
<!-- Desktop nav -->
|
||||||
<nav class="hidden items-center gap-6 md:flex" aria-label="Main">
|
<nav class="hidden items-center gap-7 md:flex" aria-label="Main">
|
||||||
@if (locale == "fa")
|
@if (locale == "fa")
|
||||||
{
|
{
|
||||||
<a href="/#services" class="nav-link">خدمات</a>
|
<a href="/#services" class="nav-link">خدمات</a>
|
||||||
@@ -104,7 +58,6 @@
|
|||||||
<a href="/#expertise" class="nav-link">تخصص</a>
|
<a href="/#expertise" class="nav-link">تخصص</a>
|
||||||
<a href="/#portfolio" class="nav-link">نمونهکارها</a>
|
<a href="/#portfolio" class="nav-link">نمونهکارها</a>
|
||||||
<a href="/#blog" class="nav-link">بلاگ</a>
|
<a href="/#blog" class="nav-link">بلاگ</a>
|
||||||
<a href="/#contact" class="nav-link">تماس</a>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -113,31 +66,26 @@
|
|||||||
<a href="/#expertise" class="nav-link">Expertise</a>
|
<a href="/#expertise" class="nav-link">Expertise</a>
|
||||||
<a href="/#portfolio" class="nav-link">Portfolio</a>
|
<a href="/#portfolio" class="nav-link">Portfolio</a>
|
||||||
<a href="/#blog" class="nav-link">Blog</a>
|
<a href="/#blog" class="nav-link">Blog</a>
|
||||||
<a href="/#contact" class="nav-link">Contact</a>
|
|
||||||
}
|
}
|
||||||
<a href="/#contact" class="btn-primary text-sm">
|
<a href="/#contact" class="btn text-sm">@(locale == "fa" ? "رزرو جلسه" : "Book a call")</a>
|
||||||
@(locale == "fa" ? "رزرو جلسه" : "Book a call")
|
|
||||||
</a>
|
|
||||||
<!-- Locale toggle -->
|
<!-- Locale toggle -->
|
||||||
<form method="post" action="/locale">
|
<form method="post" action="/locale">
|
||||||
<input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" />
|
<input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" />
|
||||||
<input type="hidden" name="returnUrl" value="@Context.Request.Path@Context.Request.QueryString" />
|
<input type="hidden" name="returnUrl" value="@Context.Request.Path@Context.Request.QueryString" />
|
||||||
<button type="submit" class="label-mono text-slate-400 hover:text-white transition-colors">
|
<button type="submit" class="nav-link text-xs tracking-wide">@(locale == "fa" ? "EN" : "FA")</button>
|
||||||
@(locale == "fa" ? "EN" : "FA")
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Mobile menu button -->
|
<!-- Mobile menu button -->
|
||||||
<button id="menu-btn" class="flex flex-col gap-1.5 p-2 md:hidden" aria-label="Menu">
|
<button id="menu-btn" class="flex flex-col gap-1.5 p-2 md:hidden" aria-label="@(locale == "fa" ? "منو" : "Menu")" aria-expanded="false">
|
||||||
<span class="block h-0.5 w-5 bg-slate-300 transition-all"></span>
|
<span class="block h-0.5 w-5 bg-zinc-800"></span>
|
||||||
<span class="block h-0.5 w-5 bg-slate-300 transition-all"></span>
|
<span class="block h-0.5 w-5 bg-zinc-800"></span>
|
||||||
<span class="block h-0.5 w-5 bg-slate-300 transition-all"></span>
|
<span class="block h-0.5 w-5 bg-zinc-800"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile drawer -->
|
<!-- Mobile drawer -->
|
||||||
<div id="mobile-menu" class="hidden border-t border-white/5 bg-base-800/95 backdrop-blur-xl md:hidden">
|
<div id="mobile-menu" class="hidden border-t border-zinc-200 bg-white/95 backdrop-blur-xl md:hidden">
|
||||||
<nav class="flex flex-col gap-1 px-5 py-4">
|
<nav class="flex flex-col gap-1 px-5 py-4">
|
||||||
@if (locale == "fa")
|
@if (locale == "fa")
|
||||||
{
|
{
|
||||||
@@ -160,9 +108,7 @@
|
|||||||
<form method="post" action="/locale" class="mt-2">
|
<form method="post" action="/locale" class="mt-2">
|
||||||
<input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" />
|
<input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" />
|
||||||
<input type="hidden" name="returnUrl" value="@Context.Request.Path@Context.Request.QueryString" />
|
<input type="hidden" name="returnUrl" value="@Context.Request.Path@Context.Request.QueryString" />
|
||||||
<button type="submit" class="label-mono text-slate-400">
|
<button type="submit" class="nav-link text-xs tracking-wide">@(locale == "fa" ? "Switch to English" : "تغییر به فارسی")</button>
|
||||||
@(locale == "fa" ? "Switch to English" : "تغییر به فارسی")
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,8 +118,44 @@
|
|||||||
@RenderBody()
|
@RenderBody()
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Footer (shared across all public pages) -->
|
||||||
<script src="/js/app.js" defer></script>
|
<footer class="site-footer mt-8 px-5 py-14 sm:px-8">
|
||||||
|
<div class="mx-auto grid max-w-6xl grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-[1.5fr_1fr_1fr]">
|
||||||
|
<div>
|
||||||
|
<a href="/#top" class="flex items-center gap-2.5" aria-label="@(locale == "fa" ? "خانه" : "Home")">
|
||||||
|
<img src="/logo-mark.svg" alt="" width="26" height="26" class="h-[26px] w-[26px]" />
|
||||||
|
<span class="font-display text-[15px] font-bold text-zinc-900 @(isRtl ? "font-fa" : "")">@(locale == "fa" ? "سروش اسعدی" : "Soroush Asadi")</span>
|
||||||
|
</a>
|
||||||
|
<p class="mt-4 max-w-xs text-sm leading-relaxed text-zinc-600">@(fa ? "نرمافزار، اپلیکیشنهای سازمانی و راهکارهای هوش مصنوعی که در عمل کار میکنند." : "Software, enterprise apps, and AI solutions, engineered to last.")</p>
|
||||||
|
<div class="mt-5 flex gap-2.5">
|
||||||
|
<a class="social" href="https://www.linkedin.com/in/soroushdes/" target="_blank" rel="noopener" aria-label="LinkedIn"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5ZM3 9h4v12H3V9Zm6 0h3.8v1.64h.05c.53-1 1.83-2.06 3.76-2.06 4.02 0 4.76 2.65 4.76 6.1V21h-4v-5.4c0-1.29-.02-2.95-1.8-2.95-1.8 0-2.07 1.4-2.07 2.85V21H9V9Z"/></svg></a>
|
||||||
|
<a class="social" href="https://www.instagram.com/soroushasadicom/" target="_blank" rel="noopener" aria-label="Instagram"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.2" cy="6.8" r="1.1" fill="currentColor" stroke="none"/></svg></a>
|
||||||
|
<a class="social" href="mailto:code.soroush@gmail.com" aria-label="Email"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav aria-label="Footer" class="flex flex-col gap-2.5">
|
||||||
|
<span class="kicker mb-1">@(fa ? "پیمایش" : "Navigate")</span>
|
||||||
|
<a href="/#services" class="foot-link">@(fa ? "خدمات" : "Services")</a>
|
||||||
|
<a href="/#stack" class="foot-link">@(fa ? "استک" : "Stack")</a>
|
||||||
|
<a href="/#portfolio" class="foot-link">@(fa ? "نمونهکارها" : "Work")</a>
|
||||||
|
<a href="/blog" class="foot-link">@(fa ? "بلاگ" : "Blog")</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2.5">
|
||||||
|
<span class="kicker mb-1">@(fa ? "تماس" : "Get in touch")</span>
|
||||||
|
<a href="/#contact" class="foot-link">@(fa ? "رزرو جلسه" : "Book a call")</a>
|
||||||
|
<a href="mailto:code.soroush@gmail.com" class="foot-link">code.soroush@gmail.com</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-auto mt-12 flex max-w-6xl flex-col items-center gap-2 border-t border-zinc-200 pt-6 text-center sm:flex-row sm:justify-between sm:text-start">
|
||||||
|
<p class="text-[.78rem] text-zinc-500">© 2026 Soroush Asadi. @(fa ? "تمام حقوق محفوظ است." : "All rights reserved.")</p>
|
||||||
|
<p class="text-[.78rem] text-zinc-500">@(fa ? "ساختهشده با دقت در تهران." : "Built with care in Tehran.")</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/js/app.js" defer asp-append-version="true"></script>
|
||||||
@await RenderSectionAsync("Scripts", required: false)
|
@await RenderSectionAsync("Scripts", required: false)
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+23
-1
@@ -50,7 +50,29 @@ using (var scope = app.Services.CreateScope())
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.UseStatusCodePagesWithReExecute("/Error/{0}");
|
app.UseStatusCodePagesWithReExecute("/Error/{0}");
|
||||||
app.UseStaticFiles();
|
|
||||||
|
// HTML pages must never be cached by the browser or CDN, so a new deploy is
|
||||||
|
// visible immediately. Static assets are fingerprinted via asp-append-version
|
||||||
|
// (?v=hash), so they can be cached aggressively and bust automatically.
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
context.Response.OnStarting(() =>
|
||||||
|
{
|
||||||
|
if (context.Response.ContentType?.Contains("text/html", StringComparison.OrdinalIgnoreCase) == true)
|
||||||
|
{
|
||||||
|
context.Response.Headers.CacheControl = "no-cache, no-store, must-revalidate";
|
||||||
|
context.Response.Headers.Pragma = "no-cache";
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
|
{
|
||||||
|
OnPrepareResponse = ctx =>
|
||||||
|
ctx.Context.Response.Headers.CacheControl = "public, max-age=31536000"
|
||||||
|
});
|
||||||
|
|
||||||
// Serve uploaded files from /data/uploads under /uploads/*
|
// Serve uploaded files from /data/uploads under /uploads/*
|
||||||
var uploadsPath = Path.Combine(dataDir, "uploads");
|
var uploadsPath = Path.Combine(dataDir, "uploads");
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<!-- Override the transitive SQLitePCLRaw 2.1.11 (GHSA-2m69-gcr7-jv3q, no 2.x patch)
|
||||||
|
with the 3.0.x line, which is outside the vulnerable range (<= 2.1.11). -->
|
||||||
|
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
+3
-1
@@ -11,8 +11,10 @@ services:
|
|||||||
image: soroushasadi-site:latest
|
image: soroushasadi-site:latest
|
||||||
container_name: soroushasadi-site
|
container_name: soroushasadi-site
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# Host port 3020 → container 3000. Port 3000 on the host is Gitea
|
||||||
|
# (git.soroushasadi.com proxies to :3000) — NEVER publish on 3000.
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3020:3000"
|
||||||
environment:
|
environment:
|
||||||
ASPNETCORE_ENVIRONMENT: Production
|
ASPNETCORE_ENVIRONMENT: Production
|
||||||
DataDir: /data
|
DataDir: /data
|
||||||
|
|||||||
Generated
+1028
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "soroushasadi-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Tailwind CSS build for the SoroushAsadi public + admin UI (replaces the runtime Play CDN).",
|
||||||
|
"scripts": {
|
||||||
|
"build:css": "tailwindcss -c tailwind.config.js -i ./Styles/tailwind.css -o ./wwwroot/css/tailwind.css --minify",
|
||||||
|
"build:css:admin": "tailwindcss -c tailwind.admin.config.js -i ./Styles/tailwind.css -o ./wwwroot/css/tailwind-admin.css --minify",
|
||||||
|
"build": "npm run build:css && npm run build:css:admin",
|
||||||
|
"watch:css": "tailwindcss -c tailwind.config.js -i ./Styles/tailwind.css -o ./wwwroot/css/tailwind.css --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tailwindcss": "^3.4.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
// Admin (dark) theme. Separate build so its flat custom colors do not collide
|
||||||
|
// with the public site's use of built-in color scales (e.g. emerald-600).
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./Pages/Admin/**/*.cshtml',
|
||||||
|
'./Pages/Shared/_AdminLayout.cshtml',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
base: { DEFAULT: '#020510', 800: '#050a1a' },
|
||||||
|
electric: '#38bdf8',
|
||||||
|
violet: '#818cf8',
|
||||||
|
magenta: '#e879f9',
|
||||||
|
emerald: '#34d399',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
display: ['Syne', 'system-ui', 'sans-serif'],
|
||||||
|
sans: ['Syne', 'system-ui', 'sans-serif'],
|
||||||
|
mono: ['SpaceMono', 'monospace'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
// Public site theme. Scoped away from the admin theme (which redefines
|
||||||
|
// `emerald` as a flat color and would collide with the emerald-600 scale).
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./Pages/*.cshtml',
|
||||||
|
'./Pages/Blog/**/*.cshtml',
|
||||||
|
'./Pages/Shared/_Layout.cshtml',
|
||||||
|
'./wwwroot/js/**/*.js',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
accent: '#2563eb',
|
||||||
|
accentink: '#1d4ed8',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
display: ['Syne', 'system-ui', 'sans-serif'],
|
||||||
|
fa: ['Vazirmatn', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
+230
-86
@@ -1,115 +1,257 @@
|
|||||||
/* ─── Design tokens ──────────────────────────────────────────────────── */
|
/* ════════════════════════════════════════════════════════════════════════
|
||||||
|
Minimal editorial theme, premium pass - light, single accent.
|
||||||
|
Public styles scoped under `.site`; the dark admin (which shares this file
|
||||||
|
via _AdminLayout) keeps its own dark classes at the bottom (.glass etc).
|
||||||
|
Dials: VARIANCE 7 / MOTION 5 / DENSITY 3.
|
||||||
|
════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #020510;
|
--bg: #fafafa;
|
||||||
--electric: #38bdf8;
|
--surface: #ffffff;
|
||||||
--violet: #818cf8;
|
--text: #18181b; /* zinc-900 */
|
||||||
--magenta: #e879f9;
|
--text-2: #52525b; /* zinc-600 */
|
||||||
--emerald: #34d399;
|
--text-3: #71717a; /* zinc-500 - meets WCAG AA 4.5:1 on the off-white bg */
|
||||||
--cyan: #22d3ee;
|
--line: #e4e4e7; /* zinc-200 */
|
||||||
--radius: 14px;
|
--line-strong: #d4d4d8; /* zinc-300 */
|
||||||
color-scheme: dark;
|
--accent: #2563eb; /* blue-600 - the single accent */
|
||||||
|
--accent-ink: #1d4ed8; /* blue-700 */
|
||||||
|
--accent-weak: #eff4ff;
|
||||||
|
--accent-line: #c7d2fe; /* indigo-200, for hover borders */
|
||||||
|
--ok: #16a34a; /* availability indicator only */
|
||||||
|
--radius: 8px;
|
||||||
|
--nav-h: 4.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
* { box-sizing: border-box; }
|
||||||
background: var(--bg);
|
html { scroll-behavior: smooth; background: var(--bg); }
|
||||||
font-feature-settings: 'ss01','cv11';
|
|
||||||
-webkit-font-smoothing: antialiased;
|
::selection { background: #dbe5ff; color: #18181b; }
|
||||||
|
|
||||||
|
a:focus-visible, button:focus-visible,
|
||||||
|
input:focus-visible, textarea:focus-visible, select:focus-visible {
|
||||||
|
outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px;
|
||||||
}
|
}
|
||||||
html { scroll-behavior: smooth; }
|
|
||||||
|
|
||||||
[dir='rtl'] body { font-family: 'Vazirmatn','VazirmatnLat','Syne',system-ui,sans-serif; }
|
|
||||||
[dir='ltr'] body { font-family: 'Syne','Vazirmatn','VazirmatnLat',system-ui,sans-serif; }
|
|
||||||
|
|
||||||
::selection { background: rgba(56,189,248,.35); color: #f8fafc; }
|
|
||||||
|
|
||||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||||
::-webkit-scrollbar-track { background: #050a1a; }
|
::-webkit-scrollbar-track { background: var(--bg); }
|
||||||
::-webkit-scrollbar-thumb { background: linear-gradient(180deg,#38bdf8,#818cf8); border-radius:999px; border:2px solid #050a1a; }
|
::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; border: 3px solid var(--bg); }
|
||||||
|
|
||||||
/* ─── Component classes (used across Razor templates) ────────────────── */
|
/* ─── Public base (scoped to .site) ──────────────────────────────────── */
|
||||||
|
body.site {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Vazirmatn', sans-serif;
|
||||||
|
font-size: 1rem; line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
[dir='rtl'] body.site { font-family: 'Vazirmatn', system-ui, sans-serif; }
|
||||||
|
|
||||||
.glass {
|
.site h1, .site h2, .site h3, .site h4 {
|
||||||
background: linear-gradient(180deg,rgba(255,255,255,.04) 0%,rgba(255,255,255,.015) 100%);
|
font-family: 'Syne', system-ui, sans-serif;
|
||||||
border: 1px solid rgba(56,189,248,.14);
|
font-weight: 700; color: var(--text);
|
||||||
backdrop-filter: blur(14px);
|
letter-spacing: -0.02em; line-height: 1.12; text-wrap: balance;
|
||||||
box-shadow: inset 0 1px 0 0 rgba(255,255,255,.06), 0 30px 60px -30px rgba(0,0,0,.6);
|
}
|
||||||
border-radius: var(--radius);
|
[dir='rtl'] .site h1, [dir='rtl'] .site h2,
|
||||||
|
[dir='rtl'] .site h3, [dir='rtl'] .site h4 {
|
||||||
|
font-family: 'Vazirmatn', system-ui, sans-serif; letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
/* anchor targets clear the fixed navbar */
|
||||||
display: inline-flex; align-items: center; gap: .5rem;
|
.site section[id] { scroll-margin-top: var(--nav-h); }
|
||||||
padding: .35rem .75rem; border-radius: 999px;
|
|
||||||
border: 1px solid rgba(52,211,153,.25); background: rgba(52,211,153,.06);
|
|
||||||
color: #a7f3d0;
|
|
||||||
font-family: 'SpaceMono',ui-monospace,monospace;
|
|
||||||
font-size: .72rem; letter-spacing: .04em; text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-mono {
|
.accent-text { color: var(--accent); }
|
||||||
font-family: 'SpaceMono',ui-monospace,monospace;
|
|
||||||
font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gradient-text {
|
/* thin reading-progress bar (CSS scroll-driven; degrades to nothing) */
|
||||||
background: linear-gradient(135deg,#38bdf8 0%,#818cf8 45%,#e879f9 100%);
|
.scroll-progress {
|
||||||
-webkit-background-clip: text; background-clip: text; color: transparent;
|
position: fixed; inset: 0 0 auto 0; height: 2px; z-index: 60;
|
||||||
background-size: 200% 200%; animation: gradient-pan 8s ease-in-out infinite;
|
background: var(--accent); transform-origin: left center; transform: scaleX(0);
|
||||||
|
animation: sp-grow linear both; animation-timeline: scroll(root);
|
||||||
}
|
}
|
||||||
@keyframes gradient-pan { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
|
[dir='rtl'] .scroll-progress { transform-origin: right center; }
|
||||||
|
@keyframes sp-grow { to { transform: scaleX(1); } }
|
||||||
|
|
||||||
.btn-primary {
|
/* ─── Navigation ─────────────────────────────────────────────────────── */
|
||||||
display: inline-flex; align-items: center; gap: .6rem;
|
#navbar { transition: background .25s ease, border-color .25s ease; border-bottom: 1px solid transparent; }
|
||||||
padding: .85rem 1.4rem; border-radius: 999px;
|
#navbar.scrolled { background: rgba(250,250,250,.82); backdrop-filter: blur(12px); border-bottom-color: var(--line); }
|
||||||
font-weight: 600; color: #020510;
|
|
||||||
background: linear-gradient(135deg,#38bdf8 0%,#818cf8 60%,#e879f9 100%);
|
.nav-link { position: relative; font-size: .9rem; color: var(--text-2); transition: color .18s ease; }
|
||||||
background-size: 200% 200%;
|
.nav-link:hover { color: var(--text); }
|
||||||
transition: transform .25s ease, box-shadow .25s ease, background-position .6s ease;
|
.nav-link.active { color: var(--text); }
|
||||||
box-shadow: 0 12px 40px -12px rgba(56,189,248,.55);
|
.nav-link::after {
|
||||||
|
content: ""; position: absolute; left: 0; right: 0; bottom: -6px; height: 1.5px;
|
||||||
|
background: var(--text); transform: scaleX(0); transform-origin: left; transition: transform .25s ease;
|
||||||
}
|
}
|
||||||
.btn-primary:hover { transform: translateY(-1px); background-position: 100% 0; box-shadow: 0 18px 50px -10px rgba(232,121,249,.55); }
|
[dir='rtl'] .nav-link::after { transform-origin: right; }
|
||||||
|
.nav-link:hover::after, .nav-link.active::after { transform: scaleX(1); }
|
||||||
|
|
||||||
|
/* ─── Buttons (one shape: 8px radius, no pills, no glow) ──────────────── */
|
||||||
|
.btn, .btn-primary {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
|
||||||
|
padding: .72rem 1.2rem; border-radius: var(--radius);
|
||||||
|
font-weight: 600; font-size: .92rem;
|
||||||
|
background: var(--accent); color: #fff; border: 1px solid var(--accent);
|
||||||
|
transition: background .18s ease, transform .12s ease, box-shadow .18s ease;
|
||||||
|
}
|
||||||
|
.btn:hover, .btn-primary:hover { background: var(--accent-ink); border-color: var(--accent-ink); box-shadow: 0 8px 24px -10px rgba(37,99,235,.5); }
|
||||||
|
.btn:active, .btn-primary:active { transform: translateY(1px); }
|
||||||
|
.btn svg { transition: transform .2s ease; }
|
||||||
|
.btn:hover svg { transform: translateX(3px); }
|
||||||
|
[dir='rtl'] .btn:hover svg { transform: translateX(-3px); }
|
||||||
|
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
display: inline-flex; align-items: center; gap: .6rem;
|
display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
|
||||||
padding: .8rem 1.35rem; border-radius: 999px;
|
padding: .72rem 1.2rem; border-radius: var(--radius);
|
||||||
font-weight: 500; color: #e2e8f0;
|
font-weight: 500; font-size: .92rem;
|
||||||
border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.02);
|
color: var(--text); background: transparent; border: 1px solid var(--line-strong);
|
||||||
transition: border-color .25s ease, background .25s ease, transform .25s ease;
|
transition: border-color .18s ease, background .18s ease, transform .12s ease;
|
||||||
}
|
}
|
||||||
.btn-ghost:hover { border-color: rgba(56,189,248,.6); background: rgba(56,189,248,.06); transform: translateY(-1px); }
|
.btn-ghost:hover { border-color: var(--text); background: #fff; }
|
||||||
|
.btn-ghost:active { transform: translateY(1px); }
|
||||||
|
|
||||||
.nav-link {
|
/* pointer affordance + disabled semantics (ui-ux-pro-max: cursor-pointer, disabled-states) */
|
||||||
font-size: .875rem; color: #94a3b8;
|
.site button { cursor: pointer; }
|
||||||
transition: color .2s ease; text-decoration: none;
|
.site button:disabled { cursor: not-allowed; opacity: .55; }
|
||||||
|
|
||||||
|
/* ─── Availability status ────────────────────────────────────────────── */
|
||||||
|
.status {
|
||||||
|
display: inline-flex; align-items: center; gap: .5rem;
|
||||||
|
font-size: .8rem; color: var(--text-2);
|
||||||
|
border: 1px solid var(--line); background: #fff; border-radius: var(--radius); padding: .32rem .7rem;
|
||||||
}
|
}
|
||||||
.nav-link:hover { color: #e2e8f0; }
|
.status .dot { position: relative; width: 7px; height: 7px; border-radius: 999px; background: var(--ok); }
|
||||||
|
.status .dot::after { content: ""; position: absolute; inset: 0; border-radius: 999px; background: var(--ok); opacity: .55; animation: ping 1.9s cubic-bezier(0,0,.2,1) infinite; }
|
||||||
|
@keyframes ping { 70%, 100% { transform: scale(2.4); opacity: 0; } }
|
||||||
|
|
||||||
#navbar.scrolled { background: rgba(2,5,16,.85); backdrop-filter: blur(20px); border-bottom: 1px solid rgba(255,255,255,.05); }
|
/* ─── Social ─────────────────────────────────────────────────────────── */
|
||||||
|
.social {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
width: 44px; height: 44px; border: 1px solid var(--line-strong); border-radius: var(--radius);
|
||||||
|
color: var(--text-2); transition: color .18s ease, border-color .18s ease, transform .18s ease;
|
||||||
|
}
|
||||||
|
.social:hover { color: var(--text); border-color: var(--text); transform: translateY(-2px); }
|
||||||
|
.social svg { width: 18px; height: 18px; }
|
||||||
|
|
||||||
/* ─── Section header ─────────────────────────────────────────────── */
|
/* ─── Type helpers ───────────────────────────────────────────────────── */
|
||||||
.section-header { text-align: center; margin-bottom: 3.5rem; }
|
.kicker { font-size: .76rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); font-weight: 500; }
|
||||||
.section-header .eyebrow { margin-bottom: 1rem; }
|
[dir='rtl'] .kicker { letter-spacing: .06em; }
|
||||||
.section-header h2 { font-family:'Syne',sans-serif; font-size:clamp(1.8rem,3.5vw,2.75rem); font-weight:800; color:#fff; margin-bottom:1rem; }
|
.lede { color: var(--text-2); font-size: 1.05rem; line-height: 1.7; max-width: 44rem; }
|
||||||
[dir='rtl'] .section-header h2 { font-family:'Vazirmatn',sans-serif; }
|
|
||||||
.section-header p { max-width:42rem; margin:0 auto; color:#94a3b8; font-size:1.0625rem; line-height:1.7; }
|
|
||||||
|
|
||||||
/* ─── Service cards ──────────────────────────────────────────────── */
|
.sec-head { margin-bottom: 3rem; max-width: 46rem; }
|
||||||
.service-card { transition: border-color .3s, box-shadow .3s, transform .3s; }
|
.sec-head h2 { font-size: clamp(1.7rem, 3.2vw, 2.4rem); }
|
||||||
.service-card:hover { transform: translateY(-2px); }
|
.sec-head .lede { margin-top: .9rem; }
|
||||||
|
|
||||||
/* ─── Expertise bars ─────────────────────────────────────────────── */
|
.arrow-link { display: inline-flex; align-items: center; gap: .4rem; color: var(--accent); font-weight: 500; font-size: .85rem; }
|
||||||
.bar-track { height: .375rem; background: rgba(255,255,255,.07); border-radius:999px; overflow:hidden; }
|
.arrow-link svg { transition: transform .2s ease; }
|
||||||
.bar-fill { height: 100%; border-radius:999px; transform-origin:left; }
|
.arrow-link:hover svg, .group:hover .arrow-link svg { transform: translateX(3px); }
|
||||||
[dir='rtl'] .bar-fill { transform-origin:right; }
|
[dir='rtl'] .arrow-link:hover svg, [dir='rtl'] .group:hover .arrow-link svg { transform: translateX(-3px); }
|
||||||
|
|
||||||
/* ─── Scroll-reveal ──────────────────────────────────────────────── */
|
/* ─── Neutral tag chip (one shape) ───────────────────────────────────── */
|
||||||
.reveal { opacity: 0; transform: translateY(24px); transition: opacity .7s cubic-bezier(.22,1,.36,1), transform .7s cubic-bezier(.22,1,.36,1); }
|
.chip {
|
||||||
|
display: inline-flex; align-items: center;
|
||||||
|
border: 1px solid var(--line); border-radius: 6px;
|
||||||
|
padding: .2rem .55rem; font-size: .72rem; color: var(--text-2);
|
||||||
|
background: #fff; white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Service blocks (animated accent top-rule on hover) ─────────────── */
|
||||||
|
.svc { position: relative; }
|
||||||
|
.svc::before {
|
||||||
|
content: ""; position: absolute; inset-block-start: -1px; inset-inline: 0; height: 2px;
|
||||||
|
background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform .3s ease;
|
||||||
|
}
|
||||||
|
[dir='rtl'] .svc::before { transform-origin: right; }
|
||||||
|
.svc:hover::before { transform: scaleX(1); }
|
||||||
|
.svc:hover .svc-icon { color: var(--accent); }
|
||||||
|
.svc-icon { transition: color .2s ease; }
|
||||||
|
|
||||||
|
/* ─── Surface card (portfolio, contact) ──────────────────────────────── */
|
||||||
|
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
|
||||||
|
.card-link { transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease; }
|
||||||
|
.card-link:hover { border-color: var(--accent-line); transform: translateY(-3px); box-shadow: 0 16px 40px -24px rgba(24,24,27,.35); }
|
||||||
|
.cover { overflow: hidden; }
|
||||||
|
.cover > * { transition: transform .45s cubic-bezier(.22,1,.36,1); }
|
||||||
|
.card-link:hover .cover > * { transform: scale(1.04); }
|
||||||
|
|
||||||
|
/* ─── Bento grid (Apple-style tiles, varied spans) ───────────────────── */
|
||||||
|
.bento { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); grid-auto-rows: minmax(158px, auto); gap: 14px; }
|
||||||
|
.tile {
|
||||||
|
position: relative; display: flex; flex-direction: column;
|
||||||
|
background: var(--surface); border: 1px solid var(--line); border-radius: 22px;
|
||||||
|
padding: 1.5rem; overflow: hidden;
|
||||||
|
transition: transform .22s cubic-bezier(.22,1,.36,1), box-shadow .22s ease, border-color .22s ease;
|
||||||
|
}
|
||||||
|
.tile-link { cursor: pointer; }
|
||||||
|
.tile-link:hover { transform: translateY(-4px); box-shadow: 0 22px 48px -26px rgba(24,24,27,.4); border-color: var(--line-strong); }
|
||||||
|
.span-2 { grid-column: span 2; } .span-3 { grid-column: span 3; } .span-4 { grid-column: span 4; }
|
||||||
|
.row-2 { grid-row: span 2; }
|
||||||
|
.tile-dark { background: #18181b; border-color: #18181b; }
|
||||||
|
.tile-accent { background: var(--accent); border-color: var(--accent); }
|
||||||
|
.tile-tint { background: var(--accent-weak); border-color: #dbe3ff; }
|
||||||
|
.tile-icon { color: var(--accent); }
|
||||||
|
.tile-dark .tile-icon, .tile-accent .tile-icon { color: #fff; }
|
||||||
|
.tile-dark h1, .tile-dark h2, .tile-dark h3, .tile-accent h1, .tile-accent h2, .tile-accent h3 { color: #fafafa; }
|
||||||
|
.t-sub { color: #d4d4d8; }
|
||||||
|
.tile-accent .t-sub { color: rgba(255,255,255,.85); }
|
||||||
|
.btn-on-dark { color: #fafafa; background: transparent; border-color: rgba(255,255,255,.28); }
|
||||||
|
.btn-on-dark:hover { color: #fafafa; background: rgba(255,255,255,.08); border-color: #fafafa; }
|
||||||
|
.pcover { display: flex; align-items: center; justify-content: center; }
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.bento { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||||
|
.span-3, .span-4 { grid-column: span 2; }
|
||||||
|
}
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.bento { grid-template-columns: 1fr; grid-auto-rows: auto; }
|
||||||
|
.span-2, .span-3, .span-4 { grid-column: span 1; }
|
||||||
|
.row-2 { grid-row: span 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Form fields ────────────────────────────────────────────────────── */
|
||||||
|
.flabel { display: block; font-size: .85rem; font-weight: 500; color: var(--text-2); margin-bottom: .4rem; }
|
||||||
|
.field {
|
||||||
|
width: 100%; font-family: inherit; font-size: .92rem; color: var(--text);
|
||||||
|
background: #fff; border: 1px solid var(--line-strong); border-radius: var(--radius);
|
||||||
|
padding: .7rem .85rem; transition: border-color .18s ease, box-shadow .18s ease;
|
||||||
|
}
|
||||||
|
.field::placeholder { color: var(--text-3); }
|
||||||
|
.field:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
|
||||||
|
|
||||||
|
/* ─── Footer ─────────────────────────────────────────────────────────── */
|
||||||
|
.site-footer { border-top: 1px solid var(--line); background: #fff; }
|
||||||
|
.foot-link { color: var(--text-2); font-size: .9rem; transition: color .18s ease; }
|
||||||
|
.foot-link:hover { color: var(--text); }
|
||||||
|
|
||||||
|
/* ─── Scroll reveal (MOTION 5: subtle, transform+opacity only) ───────── */
|
||||||
|
.reveal { opacity: 0; transform: translateY(18px); transition: opacity .6s ease, transform .7s cubic-bezier(.22,1,.36,1); }
|
||||||
.reveal.visible { opacity: 1; transform: none; }
|
.reveal.visible { opacity: 1; transform: none; }
|
||||||
|
|
||||||
/* ─── Portfolio modal ────────────────────────────────────────────── */
|
/* ─── Blog prose ─────────────────────────────────────────────────────── */
|
||||||
#portfolio-modal { transition: opacity .25s ease; }
|
.prose-custom { color: var(--text-2); line-height: 1.8; font-size: 1.02rem; }
|
||||||
#portfolio-modal.hidden { pointer-events: none; }
|
.prose-custom h2, .prose-custom h3 { color: var(--text); font-family: 'Syne', system-ui, sans-serif; margin: 2rem 0 .6rem; }
|
||||||
|
[dir='rtl'] .prose-custom h2, [dir='rtl'] .prose-custom h3 { font-family: 'Vazirmatn', system-ui, sans-serif; }
|
||||||
|
.prose-custom p { margin-bottom: 1.2rem; }
|
||||||
|
.prose-custom a { color: var(--accent); text-decoration: underline; text-underline-offset: .2em; }
|
||||||
|
.prose-custom strong { color: var(--text); }
|
||||||
|
.prose-custom code { font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace; background: #f4f4f5; border: 1px solid var(--line); border-radius: 5px; padding: .12em .4em; font-size: .88em; color: #3f3f46; }
|
||||||
|
.prose-custom pre { background: #f4f4f5; border: 1px solid var(--line); border-radius: 8px; padding: 1.1rem; overflow-x: auto; margin: 1.4rem 0; }
|
||||||
|
.prose-custom pre code { background: none; border: 0; padding: 0; color: #27272a; }
|
||||||
|
.prose-custom ul, .prose-custom ol { padding-inline-start: 1.4rem; margin-bottom: 1.2rem; }
|
||||||
|
.prose-custom li { margin-bottom: .4rem; }
|
||||||
|
.prose-custom blockquote { border-inline-start: 3px solid var(--accent); padding-inline-start: 1rem; color: var(--text-2); margin: 1.4rem 0; }
|
||||||
|
|
||||||
/* ─── Admin nav ──────────────────────────────────────────────────── */
|
/* ════════════════════════════════════════════════════════════════════════
|
||||||
|
Admin (dark) - preserved. _AdminLayout has its own dark Tailwind config;
|
||||||
|
these classes keep working on its dark surface. Do not light-theme these.
|
||||||
|
════════════════════════════════════════════════════════════════════════ */
|
||||||
|
.glass {
|
||||||
|
background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.015));
|
||||||
|
border: 1px solid rgba(255,255,255,.10);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: inset 0 1px 0 0 rgba(255,255,255,.05);
|
||||||
|
}
|
||||||
|
.label-mono {
|
||||||
|
font-family: 'SpaceMono', ui-monospace, monospace;
|
||||||
|
font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: #94a3b8;
|
||||||
|
}
|
||||||
.admin-nav-link {
|
.admin-nav-link {
|
||||||
display: block; padding: .5rem .75rem; border-radius: .5rem;
|
display: block; padding: .5rem .75rem; border-radius: .5rem;
|
||||||
color: #94a3b8; font-size: .875rem; text-decoration: none;
|
color: #94a3b8; font-size: .875rem; text-decoration: none;
|
||||||
@@ -118,10 +260,12 @@ html { scroll-behavior: smooth; }
|
|||||||
.admin-nav-link:hover { background: rgba(255,255,255,.05); color: #e2e8f0; }
|
.admin-nav-link:hover { background: rgba(255,255,255,.05); color: #e2e8f0; }
|
||||||
.admin-nav-link.active { background: rgba(56,189,248,.1); color: #38bdf8; }
|
.admin-nav-link.active { background: rgba(56,189,248,.1); color: #38bdf8; }
|
||||||
|
|
||||||
/* ─── Reduced motion ─────────────────────────────────────────────── */
|
/* ─── Reduced motion ─────────────────────────────────────────────────── */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,*::before,*::after {
|
*, *::before, *::after {
|
||||||
animation-duration: .001ms !important; animation-iteration-count: 1 !important;
|
animation-duration: .001ms !important; animation-iteration-count: 1 !important;
|
||||||
transition-duration: .001ms !important; scroll-behavior: auto !important;
|
transition-duration: .001ms !important; scroll-behavior: auto !important;
|
||||||
}
|
}
|
||||||
|
.reveal { opacity: 1 !important; transform: none !important; }
|
||||||
|
.scroll-progress { display: none; }
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+60
-215
@@ -1,257 +1,102 @@
|
|||||||
/* ─── Custom cursor ───────────────────────────────────────────────────── */
|
/* ════════════════════════════════════════════════════════════════════════
|
||||||
(function () {
|
Minimal interactions. MOTION_INTENSITY 3: hover + a single subtle reveal.
|
||||||
const ring = document.getElementById('cursor');
|
No custom cursor, no particle canvas, no typewriter, no scroll listeners.
|
||||||
const dot = document.getElementById('cursor-dot');
|
════════════════════════════════════════════════════════════════════════ */
|
||||||
if (!ring || !dot) return;
|
|
||||||
let mx = 0, my = 0, rx = 0, ry = 0;
|
|
||||||
document.addEventListener('mousemove', e => { mx = e.clientX; my = e.clientY; });
|
|
||||||
document.addEventListener('mouseenter', () => { ring.style.opacity = '1'; dot.style.opacity = '1'; });
|
|
||||||
document.addEventListener('mouseleave', () => { ring.style.opacity = '0'; dot.style.opacity = '0'; });
|
|
||||||
(function raf() {
|
|
||||||
rx += (mx - rx) * 0.12; ry += (my - ry) * 0.12;
|
|
||||||
ring.style.left = rx + 'px'; ring.style.top = ry + 'px';
|
|
||||||
dot.style.left = mx + 'px'; dot.style.top = my + 'px';
|
|
||||||
requestAnimationFrame(raf);
|
|
||||||
})();
|
|
||||||
document.querySelectorAll('a,button,label,input,textarea,select').forEach(el => {
|
|
||||||
el.addEventListener('mouseenter', () => ring.style.transform = 'translate(-50%,-50%) scale(1.6)');
|
|
||||||
el.addEventListener('mouseleave', () => ring.style.transform = 'translate(-50%,-50%) scale(1)');
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* ─── Navbar scroll shadow ────────────────────────────────────────────── */
|
/* ─── Navbar border on scroll (IntersectionObserver, not a scroll listener) */
|
||||||
(function () {
|
(function () {
|
||||||
const nav = document.getElementById('navbar');
|
const nav = document.getElementById('navbar');
|
||||||
if (!nav) return;
|
const sentinel = document.getElementById('nav-sentinel');
|
||||||
const update = () => nav.classList.toggle('scrolled', window.scrollY > 30);
|
if (!nav || !sentinel) return;
|
||||||
window.addEventListener('scroll', update, { passive: true });
|
const io = new IntersectionObserver(
|
||||||
update();
|
([entry]) => nav.classList.toggle('scrolled', !entry.isIntersecting),
|
||||||
|
{ threshold: 0 }
|
||||||
|
);
|
||||||
|
io.observe(sentinel);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* ─── Mobile menu toggle ──────────────────────────────────────────────── */
|
/* ─── Scrollspy: highlight the nav link for the section in view ───────── */
|
||||||
|
(function () {
|
||||||
|
const links = Array.from(document.querySelectorAll('#navbar a[href*="#"]'));
|
||||||
|
if (!links.length || !('IntersectionObserver' in window)) return;
|
||||||
|
const map = {};
|
||||||
|
links.forEach((a) => {
|
||||||
|
const id = (a.getAttribute('href') || '').split('#')[1];
|
||||||
|
if (id) (map[id] = map[id] || []).push(a);
|
||||||
|
});
|
||||||
|
const sections = Object.keys(map)
|
||||||
|
.map((id) => document.getElementById(id))
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!sections.length) return;
|
||||||
|
|
||||||
|
const io = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (!e.isIntersecting) return;
|
||||||
|
links.forEach((a) => a.classList.remove('active'));
|
||||||
|
(map[e.target.id] || []).forEach((a) => a.classList.add('active'));
|
||||||
|
});
|
||||||
|
}, { rootMargin: '-45% 0px -50% 0px', threshold: 0 });
|
||||||
|
sections.forEach((s) => io.observe(s));
|
||||||
|
})();
|
||||||
|
|
||||||
|
/* ─── Mobile menu toggle ─────────────────────────────────────────────── */
|
||||||
(function () {
|
(function () {
|
||||||
const btn = document.getElementById('menu-btn');
|
const btn = document.getElementById('menu-btn');
|
||||||
const menu = document.getElementById('mobile-menu');
|
const menu = document.getElementById('mobile-menu');
|
||||||
if (!btn || !menu) return;
|
if (!btn || !menu) return;
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const open = menu.classList.toggle('hidden');
|
const hidden = menu.classList.toggle('hidden');
|
||||||
btn.setAttribute('aria-expanded', String(!open));
|
btn.setAttribute('aria-expanded', String(!hidden));
|
||||||
});
|
});
|
||||||
menu.querySelectorAll('a').forEach(a => a.addEventListener('click', () => menu.classList.add('hidden')));
|
menu.querySelectorAll('a').forEach(a => a.addEventListener('click', () => menu.classList.add('hidden')));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* ─── Scroll-reveal (Intersection Observer) ───────────────────────────── */
|
/* ─── Scroll reveal (one subtle entry per element, then unobserve) ────── */
|
||||||
(function () {
|
(function () {
|
||||||
|
const els = document.querySelectorAll('.reveal');
|
||||||
|
if (!els.length) return;
|
||||||
|
if (!('IntersectionObserver' in window)) {
|
||||||
|
els.forEach(el => el.classList.add('visible'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const io = new IntersectionObserver((entries) => {
|
const io = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); } });
|
|
||||||
}, { threshold: 0.12 });
|
|
||||||
document.querySelectorAll('.reveal').forEach(el => io.observe(el));
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* ─── Particle canvas (hero background) ──────────────────────────────── */
|
|
||||||
(function () {
|
|
||||||
const canvas = document.getElementById('particle-canvas');
|
|
||||||
if (!canvas) return;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
let W, H, particles = [], animId;
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
W = canvas.width = canvas.offsetWidth;
|
|
||||||
H = canvas.height = canvas.offsetHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeParticle() {
|
|
||||||
return {
|
|
||||||
x: Math.random() * W, y: Math.random() * H,
|
|
||||||
vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3,
|
|
||||||
r: Math.random() * 1.5 + 0.5,
|
|
||||||
a: Math.random() * 0.5 + 0.1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() { particles = Array.from({ length: 90 }, makeParticle); }
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
ctx.clearRect(0, 0, W, H);
|
|
||||||
const MAX_DIST = 120;
|
|
||||||
for (let i = 0; i < particles.length; i++) {
|
|
||||||
const p = particles[i];
|
|
||||||
p.x += p.vx; p.y += p.vy;
|
|
||||||
if (p.x < 0) p.x = W; if (p.x > W) p.x = 0;
|
|
||||||
if (p.y < 0) p.y = H; if (p.y > H) p.y = 0;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = `rgba(56,189,248,${p.a})`;
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
for (let j = i + 1; j < particles.length; j++) {
|
|
||||||
const q = particles[j];
|
|
||||||
const dx = p.x - q.x, dy = p.y - q.y;
|
|
||||||
const d = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (d < MAX_DIST) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y);
|
|
||||||
ctx.strokeStyle = `rgba(56,189,248,${(1 - d / MAX_DIST) * 0.15})`;
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
animId = requestAnimationFrame(draw);
|
|
||||||
}
|
|
||||||
|
|
||||||
resize(); init(); draw();
|
|
||||||
window.addEventListener('resize', () => { resize(); });
|
|
||||||
|
|
||||||
// Pause when hidden
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.hidden) { cancelAnimationFrame(animId); } else { draw(); }
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* ─── Typewriter ──────────────────────────────────────────────────────── */
|
|
||||||
(function () {
|
|
||||||
const el = document.getElementById('typewriter');
|
|
||||||
if (!el) return;
|
|
||||||
const words = JSON.parse(el.dataset.words || '[]');
|
|
||||||
if (!words.length) return;
|
|
||||||
let wi = 0, ci = 0, deleting = false;
|
|
||||||
|
|
||||||
function tick() {
|
|
||||||
const word = words[wi];
|
|
||||||
if (!deleting) {
|
|
||||||
el.textContent = word.slice(0, ++ci);
|
|
||||||
if (ci === word.length) { setTimeout(tick, 1800); deleting = true; return; }
|
|
||||||
setTimeout(tick, 80);
|
|
||||||
} else {
|
|
||||||
el.textContent = word.slice(0, --ci);
|
|
||||||
if (ci === 0) { deleting = false; wi = (wi + 1) % words.length; setTimeout(tick, 300); return; }
|
|
||||||
setTimeout(tick, 40);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tick();
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* ─── Animated counters ───────────────────────────────────────────────── */
|
|
||||||
(function () {
|
|
||||||
const io = new IntersectionObserver(entries => {
|
|
||||||
entries.forEach(e => {
|
entries.forEach(e => {
|
||||||
if (!e.isIntersecting) return;
|
if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); }
|
||||||
const el = e.target;
|
|
||||||
const target = parseInt(el.dataset.target || '0', 10);
|
|
||||||
const duration = 1200;
|
|
||||||
const start = performance.now();
|
|
||||||
function step(now) {
|
|
||||||
const t = Math.min((now - start) / duration, 1);
|
|
||||||
const ease = 1 - Math.pow(1 - t, 3);
|
|
||||||
el.textContent = el.dataset.prefix + Math.round(target * ease) + (el.dataset.suffix || '');
|
|
||||||
if (t < 1) requestAnimationFrame(step);
|
|
||||||
}
|
|
||||||
requestAnimationFrame(step);
|
|
||||||
io.unobserve(el);
|
|
||||||
});
|
});
|
||||||
}, { threshold: 0.5 });
|
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
|
||||||
document.querySelectorAll('.counter').forEach(el => io.observe(el));
|
els.forEach(el => io.observe(el));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* ─── Expertise bars (animate on scroll) ─────────────────────────────── */
|
/* ─── Contact form (AJAX, JSON → /contact) ───────────────────────────── */
|
||||||
(function () {
|
|
||||||
const io = new IntersectionObserver(entries => {
|
|
||||||
entries.forEach(e => {
|
|
||||||
if (!e.isIntersecting) return;
|
|
||||||
const fill = e.target.querySelector('.bar-fill');
|
|
||||||
if (fill) { fill.style.width = fill.dataset.w; }
|
|
||||||
io.unobserve(e.target);
|
|
||||||
});
|
|
||||||
}, { threshold: 0.3 });
|
|
||||||
document.querySelectorAll('.bar-track').forEach(el => io.observe(el));
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* ─── Portfolio modal ─────────────────────────────────────────────────── */
|
|
||||||
(function () {
|
|
||||||
const modal = document.getElementById('portfolio-modal');
|
|
||||||
const overlay = document.getElementById('modal-overlay');
|
|
||||||
if (!modal) return;
|
|
||||||
|
|
||||||
let images = [], idx = 0;
|
|
||||||
|
|
||||||
const imgEl = document.getElementById('modal-img');
|
|
||||||
const titleEl = document.getElementById('modal-title');
|
|
||||||
const bodyEl = document.getElementById('modal-body');
|
|
||||||
const prevBtn = document.getElementById('modal-prev');
|
|
||||||
const nextBtn = document.getElementById('modal-next');
|
|
||||||
const closeBtn = document.getElementById('modal-close');
|
|
||||||
|
|
||||||
function showModal(card) {
|
|
||||||
images = JSON.parse(card.dataset.gallery || '[]');
|
|
||||||
idx = 0;
|
|
||||||
if (titleEl) titleEl.textContent = card.dataset.title || '';
|
|
||||||
if (bodyEl) bodyEl.innerHTML = card.dataset.summary || '';
|
|
||||||
updateImg();
|
|
||||||
modal.classList.remove('hidden');
|
|
||||||
modal.style.opacity = '0';
|
|
||||||
requestAnimationFrame(() => { modal.style.opacity = '1'; });
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideModal() {
|
|
||||||
modal.style.opacity = '0';
|
|
||||||
setTimeout(() => { modal.classList.add('hidden'); document.body.style.overflow = ''; }, 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateImg() {
|
|
||||||
if (!imgEl) return;
|
|
||||||
imgEl.src = images[idx] || '';
|
|
||||||
if (prevBtn) prevBtn.disabled = idx === 0;
|
|
||||||
if (nextBtn) nextBtn.disabled = idx === images.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-portfolio-card]').forEach(card => {
|
|
||||||
card.addEventListener('click', () => showModal(card));
|
|
||||||
card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); showModal(card); } });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (closeBtn) closeBtn.addEventListener('click', hideModal);
|
|
||||||
if (overlay) overlay.addEventListener('click', hideModal);
|
|
||||||
if (prevBtn) prevBtn.addEventListener('click', () => { if (idx > 0) { idx--; updateImg(); } });
|
|
||||||
if (nextBtn) nextBtn.addEventListener('click', () => { if (idx < images.length - 1) { idx++; updateImg(); } });
|
|
||||||
|
|
||||||
document.addEventListener('keydown', e => {
|
|
||||||
if (modal.classList.contains('hidden')) return;
|
|
||||||
if (e.key === 'Escape') hideModal();
|
|
||||||
if (e.key === 'ArrowLeft') { if (idx > 0) { idx--; updateImg(); } }
|
|
||||||
if (e.key === 'ArrowRight') { if (idx < images.length - 1) { idx++; updateImg(); } }
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* ─── Contact form (AJAX) ─────────────────────────────────────────────── */
|
|
||||||
(function () {
|
(function () {
|
||||||
const form = document.getElementById('contact-form');
|
const form = document.getElementById('contact-form');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
const status = document.getElementById('contact-status');
|
const status = document.getElementById('contact-status');
|
||||||
|
|
||||||
form.addEventListener('submit', async e => {
|
form.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = Object.fromEntries(new FormData(form));
|
const data = Object.fromEntries(new FormData(form));
|
||||||
const btn = form.querySelector('[type="submit"]');
|
const btn = form.querySelector('[type="submit"]');
|
||||||
btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
|
|
||||||
|
const setStatus = (msg, cls) => { if (status) { status.textContent = msg; status.className = 'mt-1 text-sm ' + cls; } };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/contact', {
|
const res = await fetch('/contact', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
if (status) { status.textContent = form.dataset.successMsg || 'Sent!'; status.className = 'mt-3 text-sm text-emerald-400'; }
|
setStatus(form.dataset.successMsg || 'Sent.', 'text-emerald-600');
|
||||||
form.reset();
|
form.reset();
|
||||||
} else {
|
} else {
|
||||||
if (status) { status.textContent = form.dataset.errorMsg || 'Something went wrong.'; status.className = 'mt-3 text-sm text-red-400'; }
|
setStatus(form.dataset.errorMsg || 'Something went wrong.', 'text-red-600');
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (status) { status.textContent = form.dataset.errorMsg || 'Network error.'; status.className = 'mt-3 text-sm text-red-400'; }
|
setStatus(form.dataset.errorMsg || 'Network error.', 'text-red-600');
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
if (btn) btn.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100" role="img" aria-label="Soroush Asadi">
|
||||||
|
<rect width="100" height="100" rx="23" fill="#18181b"/>
|
||||||
|
<path d="M70 33 C70 22 57 19 47 19 C33 19 28 28 28 36 C28 45 38 49 50 51 C62 53 72 57 72 66 C72 76 60 82 47 82 C35 82 29 78 27 70" fill="none" stroke="#fafafa" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="20" cy="78" r="7.5" fill="#2563eb"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 471 B |
Reference in New Issue
Block a user