Compare commits

..

11 Commits

Author SHA1 Message Date
soroush.asadi 4c759851ce Redesign homepage as an Apple-style bento grid
deploy / deploy (push) Successful in 24s
Replace the flat minimal sections with a bento layout (ui-ux-pro-max "Bento
Box Grid" style) while keeping the light theme and single blue accent.

- Bento grid system in CSS: 4-col -> 2-col -> 1-col, varied spans (span-2,
  row-2), 22px tiles, hover lift, dark/accent/tint tile variants
- Hero is now a bento: dark name/anchor tile + value-prop tile + accent
  availability tile + social tile
- Services: bento tiles with a tinted featured tile and a dark AI tile
- Stack: four category tiles (AI/ML tinted)
- Portfolio: featured 2x2 tile + colored covers per project
- Pipeline / expertise / blog / contact kept as different layouts for rhythm

Verified: 4-col desktop, clean 1-col mobile with no horizontal overflow,
no console errors. Tailwind bundle rebuilt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:52:59 +03:30
soroush.asadi 7a7542d77b Translate blog articles to Persian (locale-aware bodies)
deploy / deploy (push) Successful in 25s
The 6 blog post bodies were English-only even in Persian mode. Add natural
Persian translations and select the body by locale (IsFa ? BodyFa : BodyEn),
so a Persian reader gets a fully Persian article. Also removed the em/en
dashes from the English bodies (taste compliance) and stripped stray bidi
control chars (kept ZWNJ).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 12:12:36 +03:30
soroush.asadi 154e06ef54 Localize the budget dropdown to Persian
deploy / deploy (push) Successful in 24s
The budget options were English/USD even in Persian mode. Make them
locale-aware (stable option values) so the FA form is fully Persian.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:13:34 +03:30
soroush.asadi ffba74a727 Cache-busting so deploys are visible immediately
deploy / deploy (push) Successful in 26s
The site sits behind a CDN and shipped static assets with no Cache-Control
and no versioning, so browsers/CDN kept serving stale css/js after a deploy
(the "design didn't change" symptom).

- HTML responses now send Cache-Control: no-cache, no-store, must-revalidate
  so the page itself is always revalidated.
- Static assets get a long cache and are fingerprinted via asp-append-version
  (/css/site.css?v=<contenthash>), so they bust automatically on every change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:03:37 +03:30
soroush.asadi 8896740895 Harden UX & accessibility (ui-ux-pro-max pass)
deploy / deploy (push) Successful in 24s
Audited the site with the ui-ux-pro-max skill. It validated the brand blue
(#2563EB == its SaaS primary) but flagged real high-severity gaps:

- Contrast: muted grays were zinc-400 (~2.8:1, fails WCAG AA). Bumped the
  muted token + all text-zinc-400 to zinc-500 (#71717a, ~4.6:1).
- Touch targets: social buttons 38px -> 44x44 (meets 44pt minimum).
- Cursor + disabled: cursor-pointer on buttons; disabled state dims + blocks.
- Form a11y: required-field asterisks (name/service/budget/message),
  autocomplete on name/company, and role=status aria-live=polite on the
  submit status so screen readers announce success/error.

Kept Syne + system fonts and the blue accent (skill suggested Inter + an
AI-purple palette its own anti-patterns reject). Rebuilt Tailwind bundles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 05:33:44 +03:30
soroush.asadi 93f7873dd1 Reposition from AI-only to engineering + apps + AI
deploy / deploy (push) Successful in 23s
Broaden the messaging so AI is one strong pillar, not the whole story
(matches the real portfolio: web/SaaS, mobile, a game, plus AI tools).

- Hero: "software, enterprise apps, and AI solutions"; role is now
  "Software & AI Engineer, Solution Architect"
- Services reframed: Web & enterprise apps, Mobile apps, Solution
  architecture & cloud, AI solutions, Automation & integrations, Strategy
  (replaces the six AI-centric ones; new "apps" icon)
- Expertise areas lead with architecture + web/enterprise apps, AI as one
- Contact service options, meta description, title, footer blurb updated

English and Persian both. No CSS/JS changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 04:03:24 +03:30
soroush.asadi 255e8d25e5 Humanize all Persian copy across the site
deploy / deploy (push) Successful in 24s
Rewrite the FA strings sitewide in natural, human Persian (English unchanged),
removing translation calques like «معمار راهکار», «هوش مصنوعی تولیدی»,
«موارد کاربری», «چرخه‌های هیجان», «استقرار در تولید», «محیط تولید».

Covers: hero, services, pipeline, stack, expertise, portfolio, blog, contact
(Index), nav/meta/footer (_Layout), the /blog list + per-post FA titles
(BlogIndexModel, PostModel). Also removed two stray English em-dashes in the
blog excerpts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:50:21 +03:30
soroush.asadi 29b5f07ebf Humanize Persian hero copy
deploy / deploy (push) Successful in 24s
The FA hero read as machine-translated calque. Replace with natural Persian:
- subhead: «هوش مصنوعی‌ای می‌سازم که فقط روی کاغذ نمی‌ماند؛ از طراحی تا اجرا، در مقیاس سازمانی.»
- role: «مهندس هوش مصنوعی و معمار نرم‌افزار.» (was the calque «معمار راهکار»)

English copy unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:32:53 +03:30
soroush.asadi b721f01e14 Replace placeholder portfolio with real projects
deploy / deploy (push) Successful in 24s
Swap the fabricated case studies (Atlas/Sentinel/etc. with invented metrics)
for the four real shipped products, each linking to its live site:

- Hamkadr (hamkadr.ir) - healthcare staffing marketplace
- Meezi (meezi.ir) - cafe/restaurant management SaaS
- Barge Vasat (bargevasat.ir) - online Hokm card game
- Flatrender (flatrender.ir) - AI video/image studio

Cards are now external links (2x2 grid), no invented numbers or clients.
Regenerated the purged Tailwind bundle for the new classes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:20:52 +03:30
soroush.asadi 33efeac98f Replace Tailwind Play CDN with a prebuilt, purged stylesheet
deploy / deploy (push) Successful in 37s
The runtime CDN (cdn.tailwindcss.com) is not production-grade: FOUC, no
purging, and an external request that is slow/blocked from some networks.

- Add Tailwind v3 build (package.json `npm run build`) with two scoped configs:
  public (accent + zinc) -> wwwroot/css/tailwind.css, and admin (dark base/
  electric/violet/emerald, separate to avoid the emerald flat-vs-scale clash)
  -> wwwroot/css/tailwind-admin.css. Both minified + content-purged.
- Layouts now link the built CSS instead of the CDN script; built artifacts
  are committed so Docker/CI need no Node step. node_modules stays ignored.
- Verified: utilities (incl. arbitrary values like aspect-[16/9], grid-cols-
  [8rem_1fr]) resolve; public + admin render; no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:06:24 +03:30
soroush.asadi cfff934bdd Fix SQLite advisory: bump SQLitePCLRaw to 3.0.x
The transitive SQLitePCLRaw.lib.e_sqlite3 2.1.11 (via EF Core 10 Sqlite) is
flagged High by GHSA-2m69-gcr7-jv3q, and the 2.x line has no patched release
(first_patched_version: null). Pin SQLitePCLRaw.bundle_e_sqlite3 3.0.3, which
is outside the vulnerable range (<= 2.1.11). Runtime-verified: EnsureCreated
and a DB read both succeed; `dotnet list package --vulnerable` is now clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:06:10 +03:30
17 changed files with 1514 additions and 225 deletions
+2 -2
View File
@@ -11,7 +11,7 @@
<h1 class="@(fa ? "font-fa" : "")" style="font-size:clamp(2rem,4vw,2.75rem)">
@(fa ? "یادداشت‌های مهندسی" : "Engineering notes")
</h1>
<p class="lede mt-4">@(fa ? "یافته‌ها از پروژه‌های واقعی. نه ترجمه‌ی مقاله، نه فهرست هیجان." : "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 class="border-b border-zinc-200">
@@ -20,7 +20,7 @@
<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">
<div class="flex items-baseline justify-between sm:flex-col sm:gap-1">
<span class="kicker">@post.Category</span>
<span class="text-[.78rem] text-zinc-400">@post.ReadTime @(fa ? "دقیقه" : "min")</span>
<span class="text-[.78rem] text-zinc-500">@post.ReadTime @(fa ? "دقیقه" : "min")</span>
</div>
<div>
<h2 class="text-[1.1rem] font-semibold transition-colors group-hover:text-accent @(fa ? "font-fa" : "")">@post.Title</h2>
+8 -8
View File
@@ -10,19 +10,19 @@ public class BlogIndexModel : BasePageModel
{
var fa = IsFa;
Posts = fa ? new BlogPost[]{
new("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در تولید کار می‌کند","چرا BLEU و ROUGE برای RAG ناکافیاند، و معیارهایی که در پروژه‌های واقعی تصمیم می‌سازند.",8),
new("agentic-n8n-patterns","Automation","الگوهای عامل‌محور با n8n برای سازمان",گونه n8n را با LangGraph ترکیب کنیم تا گردش‌کارهای قابل ممیزی بسازیم.",11),
new("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه ضدالگو که در ۸۰٪ پروژه‌های Vertex می‌بینم، و چگونه ۶۰٪ هزینه را کاهش دادیم.",6),
new("k8s-llm-inference","Infra","استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ میلی‌ثانیه","الگوی استقرار با KEDA، GPU sharing، و request hedging برای سرویس‌دهی پایدار.",14),
new("flutter-on-device-ai","Mobile","هوش مصنوعی on-device در Flutter","استفاده از Gemini Nano و LiteRT برای استنتاج آفلاین در اپلیکیشن‌های موبایل.",9),
new("enterprise-ai-roadmap","Strategy","نقشه راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای CTOها می‌سازم — از کشف موارد کاربری تا اولین استقرار تولید.",7),
new("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در عمل جواب می‌دهد","چرا BLEU و ROUGE برای RAG کافی نیستند، و معیارهایی که واقعاً به تصمیم کمک می‌کنند.",8),
new("agentic-n8n-patterns","Automation","الگوهای عامل‌محور با n8n برای سازمان",طور n8n را با LangGraph ترکیب کنیم تا گردش‌کارهای خودکار و قابل‌ردیابی بسازیم.",11),
new("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه اشتباه رایج که در بیشتر پروژه‌های Vertex می‌بینم، و اینکه چطور ۶۰٪ هزینه را کم کردیم.",6),
new("k8s-llm-inference","Infra","اجرای LLM روی Kubernetes با تأخیر زیر ۵۰ میلی‌ثانیه","الگوی استقرار با KEDA، اشتراک GPU و request hedging برای سرویس‌دهی پایدار.",14),
new("flutter-on-device-ai","Mobile","هوش مصنوعی روی دستگاه در Flutter","استفاده از Gemini Nano و LiteRT برای پردازش آفلاین در اپ‌های موبایل.",9),
new("enterprise-ai-roadmap","Strategy","نقشه‌ی راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای مدیران فنی می‌چینم؛ از پیدا کردن بهترین ایده تا اولین اجرای واقعی.",7),
} : 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("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("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),
};
}
}
+1 -1
View File
@@ -21,7 +21,7 @@
<header class="mb-8">
<span class="kicker">@Model.Category</span>
<h1 class="mt-3 @(fa ? "font-fa" : "")" style="font-size:clamp(1.8rem,4vw,2.5rem)">@Model.Title</h1>
<p class="mt-3 text-sm text-zinc-400">@Model.ReadTime @(fa ? "دقیقه مطالعه" : "min read")</p>
<p class="mt-3 text-sm text-zinc-500">@Model.ReadTime @(fa ? "دقیقه مطالعه" : "min read")</p>
</header>
<article class="prose-custom">
+167 -38
View File
@@ -14,24 +14,25 @@ public class PostModel(ContentService content) : BasePageModel
public string BodyHtml { get; private set; } = "";
public bool PostNotFound { get; private set; }
// Default bodies (Markdown-lite, rendered server-side)
private static readonly Dictionary<string, (string Cat, string TitleEn, string TitleFa, int RT, string Body)> _defaults = new()
// 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 BodyEn, string BodyFa)> _defaults = new()
{
["rag-eval-framework"] = ("LLM", "A RAG evaluation framework that holds up in production", "چارچوب ارزیابی RAG که در تولید کار می‌کند", 8, DefaultBodies.RagEval),
["agentic-n8n-patterns"] = ("Automation", "Agentic patterns with n8n for the enterprise", "الگوهای عامل‌محور با n8n برای سازمان", 11, DefaultBodies.N8nPatterns),
["vertex-cost-control"] = ("Google Stack", "Vertex AI cost control at scale", "کنترل هزینه روی Vertex AI در مقیاس بالا", 6, DefaultBodies.VertexCost),
["k8s-llm-inference"] = ("Infra", "Sub-50ms LLM inference on Kubernetes", "استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ ms",14, DefaultBodies.K8sInference),
["flutter-on-device-ai"] = ("Mobile", "On-device AI in Flutter", "هوش مصنوعی on-device در Flutter", 9, DefaultBodies.FlutterAI),
["enterprise-ai-roadmap"] = ("Strategy", "A 90-day enterprise AI roadmap", "نقشه راه هوش مصنوعی سازمانی در ۹۰ روز", 7, DefaultBodies.EnterpriseRoadmap),
["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, DefaultBodies.N8nPatternsFa),
["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 با تأخیر زیر ۵۰ میلی‌ثانیه", 14, DefaultBodies.K8sInference, DefaultBodies.K8sInferenceFa),
["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, DefaultBodies.EnterpriseRoadmapFa),
};
public void OnGet()
{
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,...})
var overrides = content.GetPostOverrides();
string body = def.Body;
if (overrides.TryGetValue(Slug, out var node) && node["body"]?.GetValue<string>() is { } dbBody)
body = dbBody;
@@ -83,13 +84,13 @@ public class PostModel(ContentService content) : BasePageModel
private static string Esc(string s) => s.Replace("&","&amp;").Replace("<","&lt;").Replace(">","&gt;");
}
/// Default article bodies (Markdown).
/// Default article bodies (Markdown). EN + FA per post.
internal static class DefaultBodies
{
public const string RagEval = """
## 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
@@ -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.
**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
Start with a **golden dataset**: 100200 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.
""";
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 = """
## 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
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
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
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 = """
## Anti-pattern 1: calling Gemini Ultra for everything
Gemini Ultra (or GPT-4-class models) costs 1030× 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, 6080% 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
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
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.
""";
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 = """
@@ -155,25 +218,47 @@ A single Kubernetes `Deployment` behind a `ClusterIP` `Service`, fronted by an I
## 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
For models that fit in 48 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
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 = """
## 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
@@ -183,34 +268,78 @@ LiteRT (formerly TensorFlow Lite) handles vision and custom small models. For cl
## 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
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 = """
## Days 130: 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 812 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 3160: 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 15. 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 6190: first production deployment
## Days 61-90: first production deployment
Scope the first deployment to a single team of 1020 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.
""";
public const string EnterpriseRoadmapFa = """
## روز ۱ تا ۳۰: کشف
گرانترین اشتباه در هوش مصنوعی سازمانی، سریعساختنِ چیز اشتباه است. کشف یک تشریفات نیست؛ خودِ کار است.
با ۸ تا ۱۲ ذینفع در واحدهای مختلفِ کسبوکار مصاحبه کنید. از هرکدام بپرسید: کدام کار دستی بیش از دو ساعت در هفته وقت میگیرد؟ کدام تصمیم را با اطلاعات ناقص میگیرید؟ کدام گزارش را آرزو دارید داشته باشید ولی ساختنش گران است؟
نامزدها را روی یک ماتریس ۲×۲ بچینید: **اثر** (درآمد، هزینه، ریسک) در برابر **امکانپذیری** (کیفیت داده، پیچیدگی یکپارچهسازی، محدودیتهای مقرراتی). ربعِ بالا-راست، اولین sprint شماست.
## روز ۳۱ تا ۶۰: نمونهی اولیه و اعتبارسنجی
یک مورد کاربری از ربعِ بالا-راست بردارید. در سه هفته یک نمونهی اولیه بسازید. نمونه لازم نیست در سطح تولید باشد؛ باید **توسط کارشناسان حوزه قابلآزمون** باشد.
یک ارزیابی ساختارمند اجرا کنید: ۱۰۰ پرسش، کارشناس حوزه به هر پاسخ از ۱ تا ۵ امتیاز میدهد. پیش از شروع sprint یک آستانه بگذارید (مثلاً میانگین ۴.۰ یا بالاتر). اگر نمونه از آن گذشت، به سمتِ سختسازی برای تولید بروید. اگر نگذشت، ریشه را پیدا کنید (معمولاً کیفیت داده یا راهبرد قطعهبندی) و بعد منابع مهندسی را متعهد کنید.
## روز ۶۱ تا ۹۰: اولین استقرار تولید
اولین استقرار را به یک تیم ۱۰ تا ۲۰ نفره محدود کنید. این کار شعاع آسیب را کم میکند و سریع دادهی استفادهی واقعی میسازد.
همهچیز را اندازه بگیرید: تأخیر، هزینه به ازای هر پرسش، بازخورد مثبت و منفی کاربرها، امتیاز وفاداری از بسترِ خودکار. هفتگی با صاحب کسبوکار معیارها را مرور کنید. قطعهبندی، راهبرد بازیابی یا ردهی مدل را بر اساس آنچه داده نشان میدهد تنظیم کنید، نه بر اساس حدس.
در روز ۹۰ یک سامانهی زنده، یک بسترِ ارزیابیِ تنظیمشده، و تصویری روشن از اینکه دومین مورد کاربری چه باید باشد دارید. این، پایهی یک نقشهی راهِ ۱۲ماههی معتبر است.
""";
}
+158 -137
View File
@@ -5,35 +5,48 @@
var locale = Model.Locale;
}
<!-- ─── HERO (editorial, centered) ───────────────────────────────────── -->
<section id="top" class="px-5 pt-28 pb-24 sm:px-8 sm:pt-32">
<div class="mx-auto max-w-3xl text-center">
<div class="reveal mb-7 flex justify-center">
<span class="status"><span class="dot"></span>@(fa ? "پذیرای پروژه‌های جدید" : "Available for new projects")</span>
</div>
<!-- ─── HERO (bento) ─────────────────────────────────────────────────── -->
<section id="top" class="px-5 pt-24 pb-12 sm:px-8 sm:pt-28">
<div class="mx-auto max-w-6xl">
<div class="bento">
<h1 class="reveal @(fa ? "font-fa" : "")" style="font-size:clamp(2.6rem,7vw,4.75rem);transition-delay:.05s">
@(fa ? "سروش اسعدی" : "Soroush Asadi")
</h1>
<!-- Name / anchor tile -->
<div class="tile tile-dark span-2 row-2 reveal">
<span class="kicker @(fa ? "font-fa" : "")" style="color:#a1a1aa">@(fa ? "مهندس نرم‌افزار و هوش مصنوعی" : "Software & AI Engineer")</span>
<h1 class="mt-3 @(fa ? "font-fa" : "")" style="font-size:clamp(2.4rem,5.5vw,4rem)">@(fa ? "سروش اسعدی" : "Soroush Asadi")</h1>
<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">
<a href="#contact" class="btn">
@(fa ? "رزرو جلسه" : "Book a call")
<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>
</a>
<a href="#portfolio" class="btn-ghost btn-on-dark">@(fa ? "نمونه‌کارها" : "View work")</a>
</div>
</div>
<p class="reveal mx-auto mt-6 max-w-2xl text-balance leading-snug text-zinc-800" style="font-size:clamp(1.2rem,2.4vw,1.6rem);transition-delay:.1s">
@(fa ? "طراحی و استقرار " : "I build ")<span class="accent-text font-semibold">@(fa ? "هوش مصنوعی تولیدی" : "production-grade AI")</span>@(fa ? " برای سازمان‌ها، از راهبرد تا استقرار در تولید." : " for the enterprise, from strategy to live deployment.")
</p>
<!-- Value-prop tile -->
<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)">
@(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.")
</p>
</div>
<p class="lede reveal mx-auto mt-4 text-[.98rem]" style="transition-delay:.15s">@(fa ? "مهندس هوش مصنوعی، مشاور و معمار راهکار." : "AI Engineer, Consultant, and Solution Architect.")</p>
<!-- Availability tile -->
<div class="tile tile-accent reveal" style="transition-delay:.12s">
<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>
<p class="mt-auto pt-6 text-[1.05rem] font-semibold @(fa ? "font-fa" : "")" style="color:#fff">@(fa ? "پذیرای پروژه‌های جدید" : "Open for new projects")</p>
</div>
<div class="reveal mt-9 flex flex-wrap items-center justify-center gap-3" style="transition-delay:.2s">
<a href="#contact" class="btn">
@(fa ? "رزرو جلسه" : "Book a call")
<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>
</a>
<a href="#portfolio" class="btn-ghost">@(fa ? "نمونه‌کارها" : "View work")</a>
</div>
<!-- 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 class="reveal mt-8 flex items-center justify-center gap-2.5" style="transition-delay:.25s">
<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>
</section>
@@ -42,36 +55,42 @@
<section id="services" class="px-5 py-24 sm:px-8 sm:py-28">
<div class="mx-auto max-w-6xl">
<div class="sec-head">
<h2>@(fa ? "شش حوزه‌ی تخصص" : "Six areas of practice")</h2>
<p class="lede">@(fa ? "از نخستین جلسه‌ی راهبرد تا استقرار تولید؛ یک شریک مهندسی برای کل چرخه‌ی عمر هوش مصنوعی." : "From the first strategy session to production rollout, one engineering partner for the full AI lifecycle.")</p>
<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>
</div>
<div class="grid grid-cols-1 gap-x-10 gap-y-10 sm:grid-cols-2 lg:grid-cols-3">
<div class="bento">
@{
var services = fa ? new[]{
("strategy","راهبرد و نقشه راه هوش مصنوعی","ارزیابی بلوغ سازمانی، شناسایی موارد کاربری با بیشترین بازده، و طراحی نقشه راه ۱۲ تا ۱۸ ماهه با KPIهای روشن.",new[]{"Discovery","ROI Mapping","Roadmap"}),
("automation","اتوماسیون هوش مصنوعی","ساخت عامل‌های خودکار و گردش‌کارهای n8n که فرایندهای دستی را به سامانه‌های قابل ممیزی تبدیل می‌کنند.",new[]{"n8n","Agents","Workflows"}),
("llm-rag","مهندسی LLM و RAG","طراحی pipeline‌های RAG با پایگاه‌های برداری، evaluation framework، و سرویس‌دهی با تأخیر زیر ۵۰ میلی‌ثانیه.",new[]{"RAG","Vector DB","Eval"}),
("architecture","معماری راهکار","طراحی سامانه‌های توزیع‌شده روی Kubernetes با میکروسرویس‌ها، event streaming، و الگوهای پایداری در مقیاس بالا.",new[]{"K8s","Microservices","Event-Driven"}),
("mobile","اپلیکیشن‌های موبایل هوش مصنوعی","برنامه‌های Flutter، Swift و Kotlin با on-device inference، استریم LLM و تجربه‌ی کاربری بومی.",new[]{"Flutter","Swift","Kotlin"}),
("google-stack","تخصص استک گوگل","استقرار روی Vertex AI، GKE و Gemini با بهینه‌سازی هزینه و الگوهای امنیتی سطح enterprise.",new[]{"Vertex AI","GKE","Gemini"}),
("apps","اپلیکیشن‌های وب و سازمانی","پلتفرم‌های وب و SaaS از صفر تا صد: داشبورد، چندمستاجری، صورت‌حساب و پنل مدیریت، ساخته‌شده برای رشد.",new[]{"Web","SaaS","Dashboards"}),
("mobile","اپلیکیشن‌های موبایل","اپ‌های بومی و چندسکویی با Flutter، Swift و Kotlin، با حسی روان و نزدیک به تجربه‌ی بومی.",new[]{"Flutter","Swift","Kotlin"}),
("architecture","معماری راهکار و زیرساخت ابری","سیستم‌های توزیع‌شده روی Kubernetes؛ میکروسرویس، استریم رویداد، و پایداری زیر بار سنگین.",new[]{"K8s","Microservices","Cloud"}),
("llm-rag","راهکارهای هوش مصنوعی","قابلیت‌های LLM و RAG، عامل‌ها و اتوماسیون که داخل محصول واقعی کار می‌کنند، نه فقط دمو.",new[]{"LLM","RAG","Agents"}),
("automation","اتوماسیون و یکپارچه‌سازی","ابزارهایتان را به هم وصل می‌کنم و کارهای دستی را حذف؛ با گردش‌کارهای n8n، API و وب‌هوک.",new[]{"n8n","APIs","Webhooks"}),
("strategy","راهبرد و نقشه‌ی راه","راهبرد فنی، بازبینی معماری، و نقشه‌ی راهی روشن از ایده تا عرضه.",new[]{"Discovery","Architecture","Roadmap"}),
} : new[]{
("strategy","AI Strategy and Roadmap","Maturity assessment, highest-ROI use-case discovery, and a 12 to 18 month roadmap with measurable KPIs.",new[]{"Discovery","ROI Mapping","Roadmap"}),
("automation","AI Automation","Autonomous agents and n8n workflows that turn manual processes into auditable, observable systems.",new[]{"n8n","Agents","Workflows"}),
("llm-rag","LLM and RAG Engineering","Production RAG pipelines with vector stores, evaluation frameworks, and sub-50ms serving.",new[]{"RAG","Vector DB","Eval"}),
("architecture","Solution Architecture","Distributed systems on Kubernetes: microservices, event streaming, and resilience patterns at scale.",new[]{"K8s","Microservices","Event-Driven"}),
("mobile","Mobile AI Apps","Flutter, Swift, and Kotlin apps with on-device inference, streaming LLM UX, and native polish.",new[]{"Flutter","Swift","Kotlin"}),
("google-stack","Google Stack Specialist","Vertex AI, GKE, and Gemini deployments with cost optimization and enterprise security patterns.",new[]{"Vertex AI","GKE","Gemini"}),
("apps","Web & enterprise apps","End-to-end web platforms and SaaS: dashboards, multi-tenant, billing, and admin, built to scale.",new[]{"Web","SaaS","Dashboards"}),
("mobile","Mobile apps","Native and cross-platform apps with Flutter, Swift, and Kotlin, with a smooth native feel.",new[]{"Flutter","Swift","Kotlin"}),
("architecture","Solution architecture & cloud","Distributed systems on Kubernetes: microservices, event streaming, and resilience at scale.",new[]{"K8s","Microservices","Cloud"}),
("llm-rag","AI solutions","LLM and RAG features, agents, and automation built into real products, not just demos.",new[]{"LLM","RAG","Agents"}),
("automation","Automation & integrations","Connect your tools and remove manual work with n8n workflows, APIs, and webhooks.",new[]{"n8n","APIs","Webhooks"}),
("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, tags) in services)
{
<article class="svc reveal border-t border-zinc-200 pt-6" style="transition-delay:@(si * 60)ms">
<span class="svc-icon text-zinc-400" aria-hidden="true">@Html.Raw(ServiceIcon(id))</span>
<h3 class="mt-5 text-lg font-semibold @(fa ? "font-fa" : "")">@title</h3>
<p class="mt-2.5 text-[.95rem] leading-relaxed text-zinc-600">@desc</p>
<div class="mt-4 flex flex-wrap gap-1.5">
var (spanCls, variant) = id switch {
"apps" => ("span-2", "tile-tint"),
"llm-rag" => ("span-2", "tile-dark"),
_ => ("", ""),
};
var descCls = variant == "tile-dark" ? "t-sub" : "text-zinc-600";
<article class="tile reveal @spanCls @variant" style="transition-delay:@(si * 60)ms">
<span class="tile-icon" aria-hidden="true">@Html.Raw(ServiceIcon(id))</span>
<h3 class="mt-4 text-lg font-semibold @(fa ? "font-fa" : "")">@title</h3>
<p class="mt-2.5 text-[.93rem] leading-relaxed @descCls">@desc</p>
<div class="mt-auto flex flex-wrap gap-1.5 pt-5">
@foreach (var tag in tags) { <span class="chip">@tag</span> }
</div>
</article>
@@ -86,17 +105,17 @@
<div class="mx-auto max-w-6xl">
<div class="sec-head">
<h2>@(fa ? "از سند خام تا پاسخ قابل اتکا" : "From raw document to a 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 class="lede">@(fa ? "مسیری که هر پرسش در یک سامانه‌ی RAG واقعی طی می‌کند. هر مرحله را می‌شود اندازه گرفت، دنبال کرد و برای سرعت بهتر کرد." : "The path every query takes through a production RAG system. Each stage is measurable, auditable, and tuned for latency.")</p>
</div>
<ol class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-3 lg:grid-cols-5">
@{
var nodes = fa ? new[]{
("دریافت","نرمال‌سازی، قطعه‌بندی و پاک‌سازی اسناد منبع"),
("برداری‌سازی","تولید embedding و نمایه‌سازی در پایگاه برداری"),
("دریافت","نرمال‌سازی، تکه‌تکه‌کردن و پاک‌سازی سندهای منبع"),
("برداری‌سازی","ساخت embedding و نمایه‌کردن در پایگاه برداری"),
("بازیابی","جستجوی ترکیبی معنایی و کلیدواژه‌ای"),
("بازرتبه‌بندی","مرتب‌سازی مجدد نامزدها با cross-encoder"),
("تولید","پاسخ مستند با ارجاع به منبع"),
("بازرتبه‌بندی","چیدن دوباره‌ی نتایج با cross-encoder"),
("تولید","پاسخ مستند همراه با ذکر منبع"),
} : new[]{
("Ingest","Normalize, chunk, and clean source documents"),
("Embed","Generate embeddings and index in the vector store"),
@@ -110,13 +129,13 @@
{
stepN++;
<li class="reveal border-t border-zinc-200 pt-4" style="transition-delay:@((stepN-1) * 40)ms">
<span class="font-display text-sm text-zinc-400">@stepN.ToString("D2")</span>
<span class="font-display text-sm text-zinc-500">@stepN.ToString("D2")</span>
<h3 class="mt-2 text-base font-semibold @(fa ? "font-fa" : "")">@nlabel</h3>
<p class="mt-1.5 text-[.85rem] leading-relaxed text-zinc-600">@ndesc</p>
</li>
}
</ol>
<p class="mt-8 text-sm text-zinc-500">@(fa ? "تأخیر سرتاسری زیر ۵۰ میلی‌ثانیه؛ هر مرحله مشاهده‌پذیر." : "Sub-50ms end-to-end, every stage observable.")</p>
<p class="mt-8 text-sm text-zinc-500">@(fa ? "تأخیر کل زیر ۵۰ میلی‌ثانیه؛ هر مرحله قابل مشاهده." : "Sub-50ms end-to-end, every stage observable.")</p>
</div>
</section>
@@ -125,10 +144,10 @@
<div class="mx-auto max-w-6xl">
<div class="sec-head">
<h2>@(fa ? "ابزار روزمره" : "Daily tooling")</h2>
<p class="lede">@(fa ? "هر چه می‌سازم بر این پایه‌ها استوار است؛ انتخاب‌شده برای دوام، نه چرخه‌های هیجان." : "Everything I ship sits on this foundation, chosen for longevity, not hype cycles.")</p>
<p class="lede">@(fa ? "هر چیزی که می‌سازم روی این‌ها بنا می‌شود؛ انتخاب‌شان کرده‌ام چون می‌مانند، نه چون مد روزند." : "Everything I ship sits on this foundation, chosen for longevity, not hype cycles.")</p>
</div>
<div class="grid grid-cols-1 gap-x-8 gap-y-9 sm:grid-cols-2 lg:grid-cols-4">
<div class="bento">
@{
var cats = fa ? new[]{
("زبان‌ها", new[]{"Python","TypeScript","Go","Rust","SQL"}),
@@ -142,14 +161,16 @@
("AI / ML", new[]{"Vertex AI","Gemini","OpenAI","Anthropic","LangGraph","Pinecone","pgvector"}),
};
}
@{ int ci = 0; }
@foreach (var (catLabel, items) in cats)
{
<div class="reveal border-t border-zinc-200 pt-5">
<h3 class="mb-4 text-sm font-semibold @(fa ? "font-fa" : "")">@catLabel</h3>
<div class="flex flex-wrap gap-1.5">
<div class="tile reveal @(ci == 3 ? "tile-tint" : "")" style="transition-delay:@(ci * 60)ms">
<h3 class="text-sm font-semibold @(fa ? "font-fa" : "")">@catLabel</h3>
<div class="mt-4 flex flex-wrap gap-1.5">
@foreach (var item in items) { <span class="chip">@item</span> }
</div>
</div>
ci++;
}
</div>
</div>
@@ -159,24 +180,24 @@
<section id="expertise" class="px-5 py-24 sm:px-8 sm:py-28">
<div class="mx-auto max-w-4xl">
<div class="sec-head">
<h2>@(fa ? "آنچه در آن عمیق می‌شوم" : "What I go deep on")</h2>
<p class="lede">@(fa ? "سامانه‌هایی که میلیون‌ها رویداد در روز را دوام می‌آورند. این‌ها حوزه‌هایی‌اند که برایشان بهینه می‌کنم." : "Systems that survive millions of events per day. These are the areas I optimize for.")</p>
<h2>@(fa ? "جاهایی که عمیق شده‌ام" : "What I go deep on")</h2>
<p class="lede">@(fa ? "سامانه‌هایی که روزانه میلیون‌ها رویداد را تاب می‌آورند. این‌ها همان چیزهایی‌اند که سال‌ها رویشان کار کرده‌ام." : "Systems that survive millions of events per day. These are the areas I optimize for.")</p>
</div>
<dl>
@{
var areas = fa ? new[]{
("مهندسی LLM و RAG","پایپ‌لاین‌های بازیابی، ارزیابی و تولید مستند در محیط تولید."),
("معماری ابری و Kubernetes","سرویس‌های توزیع‌شده، مقیاس خودکار و پایداری در مقیاس بالا."),
("سیستم‌های عامل‌محور و اتوماسیون","گردش‌کارهای خودکار قابل ممیزی با n8n و LangGraph."),
("استک گوگل کلود (Vertex / GKE)","Vertex AI، GKE و Gemini با انضباط هزینه."),
("موبایل بومی و cross-platform","Flutter، Swift و Kotlin با استنتاج روی دستگاه."),
("معماری نرم‌افزار و سیستم‌های توزیع‌شده","میکروسرویس، استریم رویداد و الگوهای پایداری زیر بار سنگین."),
("اپلیکیشن‌های وب و سازمانی","پلتفرم‌های چندمستاجری، داشبورد و سیستم‌های پرترافیک."),
("راهکارهای هوش مصنوعی (LLM و RAG)","بازیابی، ارزیابی و تولید پاسخ مستند، داخل محصول واقعی."),
("زیرساخت ابری و Kubernetes","استقرار، مقیاس‌پذیری خودکار و حواس‌جمعی روی هزینه."),
("موبایل بومی و چندسکویی","Flutter، Swift و Kotlin برای اپ‌های روان و سریع."),
} : new[]{
("LLM and RAG engineering","Retrieval pipelines, evals, and grounded generation in production."),
("Cloud architecture and Kubernetes","Distributed services, autoscaling, and resilience at scale."),
("Agentic systems and automation","Auditable autonomous workflows with n8n and LangGraph."),
("Google Cloud stack (Vertex / GKE)","Vertex AI, GKE, and Gemini with real cost discipline."),
("Native and cross-platform mobile","Flutter, Swift, and Kotlin with on-device inference."),
("Software architecture & distributed systems","Microservices, event streaming, and resilience patterns at scale."),
("Web & enterprise applications","Multi-tenant platforms, dashboards, and high-traffic systems."),
("AI solutions (LLM & RAG)","Retrieval, evals, and grounded generation, inside real products."),
("Cloud infrastructure & Kubernetes","Deployment, autoscaling, and real cost discipline."),
("Native & cross-platform mobile","Flutter, Swift, and Kotlin for smooth, fast apps."),
};
}
@foreach (var (alabel, adesc) in areas)
@@ -195,52 +216,52 @@
<div class="mx-auto max-w-6xl">
<div class="sec-head">
<h2>@(fa ? "نمونه‌کارهای منتخب" : "Selected work")</h2>
<p class="lede">@(fa ? "گزیده‌ای از پروژه‌های واقعی در حوزه‌ی هوش مصنوعی، داده و موبایل." : "A selection of real engagements across AI, data, and mobile.")</p>
<p class="lede">@(fa ? "محصولاتی که خودم طراحی و ساخته‌ام. روی هر کارت بزنید تا خودِ سایت را ببینید." : "Products I have designed and built. Tap any card to open the live site.")</p>
</div>
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div class="bento">
@{
var projects = fa ? new[]{
("atlas-rag","اطلس - پلتفرم RAG سازمانی","بانک ردیف‌اول","۲۰۲۵","دستیار دانش روی بیش از ۴ میلیون سند داخلی؛ بازیابی ترکیبی با pgvector و reranker.",new[]{"RAG","pgvector","Vertex AI"},new[]{("۴M+","سند نمایه‌شده"),("۳۸ms","تأخیر p95"),("۹۲٪","دقت پاسخ")}),
("sentinel-agents","Sentinel - اتوماسیون Ops عامل‌محور","SaaS scale-up","۲۰۲۵","پاسخ خودکار به حوادث با ترکیب n8n و LangGraph؛ عامل‌های قابل ممیزی که alert تریاژ می‌کنند.",new[]{"n8n","LangGraph","Agents"},new[]{("۷۰٪","کاهش MTTR"),("۲۴/۷","پوشش on-call"),("۱۵۰+","جریان خودکار")}),
("vertex-vision","Vertex Vision - استنتاج بینایی بلادرنگ","زنجیره خرده‌فروشی","۲۰۲۴","استنتاج بینایی بلادرنگ روی GKE با Triton و Vertex AI برای تحلیل قفسه و جریان مشتری.",new[]{"Vertex AI","GKE","Triton"},new[]{("۱.۲B","استنتاج ماهانه"),("۳۰۰+","فروشگاه"),("۶۰٪","کاهش هزینه")}),
("mirage-mobile","Mirage - مجموعه هوش مصنوعی on-device","محصول مصرفی","۲۰۲۴","اپلیکیشن Flutter با استنتاج کاملاً آفلاین با Gemini Nano و LiteRT.",new[]{"Flutter","Gemini Nano","LiteRT"},new[]{("۰","وابستگی شبکه"),("<80ms","پاسخ"),("۴.۸★","امتیاز کاربران")}),
("flux-stream","Flux - مش داده رویدادمحور","پلتفرم لجستیک","۲۰۲۳","ستون استریمینگ روی Kafka و NATS روی Kubernetes؛ ۴۰+ میکروسرویس با الگوهای پایداری.",new[]{"Kafka","NATS","Go"},new[]{("۴۰+","میکروسرویس"),("۲M/s","رویداد بر ثانیه"),("۹۹.۹٪","uptime")}),
("oracle-forecast","Oracle - موتور پیش‌بینی تقاضا","زنجیره تامین","۲۰۲۳","پایپ‌لاین پیش‌بینی سری زمانی روی BigQuery و dbt با بازآموزی خودکار.",new[]{"BigQuery","dbt","MLOps"},new[]{("۲۳٪","کاهش ضایعات"),("۸۹٪","دقت پیش‌بینی"),("روزانه","بازآموزی")}),
("hamkadr","همکادر","hamkadr.ir","بازاری که کادر درمان را برای شیفت و استخدام به بیمارستان‌ها و کلینیک‌ها وصل می‌کند؛ با پروفایل، فیلتر، تقویم هفتگی و اپ موبایل.",new[]{"Marketplace","Healthcare","Mobile"}),
("meezi","میزی","meezi.ir","سامانه‌ی یکپارچه برای کافه و رستوران: سفارش با QR، صندوق فروش، انبار، کارکنان و تحلیل فروش، روی زیرساخت داخلی.",new[]{"SaaS","POS","Analytics"}),
("bargevasat","برگ وسط","bargevasat.ir","بازی آنلاین حکم به‌صورت بلادرنگ مقابل بازیکنان واقعی یا ربات‌های هوشمند؛ با لیگ، رتبه‌بندی، جایزه‌ی روزانه و همگام‌سازی چنددستگاهه.",new[]{"Realtime","Multiplayer","Game"}),
("flatrender","فلت‌رندر","flatrender.ir","استودیوی هوش مصنوعی که بیش از ۱۲۰۰ قالب را در چند دقیقه به ویدیو و تصویر آماده‌ی انتشار تبدیل می‌کند؛ بر پایه‌ی ثانیه‌ی رندر. در نسخه‌ی بتا.",new[]{"AI","Video","SaaS"}),
} : new[]{
("atlas-rag","Atlas - Enterprise RAG Platform","Tier-1 bank","2025","A knowledge assistant over 4M+ internal documents. Hybrid retrieval with pgvector and a reranker, sub-40ms serving.",new[]{"RAG","pgvector","Vertex AI"},new[]{("4M+","docs indexed"),("38ms","p95 latency"),("92%","answer accuracy")}),
("sentinel-agents","Sentinel - Agentic Ops Automation","SaaS scale-up","2025","Autonomous incident response combining n8n and LangGraph. Auditable agents that triage alerts and self-heal.",new[]{"n8n","LangGraph","Agents"},new[]{("70%","MTTR cut"),("24/7","on-call cover"),("150+","automated flows")}),
("vertex-vision","Vertex Vision - Realtime Vision Inference","Retail chain","2024","Real-time vision inference on GKE with Triton and Vertex AI for shelf analytics and customer flow across 300+ stores.",new[]{"Vertex AI","GKE","Triton"},new[]{("1.2B","inferences / mo"),("300+","stores"),("60%","GPU cost cut")}),
("mirage-mobile","Mirage - On-device AI Suite","Consumer product","2024","A Flutter app with fully offline inference via Gemini Nano and LiteRT. Streaming response UX with zero network dependency.",new[]{"Flutter","Gemini Nano","LiteRT"},new[]{("0","network deps"),("<80ms","response"),("4.8★","user rating")}),
("flux-stream","Flux - Event-Driven Data Mesh","Logistics platform","2023","Streaming backbone on Kafka and NATS over Kubernetes. 40+ microservices with resilience and exactly-once delivery.",new[]{"Kafka","NATS","Go"},new[]{("40+","microservices"),("2M/s","events / sec"),("99.9%","uptime")}),
("oracle-forecast","Oracle - Demand Forecasting Engine","Supply chain","2023","Time-series forecasting pipeline on BigQuery and dbt with automated retraining, reducing inventory waste significantly.",new[]{"BigQuery","dbt","MLOps"},new[]{("23%","waste cut"),("89%","forecast accuracy"),("daily","retraining")}),
("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"}),
("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"}),
("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"}),
("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"}),
};
}
@foreach (var (pid, ptitle, pclient, pyear, psummary, ptags, pmetrics) in projects)
@{ int pi = 0; }
@foreach (var (pid, pname, pdomain, pdesc, ptags) in projects)
{
var initial = char.ToUpperInvariant(pid[0]);
<article class="card card-link reveal overflow-hidden">
<div class="cover flex aspect-[16/9] items-center justify-center bg-zinc-100" aria-hidden="true">
<span class="font-display text-5xl font-bold text-zinc-300">@initial</span>
var (spanCls, coverBg, coverFg) = pi switch {
0 => ("span-2 row-2", "#18181b", "#fafafa"),
1 => ("span-2", "#2563eb", "#ffffff"),
2 => ("", "#eff4ff", "#2563eb"),
_ => ("", "#f4f4f5", "#a1a1aa"),
};
<a href="https://@pdomain" target="_blank" rel="noopener" aria-label="@pname"
class="group tile tile-link reveal @spanCls" style="padding:0;transition-delay:@(pi * 60)ms">
<div class="pcover" style="background:@coverBg;@(pi == 0 ? "min-height:210px" : "min-height:104px")">
<span class="font-display font-bold" style="font-size:@(pi == 0 ? "5rem" : "2.6rem");color:@coverFg">@initial</span>
</div>
<div class="p-5">
<div class="flex flex-1 flex-col p-5">
<div class="mb-3 flex flex-wrap gap-1.5">
@foreach (var tag in ptags) { <span class="chip">@tag</span> }
</div>
<h3 class="text-[1.05rem] font-semibold @(fa ? "font-fa" : "")">@ptitle</h3>
<p class="mt-1 text-[.8rem] text-zinc-500">@pclient · @pyear</p>
<p class="mt-3 text-[.88rem] leading-relaxed text-zinc-600">@psummary</p>
<div class="mt-4 grid grid-cols-3 gap-3 border-t border-zinc-200 pt-4">
@foreach (var (mv, ml) in pmetrics)
{
<div>
<div class="font-display text-base font-bold text-zinc-900">@mv</div>
<div class="mt-0.5 text-[.68rem] leading-tight text-zinc-500">@ml</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>
</article>
</a>
pi++;
}
</div>
</div>
@@ -251,18 +272,18 @@
<div class="mx-auto max-w-4xl">
<div class="sec-head">
<h2>@(fa ? "یادداشت‌های مهندسی" : "Engineering notes")</h2>
<p class="lede">@(fa ? "یافته‌ها از پروژه‌های واقعی. نه ترجمه‌ی مقاله، نه فهرست هیجان." : "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 class="border-b border-zinc-200">
@{
var posts = fa ? new[]{
("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در تولید دوام می‌آورد","چرا BLEU و ROUGE برای RAG کافی نیستند، و معیارهایی که در پروژه‌های واقعی تصمیم می‌سازند.",8),
("agentic-n8n-patterns","Automation","الگوهای عامل‌محور با n8n برای سازمان","چگونه n8n را با LangGraph ترکیب کنیم تا گردش‌کارهای قابل ممیزی بسازیم.",11),
("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه ضدالگو که در ۸۰٪ پروژه‌های Vertex می‌بینم، و چگونه ۶۰٪ هزینه را کاهش دادیم.",6),
("k8s-llm-inference","Infra","استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ میلی‌ثانیه","الگوی استقرار با KEDA، GPU sharing، و request hedging برای سرویس‌دهی پایدار.",14),
("flutter-on-device-ai","Mobile","هوش مصنوعی on-device در Flutter","استفاده از Gemini Nano و LiteRT برای استنتاج آفلاین در اپلیکیشن‌های موبایل.",9),
("enterprise-ai-roadmap","Strategy","نقشه راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای CTOها می‌سازم؛ از کشف موارد کاربری تا اولین استقرار تولید.",7),
("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در عمل جواب می‌دهد","چرا BLEU و ROUGE برای RAG کافی نیستند، و معیارهایی که واقعاً به تصمیم کمک می‌کنند.",8),
("agentic-n8n-patterns","Automation","الگوهای عامل‌محور با n8n برای سازمان","چطور n8n را با LangGraph ترکیب کنیم تا گردش‌کارهای خودکار و قابل‌ردیابی بسازیم.",11),
("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه اشتباه رایج که در بیشتر پروژه‌های Vertex می‌بینم، و اینکه چطور ۶۰٪ هزینه را کم کردیم.",6),
("k8s-llm-inference","Infra","اجرای LLM روی Kubernetes با تأخیر زیر ۵۰ میلی‌ثانیه","الگوی استقرار با KEDA، اشتراک GPU و request hedging برای سرویس‌دهی پایدار.",14),
("flutter-on-device-ai","Mobile","هوش مصنوعی روی دستگاه در Flutter","استفاده از Gemini Nano و LiteRT برای پردازش آفلاین در اپ‌های موبایل.",9),
("enterprise-ai-roadmap","Strategy","نقشه‌ی راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای مدیران فنی می‌چینم؛ از پیدا کردن بهترین ایده تا اولین اجرای واقعی.",7),
} : 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),
("agentic-n8n-patterns","Automation","Agentic patterns with n8n for the enterprise","How to combine n8n with LangGraph to build auditable, debuggable autonomous workflows.",11),
@@ -277,7 +298,7 @@
<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">
<div class="flex items-baseline justify-between sm:flex-col sm:gap-1">
<span class="kicker">@cat</span>
<span class="text-[.78rem] text-zinc-400">@readTime @(fa ? "دقیقه" : "min")</span>
<span class="text-[.78rem] text-zinc-500">@readTime @(fa ? "دقیقه" : "min")</span>
</div>
<div>
<h3 class="text-[1.1rem] font-semibold transition-colors group-hover:text-accent @(fa ? "font-fa" : "")">@btitle</h3>
@@ -298,69 +319,69 @@
<div class="mx-auto max-w-2xl">
<div class="sec-head">
<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 class="lede">@(fa ? "بدون هزینه، بدون تعهد. با هم می‌بینیم چه می‌خواهید، چه محدودیت‌هایی هست، و قدم بعد چیست." : "No cost, no commitment. We map the use case, the constraints, and the next step together.")</p>
</div>
<form id="contact-form" class="card space-y-5 p-6 sm:p-8"
data-success-msg="@(fa ? "پیام ارسال شد. معمولاً ظرف ۲۴ ساعت کاری پاسخ می‌دهم." : "Sent. Typical reply within 24 working hours.")"
data-error-msg="@(fa ? "خطایی رخ داد. لطفاً دوباره امتحان کنید." : "Something went wrong. Please try again.")">
data-success-msg="@(fa ? "پیام رسید! معمولاً ظرف ۲۴ ساعت کاری جواب می‌دهم." : "Sent. Typical reply within 24 working hours.")"
data-error-msg="@(fa ? "یک مشکلی پیش آمد. لطفاً دوباره تلاش کنید." : "Something went wrong. Please try again.")">
<input type="hidden" name="locale" value="@locale" />
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div>
<label class="flabel" for="name">@(fa ? "نام" : "Name")</label>
<input id="name" name="name" type="text" required placeholder="@(fa ? "نام و نام خانوادگی" : "Full name")" class="field" />
<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 autocomplete="name" placeholder="@(fa ? "نام و نام خانوادگی" : "Full name")" class="field" />
</div>
<div>
<label class="flabel" for="company">@(fa ? "سازمان" : "Company")</label>
<input id="company" name="company" type="text" placeholder="@(fa ? "نام سازمان" : "Organization")" class="field" />
<input id="company" name="company" type="text" autocomplete="organization" placeholder="@(fa ? "نام سازمان" : "Organization")" class="field" />
</div>
</div>
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div>
<label class="flabel" 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="field">
<option value="" disabled selected>@(fa ? "انتخاب کنید" : "Select…")</option>
@if (fa)
{
<option value="strategy">راهبرد و نقشه راه</option>
<option value="automation">اتوماسیون هوش مصنوعی</option>
<option value="llm-rag">مهندسی LLM و RAG</option>
<option value="architecture">معماری راهکار</option>
<option value="mobile">موبایل</option>
<option value="google-stack">استک گوگل</option>
<option value="apps">اپلیکیشن وب و سازمانی</option>
<option value="mobile">اپلیکیشن موبایل</option>
<option value="architecture">معماری و زیرساخت ابری</option>
<option value="ai">راهکار هوش مصنوعی</option>
<option value="automation">اتوماسیون و یکپارچه‌سازی</option>
<option value="strategy">راهبرد و نقشه‌ی راه</option>
}
else
{
<option value="strategy">AI Strategy and Roadmap</option>
<option value="automation">AI Automation</option>
<option value="llm-rag">LLM and RAG Engineering</option>
<option value="architecture">Solution Architecture</option>
<option value="mobile">Mobile AI Apps</option>
<option value="google-stack">Google Stack</option>
<option value="apps">Web & enterprise apps</option>
<option value="mobile">Mobile apps</option>
<option value="architecture">Solution architecture & cloud</option>
<option value="ai">AI solutions</option>
<option value="automation">Automation & integrations</option>
<option value="strategy">Strategy & roadmap</option>
}
</select>
</div>
<div>
<label class="flabel" 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="field">
<option value="" disabled selected>@(fa ? "انتخاب کنید" : "Select…")</option>
<option>Under $10k</option>
<option>$10k - $50k</option>
<option>$50k - $200k</option>
<option>$200k+</option>
<option value="under-10k">@(fa ? "زیر ۱۰ هزار دلار" : "Under $10k")</option>
<option value="10-50k">@(fa ? "۱۰ تا ۵۰ هزار دلار" : "$10k - $50k")</option>
<option value="50-200k">@(fa ? "۵۰ تا ۲۰۰ هزار دلار" : "$50k - $200k")</option>
<option value="200k-plus">@(fa ? "بیش از ۲۰۰ هزار دلار" : "$200k+")</option>
</select>
</div>
</div>
<div>
<label class="flabel" for="message">@(fa ? "پیام" : "Message")</label>
<textarea id="message" name="message" required rows="4" placeholder="@(fa ? "هدف، بازه زمانی، موانع فعلی…" : "Goal, timeline, current blockers…")" class="field resize-none"></textarea>
<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="field resize-none"></textarea>
</div>
<button type="submit" class="btn w-full">@(fa ? "ارسال درخواست" : "Send request")</button>
<p id="contact-status" class="mt-1 text-sm text-zinc-500">@(fa ? "معمولاً ظرف ۲۴ ساعت کاری پاسخ می‌دهم." : "Typical reply within 24 working hours.")</p>
<button type="submit" class="btn w-full">@(fa ? "ارسال پیام" : "Send request")</button>
<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>
</form>
</div>
</section>
@@ -372,7 +393,7 @@
"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="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="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="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>""",
};
}
+3 -10
View File
@@ -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:'SpaceMono'; src:url('/fonts/SpaceMono-Regular.woff2') format('woff2'); font-display:swap; }
</style>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
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" />
<!-- Tailwind: prebuilt + purged admin stylesheet (`npm run build`). No runtime CDN. -->
<link rel="stylesheet" href="/css/tailwind-admin.css" asp-append-version="true" />
<link rel="stylesheet" href="/css/site.css" asp-append-version="true" />
</head>
<body class="min-h-screen bg-base text-slate-200 antialiased">
<div class="flex min-h-screen">
+12 -26
View File
@@ -5,8 +5,8 @@
var dir = isRtl ? "rtl" : "ltr";
var lang = locale == "fa" ? "fa" : "en";
var title = (string?)ViewData["Title"] ?? (locale == "fa"
? "سروش اسعدی - مهندس هوش مصنوعی، مشاور، معمار راهکار"
: "Soroush Asadi - AI Engineer, Consultant, Solution Architect");
? "سروش اسعدی - مهندس نرم‌افزار و هوش مصنوعی"
: "Soroush Asadi - Software & AI Engineer");
}
<!doctype html>
<html lang="@lang" dir="@dir">
@@ -15,8 +15,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>@title</title>
<meta name="description" content="@(locale == "fa"
? "طراحی و پیاده‌سازی سامانههای هوش مصنوعی در مقیاس سازمانی. راهبرد، LLM و RAG، اتوماسیون عامل‌محور، زیرساخت ابری و استک گوگل."
: "Designing and deploying enterprise-grade AI systems. Strategy, LLM and RAG, agentic automation, cloud infrastructure, and the Google stack.")" />
? "نرم‌افزار، اپلیکیشن‌های سازمانی و راهکارهای هوش مصنوعی می‌سازم: پلتفرم‌های وب و موبایل، سیستم‌های توزیع‌شده، زیرساخت ابری و قابلیت‌های LLM و RAG که به تولید می‌رسند."
: "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: Syne (display) + Vazirmatn (Persian). Body is system sans. -->
@@ -25,24 +25,10 @@
@@font-face { font-family:'Vazirmatn'; src:url('/fonts/Vazirmatn-Arabic.woff2') format('woff2'); font-display:swap; }
</style>
<!-- Tailwind Play CDN - minimal config: one accent, neutral zinc scale is built in -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: { accent: '#2563eb', accentink: '#1d4ed8' },
fontFamily: {
display: ['Syne', 'system-ui', 'sans-serif'],
fa: ['Vazirmatn', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<link rel="stylesheet" href="/css/site.css" />
<link rel="icon" href="/logo-mark.svg" type="image/svg+xml" />
<!-- Tailwind: prebuilt + purged stylesheet (`npm run build`). No runtime CDN. -->
<link rel="stylesheet" href="/css/tailwind.css" asp-append-version="true" />
<link rel="stylesheet" href="/css/site.css" asp-append-version="true" />
<link rel="icon" href="/logo-mark.svg" type="image/svg+xml" asp-append-version="true" />
</head>
<body class="site antialiased">
@@ -140,7 +126,7 @@
<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 ? "مهندسی سامانههای هوش مصنوعی برای سازمان‌ها؛ از راهبرد تا استقرار در تولید." : "AI systems engineering for the enterprise, from strategy to live deployment.")</p>
<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>
@@ -164,12 +150,12 @@
</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-400">© 2026 Soroush Asadi. @(fa ? "تمام حقوق محفوظ است." : "All rights reserved.")</p>
<p class="text-[.78rem] text-zinc-400">@(fa ? "ساخته‌شده با دقت در تهران." : "Built with care in Tehran.")</p>
<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></script>
<script src="/js/app.js" defer asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
+23 -1
View File
@@ -50,7 +50,29 @@ using (var scope = app.Services.CreateScope())
}
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/*
var uploadsPath = Path.Combine(dataDir, "uploads");
+3
View File
@@ -13,6 +13,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</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>
</Project>
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+1028
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -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"
}
}
+26
View File
@@ -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: [],
};
+24
View File
@@ -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: [],
};
+39 -2
View File
@@ -10,7 +10,7 @@
--surface: #ffffff;
--text: #18181b; /* zinc-900 */
--text-2: #52525b; /* zinc-600 */
--text-3: #a1a1aa; /* zinc-400 */
--text-3: #71717a; /* zinc-500 - meets WCAG AA 4.5:1 on the off-white bg */
--line: #e4e4e7; /* zinc-200 */
--line-strong: #d4d4d8; /* zinc-300 */
--accent: #2563eb; /* blue-600 - the single accent */
@@ -108,6 +108,10 @@ body.site {
.btn-ghost:hover { border-color: var(--text); background: #fff; }
.btn-ghost:active { transform: translateY(1px); }
/* pointer affordance + disabled semantics (ui-ux-pro-max: cursor-pointer, disabled-states) */
.site button { cursor: pointer; }
.site button:disabled { cursor: not-allowed; opacity: .55; }
/* ─── Availability status ────────────────────────────────────────────── */
.status {
display: inline-flex; align-items: center; gap: .5rem;
@@ -121,7 +125,7 @@ body.site {
/* ─── Social ─────────────────────────────────────────────────────────── */
.social {
display: inline-flex; align-items: center; justify-content: center;
width: 38px; height: 38px; border: 1px solid var(--line-strong); border-radius: var(--radius);
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); }
@@ -168,6 +172,39 @@ body.site {
.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 {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long