first commit
ci / build (push) Failing after 23s
deploy / deploy (push) Failing after 10m12s

This commit is contained in:
soroush.asadi
2026-05-31 12:47:02 +03:30
commit add78d8460
100 changed files with 15221 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
/**
* Stateless admin session — a single password gates the whole CMS.
*
* On login we mint an HMAC-signed token (payload + signature) and store it in
* an httpOnly cookie. Verification re-computes the HMAC and checks expiry.
* Everything here uses the Web Crypto API only (no `node:crypto`) so the same
* code runs in Edge middleware AND in Node route handlers.
*/
export const SESSION_COOKIE = 'sa_admin';
export const SESSION_MAX_AGE = 60 * 60 * 24 * 7; // 7 days, in seconds
const enc = new TextEncoder();
const dec = new TextDecoder();
/** Signing secret. In production set ADMIN_SESSION_SECRET (or it derives from
* ADMIN_PASSWORD). A loud, obviously-insecure default keeps dev frictionless. */
function getSecret(): string {
return (
process.env.ADMIN_SESSION_SECRET ||
process.env.ADMIN_PASSWORD ||
'dev-insecure-secret-change-me'
);
}
/** The single admin password. Falls back to "admin" in non-production only. */
function getPassword(): string | undefined {
if (process.env.ADMIN_PASSWORD) return process.env.ADMIN_PASSWORD;
if (process.env.NODE_ENV !== 'production') return 'admin';
return undefined;
}
function toB64url(bytes: ArrayBuffer | Uint8Array): string {
const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
let bin = '';
for (let i = 0; i < arr.length; i++) bin += String.fromCharCode(arr[i]);
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function fromB64url(str: string): Uint8Array {
const b64 = str.replace(/-/g, '+').replace(/_/g, '/') + '=='.slice((str.length + 3) % 4);
const bin = atob(b64);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
async function hmacKey(): Promise<CryptoKey> {
return crypto.subtle.importKey(
'raw',
enc.encode(getSecret()),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify'],
);
}
async function sha256Hex(input: string): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', enc.encode(input));
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
}
/** Constant-time comparison of two equal-length hex strings. */
function timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
/** Mint a signed session token. */
export async function createSession(): Promise<string> {
const payload = { iat: Date.now(), exp: Date.now() + SESSION_MAX_AGE * 1000 };
const data = toB64url(enc.encode(JSON.stringify(payload)));
const sig = await crypto.subtle.sign('HMAC', await hmacKey(), enc.encode(data));
return `${data}.${toB64url(sig)}`;
}
/** True when the token's signature is valid and it has not expired. */
export async function verifySession(token?: string | null): Promise<boolean> {
if (!token) return false;
const dot = token.indexOf('.');
if (dot <= 0) return false;
const data = token.slice(0, dot);
const sig = token.slice(dot + 1);
try {
const valid = await crypto.subtle.verify(
'HMAC',
await hmacKey(),
fromB64url(sig),
enc.encode(data),
);
if (!valid) return false;
const payload = JSON.parse(dec.decode(fromB64url(data))) as { exp?: number };
return typeof payload.exp === 'number' && payload.exp > Date.now();
} catch {
return false;
}
}
/** Check a submitted password against the configured one (constant-time). */
export async function verifyPassword(input: string): Promise<boolean> {
const expected = getPassword();
if (!expected) return false; // No password set in production => locked out.
const [a, b] = await Promise.all([sha256Hex(input), sha256Hex(expected)]);
return timingSafeEqual(a, b);
}
/** Whether an admin password is configured (used to warn in the UI). */
export function passwordConfigured(): boolean {
return Boolean(getPassword());
}
+57
View File
@@ -0,0 +1,57 @@
import 'server-only';
import { dict, type Dict } from '@/lib/i18n/dictionaries';
import { getAllSections } from '@/lib/db/store';
/**
* The shape handed to the client: a fully-resolved bilingual content tree.
* It is structurally identical to `dict` so `LocaleProvider` can drop it in
* without any component being aware the data now comes from a database.
*/
export type SiteContent = { fa: Dict; en: Dict };
/**
* Build the live content tree: start from the in-code `dict` defaults, then
* overlay any per-section overrides saved through the admin panel. Each stored
* override is `{ fa, en }` for one top-level section key and replaces that
* subtree wholesale (the admin always edits and saves a complete section).
*/
export function loadContent(): SiteContent {
// Shallow clone the locale roots so we can swap section subtrees safely.
// `as const` gives fa/en distinct literal types, so cast through unknown.
const fa = { ...dict.fa } as unknown as Dict;
const en = { ...dict.en } as unknown as Dict;
for (const row of getAllSections()) {
let payload: { fa?: unknown; en?: unknown };
try {
payload = JSON.parse(row.data);
} catch {
continue;
}
const key = row.key as keyof Dict;
if (payload.fa !== undefined) (fa as Record<string, unknown>)[key] = payload.fa;
if (payload.en !== undefined) (en as Record<string, unknown>)[key] = payload.en;
}
return { fa, en };
}
/**
* Resolve a single section for the admin editor: the saved override if one
* exists, otherwise the in-code default for both locales.
*/
export function loadSection(key: keyof Dict): { fa: unknown; en: unknown } {
for (const row of getAllSections()) {
if (row.key !== key) continue;
try {
const payload = JSON.parse(row.data);
return {
fa: payload.fa ?? dict.fa[key],
en: payload.en ?? dict.en[key],
};
} catch {
break;
}
}
return { fa: dict.fa[key], en: dict.en[key] };
}
+45
View File
@@ -0,0 +1,45 @@
import 'server-only';
import { POSTS, POST_SLUGS, type PostContent } from '@/lib/content/posts';
import { getSection } from '@/lib/db/store';
/**
* Live blog bodies = in-code `POSTS` defaults overlaid with admin edits.
*
* Unlike the `{ fa, en }` section overrides, the blog override stored under the
* `posts` key is a *partial* map of `slug -> PostContent` holding only the
* articles that have been edited. Reverting a single article just drops its key
* from that map, so the in-code default shows through again.
*/
export const POSTS_KEY = 'posts';
/** Only the edited articles (empty when nothing has been customized). */
export function loadPostOverrides(): Record<string, PostContent> {
try {
const row = getSection(POSTS_KEY);
if (row && row.data && typeof row.data === 'object' && !Array.isArray(row.data)) {
return row.data as Record<string, PostContent>;
}
} catch {
// A missing or locked DB must never crash a public render — defaults only.
}
return {};
}
/** Defaults merged with overrides — the full, live article set. */
export function loadAllPosts(): Record<string, PostContent> {
return { ...POSTS, ...loadPostOverrides() };
}
export function loadPost(slug: string): PostContent | undefined {
return loadAllPosts()[slug];
}
export function getPostSlugs(): string[] {
return Object.keys(loadAllPosts());
}
/** A slug is editable only if it ships with a default (and thus a blog card). */
export function isKnownSlug(slug: string): boolean {
return (POST_SLUGS as string[]).includes(slug);
}
+294
View File
@@ -0,0 +1,294 @@
/**
* Full article bodies for the blog, seeded for production.
* Metadata (title, excerpt, category, readTime) lives in the i18n dict;
* this module holds the long-form body in both locales.
*
* When the admin panel / CMS lands, this file becomes the seed source —
* the shape maps 1:1 to a `posts` table.
*/
export type Block =
| { k: 'p'; t: string }
| { k: 'h2'; t: string }
| { k: 'ul'; items: string[] }
| { k: 'quote'; t: string }
| { k: 'code'; lang?: string; t: string };
export type Article = { lead: string; blocks: Block[] };
export type PostContent = {
/** ISO date */
date: string;
/** accent key used for the cover gradient */
accent: 'electric' | 'violet' | 'magenta' | 'emerald' | 'cyan';
en: Article;
fa: Article;
};
export const POSTS: Record<string, PostContent> = {
'rag-eval-framework': {
date: '2026-04-22',
accent: 'magenta',
en: {
lead: 'Most RAG systems are shipped on a vibe. A demo answers three questions well, everyone nods, and it goes to production untested. Here is the evaluation framework I install before a single user touches it.',
blocks: [
{ k: 'h2', t: 'Why BLEU and ROUGE fail you' },
{ k: 'p', t: 'BLEU and ROUGE measure n-gram overlap with a reference answer. For translation that is fine. For retrieval-augmented generation it is misleading: a correct answer phrased differently scores low, and a fluent hallucination that happens to reuse words scores high. You end up optimizing for surface similarity instead of truth.' },
{ k: 'p', t: 'The fix is to split evaluation into two independent layers — retrieval quality and answer quality — and never average them into a single vanity number.' },
{ k: 'h2', t: 'Layer one: retrieval' },
{ k: 'p', t: 'Before the model writes anything, ask whether the right context was even fetched. Build a labelled set of question → gold-chunk pairs and track these:' },
{ k: 'ul', items: [
'Recall@k — did the gold chunk appear in the top k results?',
'MRR — how high did it rank when it did appear?',
'Context precision — what fraction of retrieved chunks were actually relevant?',
] },
{ k: 'p', t: 'If recall@5 is below 0.9, no amount of prompt engineering will save the answer. Fix retrieval first — chunking, embeddings, hybrid search — before you touch the generation prompt.' },
{ k: 'h2', t: 'Layer two: answer faithfulness' },
{ k: 'p', t: 'For generation, the metric that actually moves the needle is groundedness: is every claim in the answer supported by the retrieved context? I use an LLM-as-judge with a strict rubric and a small human-graded calibration set to keep the judge honest.' },
{ k: 'quote', t: 'A RAG system you cannot measure is a RAG system you cannot improve. Eval is not a phase — it is the control loop.' },
{ k: 'h2', t: 'Wire it into CI' },
{ k: 'p', t: 'The framework only pays off when it runs on every change. I gate deploys on a regression suite: if faithfulness drops more than two points or recall@5 falls below threshold, the pipeline blocks. That single gate has caught more silent regressions than any manual QA pass.' },
],
},
fa: {
lead: 'بیشتر سامانه‌های RAG بر اساس حس‌وحال منتشر می‌شوند. یک دموی سه‌سؤالی خوب جواب می‌دهد، همه سر تکان می‌دهند و بدون آزمون به تولید می‌رود. این چارچوب ارزیابی‌ای است که پیش از آنکه حتی یک کاربر آن را لمس کند، نصب می‌کنم.',
blocks: [
{ k: 'h2', t: 'چرا BLEU و ROUGE ناکافی‌اند' },
{ k: 'p', t: 'BLEU و ROUGE هم‌پوشانی n-gram با پاسخ مرجع را می‌سنجند. برای ترجمه قابل قبول است، اما برای RAG گمراه‌کننده: پاسخ درستی که با عبارت متفاوت بیان شود امتیاز پایین می‌گیرد و توهمی روان که اتفاقاً واژه‌ها را تکرار کند امتیاز بالا. در نهایت به جای حقیقت، شباهت سطحی را بهینه می‌کنید.' },
{ k: 'p', t: 'راه‌حل، تفکیک ارزیابی به دو لایه‌ی مستقل است — کیفیت بازیابی و کیفیت پاسخ — و هرگز میانگین‌گرفتن آن‌ها در یک عدد تزئینی.' },
{ k: 'h2', t: 'لایه‌ی اول: بازیابی' },
{ k: 'p', t: 'پیش از آنکه مدل چیزی بنویسد، بپرسید آیا اصلاً متن درست بازیابی شده است؟ یک مجموعه‌ی برچسب‌خورده از جفت‌های پرسش ← قطعه‌ی طلایی بسازید و این‌ها را پایش کنید:' },
{ k: 'ul', items: [
'Recall@k — آیا قطعه‌ی طلایی در k نتیجه‌ی برتر ظاهر شد؟',
'MRR — وقتی ظاهر شد، چه رتبه‌ای داشت؟',
'دقت متن — چه کسری از قطعات بازیابی‌شده واقعاً مرتبط بودند؟',
] },
{ k: 'p', t: 'اگر recall@5 زیر ۰٫۹ باشد، هیچ مقدار مهندسی پرامپت پاسخ را نجات نمی‌دهد. اول بازیابی را درست کنید — قطعه‌بندی، embedding، جست‌وجوی ترکیبی — بعد سراغ پرامپت تولید بروید.' },
{ k: 'h2', t: 'لایه‌ی دوم: وفاداری پاسخ' },
{ k: 'p', t: 'برای تولید، معیاری که واقعاً تأثیر دارد groundedness است: آیا هر ادعای پاسخ توسط متن بازیابی‌شده پشتیبانی می‌شود؟ من از LLM به‌عنوان داور با یک rubric سخت‌گیرانه و یک مجموعه‌ی کالیبراسیون انسانی کوچک استفاده می‌کنم تا داور صادق بماند.' },
{ k: 'quote', t: 'سامانه‌ی RAG‌ای که نتوانید اندازه بگیرید، سامانه‌ای است که نمی‌توانید بهبودش دهید. ارزیابی یک فاز نیست — حلقه‌ی کنترل است.' },
{ k: 'h2', t: 'آن را در CI ببندید' },
{ k: 'p', t: 'این چارچوب تنها وقتی ارزش دارد که روی هر تغییر اجرا شود. من استقرارها را به یک مجموعه‌ی regression مشروط می‌کنم: اگر وفاداری بیش از دو واحد افت کند یا recall@5 از آستانه پایین‌تر بیاید، خط لوله مسدود می‌شود. همین یک دروازه بیش از هر QA دستی، افت‌های خاموش را گرفته است.' },
],
},
},
'agentic-n8n-patterns': {
date: '2026-04-09',
accent: 'violet',
en: {
lead: 'n8n is the most underrated tool in the enterprise AI stack. Not because it is clever, but because it is boring in exactly the right places — and boring is what you want around an LLM agent.',
blocks: [
{ k: 'h2', t: 'Agents need a substrate, not a framework' },
{ k: 'p', t: 'The mistake teams make is reaching for an agent framework first. Frameworks hide the control flow inside the model. In production you want the opposite: deterministic orchestration around a non-deterministic core. n8n gives you that substrate — visible nodes, retries, error branches, and a durable execution log.' },
{ k: 'h2', t: 'The pattern: LLM as a node, not the conductor' },
{ k: 'p', t: 'Treat the model as one step that proposes an action, then let n8n decide whether to execute it. The agent suggests; the workflow disposes. This keeps every side effect — an API call, a database write, an email — gated behind a node you can inspect, rate-limit, and roll back.' },
{ k: 'ul', items: [
'Planner node — the LLM returns a structured action, never raw text.',
'Router node — n8n validates the action against an allow-list.',
'Tool nodes — real integrations, each with their own retry policy.',
'Audit node — every step is appended to an execution store.',
] },
{ k: 'h2', t: 'Where LangGraph fits' },
{ k: 'p', t: 'For loops that need real state — multi-turn reasoning, reflection, tool retries with memory — I drop LangGraph inside a single n8n node. n8n owns the macro workflow and the durability; LangGraph owns the micro reasoning loop. The boundary is clean and each tool does what it is good at.' },
{ k: 'quote', t: 'Make the deterministic parts boring and the boring parts auditable. The intelligence belongs in exactly one node.' },
{ k: 'h2', t: 'Observability is the whole game' },
{ k: 'p', t: 'Because every execution is a record, you can replay a failed run, diff two runs, and answer the question every stakeholder eventually asks: "why did it do that?" An agent you can explain is an agent you can ship.' },
],
},
fa: {
lead: 'n8n کم‌ارزش‌گذاری‌شده‌ترین ابزار استک هوش مصنوعی سازمانی است. نه به این دلیل که باهوش است، بلکه چون دقیقاً در جای درست «خسته‌کننده» است — و خسته‌کننده دقیقاً همان چیزی است که گرداگرد یک عامل LLM می‌خواهید.',
blocks: [
{ k: 'h2', t: 'عامل‌ها به بستر نیاز دارند، نه فریم‌ورک' },
{ k: 'p', t: 'اشتباه تیم‌ها این است که اول سراغ فریم‌ورک عامل می‌روند. فریم‌ورک‌ها جریان کنترل را داخل مدل پنهان می‌کنند. در تولید عکسش را می‌خواهید: ارکستراسیون قطعی گرداگرد یک هسته‌ی نامعین. n8n همان بستر را می‌دهد — گره‌های قابل‌مشاهده، تلاش مجدد، شاخه‌های خطا و یک گزارش اجرای پایدار.' },
{ k: 'h2', t: 'الگو: LLM به‌عنوان یک گره، نه رهبر ارکستر' },
{ k: 'p', t: 'مدل را یک گام بدانید که کنشی را پیشنهاد می‌دهد، سپس بگذارید n8n تصمیم بگیرد آن را اجرا کند یا نه. عامل پیشنهاد می‌دهد؛ گردش‌کار تصمیم می‌گیرد. این کار هر اثر جانبی — فراخوان API، نوشتن در پایگاه‌داده، ایمیل — را پشت گره‌ای نگه می‌دارد که می‌توانید بازرسی، محدود و بازگردانی‌اش کنید.' },
{ k: 'ul', items: [
'گره برنامه‌ریز — LLM یک کنش ساختارمند برمی‌گرداند، نه متن خام.',
'گره مسیریاب — n8n کنش را در برابر فهرست مجاز اعتبارسنجی می‌کند.',
'گره‌های ابزار — یکپارچه‌سازی‌های واقعی، هرکدام با سیاست تلاش مجدد خود.',
'گره ممیزی — هر گام به یک انبار اجرا افزوده می‌شود.',
] },
{ k: 'h2', t: 'جای LangGraph کجاست' },
{ k: 'p', t: 'برای حلقه‌هایی که به حالت واقعی نیاز دارند — استدلال چندمرحله‌ای، بازتاب، تلاش مجدد ابزار با حافظه — LangGraph را داخل یک گره‌ی n8n می‌گذارم. n8n مالک گردش‌کار کلان و پایداری است؛ LangGraph مالک حلقه‌ی استدلال خرد. مرز تمیز است و هر ابزار کاری را می‌کند که در آن خوب است.' },
{ k: 'quote', t: 'بخش‌های قطعی را خسته‌کننده کنید و بخش‌های خسته‌کننده را قابل‌ممیزی. هوش دقیقاً به یک گره تعلق دارد.' },
{ k: 'h2', t: 'مشاهده‌پذیری همه‌ی بازی است' },
{ k: 'p', t: 'چون هر اجرا یک رکورد است، می‌توانید اجرای ناموفق را بازپخش کنید، دو اجرا را مقایسه کنید و به پرسشی پاسخ دهید که هر ذی‌نفعی سرانجام می‌پرسد: «چرا این کار را کرد؟» عاملی که بتوانید توضیحش دهید، عاملی است که می‌توانید منتشرش کنید.' },
],
},
},
'vertex-cost-control': {
date: '2026-03-28',
accent: 'cyan',
en: {
lead: 'I have reviewed dozens of Vertex AI bills. The same three anti-patterns show up in roughly 80% of them — and removing them routinely cuts monthly spend by half without touching quality.',
blocks: [
{ k: 'h2', t: 'Anti-pattern 1: the always-on endpoint' },
{ k: 'p', t: 'Teams deploy a model to a dedicated endpoint with a minimum replica count of one and then forget about it. For bursty internal traffic that is a machine billing 24/7 to serve a few hundred requests a day. Set min replicas to zero where the latency budget allows, or batch the workload.' },
{ k: 'h2', t: 'Anti-pattern 2: the wrong model for the job' },
{ k: 'p', t: 'Not every call needs the frontier model. A cascade — cheap model first, escalate to the expensive one only when confidence is low — keeps quality high where it matters and spend low everywhere else.' },
{ k: 'ul', items: [
'Route by task complexity, not by habit.',
'Cache embeddings aggressively — they rarely change.',
'Use context caching for stable system prompts and long shared documents.',
] },
{ k: 'h2', t: 'Anti-pattern 3: no unit economics' },
{ k: 'p', t: 'If you cannot state the cost per request, you cannot control it. I instrument every call with token counts and model id, then roll it up to cost-per-feature. The moment a feature has a dollar figure attached, the optimization conversation changes from abstract to obvious.' },
{ k: 'quote', t: 'You do not cut cloud cost with a spreadsheet at month-end. You cut it with a label on every request.' },
{ k: 'h2', t: 'The result' },
{ k: 'p', t: 'On the last engagement, those three fixes plus context caching took a $40k/month Vertex bill to under $16k — and p95 latency improved, because the cascade kept most traffic on a faster, smaller model.' },
],
},
fa: {
lead: 'ده‌ها صورتحساب Vertex AI را بررسی کرده‌ام. همان سه ضدالگو در حدود ۸۰٪ آن‌ها دیده می‌شود — و حذف‌شان معمولاً هزینه‌ی ماهانه را بدون دست‌زدن به کیفیت نصف می‌کند.',
blocks: [
{ k: 'h2', t: 'ضدالگوی ۱: endpoint همیشه‌روشن' },
{ k: 'p', t: 'تیم‌ها مدلی را روی یک endpoint اختصاصی با حداقل یک replica مستقر می‌کنند و فراموشش می‌کنند. برای ترافیک داخلی پرنوسان، این یعنی ماشینی که ۲۴ ساعته صورتحساب می‌دهد تا چند صد درخواست در روز را پاسخ دهد. جایی که بودجه‌ی تأخیر اجازه می‌دهد حداقل replica را صفر کنید، یا بار کاری را batch کنید.' },
{ k: 'h2', t: 'ضدالگوی ۲: مدل نامناسب برای کار' },
{ k: 'p', t: 'هر فراخوان به مدل مرزی نیاز ندارد. یک cascade — اول مدل ارزان، فقط وقتی اطمینان پایین است به مدل گران ارتقا — کیفیت را جایی که مهم است بالا و هزینه را همه‌جا پایین نگه می‌دارد.' },
{ k: 'ul', items: [
'مسیریابی بر اساس پیچیدگی کار، نه عادت.',
'embeddingها را پرحجم cache کنید — به‌ندرت تغییر می‌کنند.',
'برای پرامپت‌های سیستمی پایدار و اسناد مشترک طولانی از context caching استفاده کنید.',
] },
{ k: 'h2', t: 'ضدالگوی ۳: نبود اقتصاد واحد' },
{ k: 'p', t: 'اگر نتوانید هزینه‌ی هر درخواست را بگویید، نمی‌توانید کنترلش کنید. من هر فراخوان را با شمار توکن و شناسه‌ی مدل ابزارگذاری می‌کنم و سپس به هزینه‌به‌ازای‌قابلیت تجمیع می‌کنم. لحظه‌ای که یک قابلیت رقم دلاری پیدا کند، گفت‌وگوی بهینه‌سازی از انتزاعی به بدیهی تبدیل می‌شود.' },
{ k: 'quote', t: 'هزینه‌ی ابر را با یک صفحه‌گسترده در پایان ماه کم نمی‌کنید. با یک برچسب روی هر درخواست کم می‌کنید.' },
{ k: 'h2', t: 'نتیجه' },
{ k: 'p', t: 'در آخرین پروژه، همین سه اصلاح به‌علاوه‌ی context caching صورتحساب ۴۰هزاردلاری ماهانه‌ی Vertex را به زیر ۱۶هزار دلار رساند — و تأخیر p95 هم بهتر شد، چون cascade بیشتر ترافیک را روی مدلی کوچک‌تر و سریع‌تر نگه داشت.' },
],
},
},
'k8s-llm-inference': {
date: '2026-03-11',
accent: 'emerald',
en: {
lead: 'Sub-50ms LLM inference on commodity Kubernetes is achievable — but not by throwing GPUs at the problem. It comes from removing the three places latency actually hides.',
blocks: [
{ k: 'h2', t: 'Latency hides in cold starts' },
{ k: 'p', t: 'A pod that scales from zero pays a model-load tax of tens of seconds. The answer is KEDA scaling on a queue depth signal, with a warm pool sized to your p50 traffic. You autoscale for the spikes, but you never serve a request from a cold replica.' },
{ k: 'h2', t: 'Latency hides in GPU contention' },
{ k: 'p', t: 'One model per GPU is wasteful; ten models fighting for one GPU is slow. The middle path is time-slicing or MIG partitions with explicit memory budgets, plus a scheduler that is GPU-topology aware so chatty replicas land on the same node.' },
{ k: 'ul', items: [
'Pin the model in GPU memory — never reload per request.',
'Use continuous batching so concurrent requests share a forward pass.',
'Hedge slow requests: fire a second attempt at p95 and take the first to finish.',
] },
{ k: 'h2', t: 'Latency hides in the network' },
{ k: 'p', t: 'Cross-AZ hops, TLS renegotiation, and an over-eager service mesh quietly add milliseconds. Keep inference traffic in-zone, reuse connections, and measure the mesh overhead before you assume it is free.' },
{ k: 'quote', t: 'You do not buy latency with bigger GPUs. You earn it by deleting the waits nobody is looking at.' },
{ k: 'h2', t: 'Prove it with a budget' },
{ k: 'p', t: 'I define an explicit latency budget per stage — queue, batch, forward pass, serialization, network — and alert when any stage drifts. When p95 regresses, the budget tells you exactly which stage to open, instead of guessing.' },
],
},
fa: {
lead: 'استنتاج LLM با تأخیر زیر ۵۰ میلی‌ثانیه روی Kubernetes معمولی دست‌یافتنی است — اما نه با ریختن GPU روی مسئله. از حذف سه جایی می‌آید که تأخیر واقعاً پنهان می‌شود.',
blocks: [
{ k: 'h2', t: 'تأخیر در cold start پنهان است' },
{ k: 'p', t: 'پادی که از صفر مقیاس می‌گیرد، مالیات بارگذاری مدل به‌اندازه‌ی ده‌ها ثانیه می‌پردازد. پاسخ، مقیاس‌دهی KEDA بر اساس عمق صف است، با یک استخر گرم به‌اندازه‌ی ترافیک p50. برای جهش‌ها autoscale می‌کنید، اما هرگز درخواستی را از replica سرد پاسخ نمی‌دهید.' },
{ k: 'h2', t: 'تأخیر در رقابت GPU پنهان است' },
{ k: 'p', t: 'یک مدل به‌ازای هر GPU اسراف است؛ ده مدل در رقابت بر سر یک GPU کند است. راه میانه، time-slicing یا پارتیشن‌های MIG با بودجه‌ی حافظه‌ی صریح است، به‌علاوه‌ی زمان‌بندی‌ای که از توپولوژی GPU آگاه باشد تا replicaهای پرگفت‌وگو روی یک گره بنشینند.' },
{ k: 'ul', items: [
'مدل را در حافظه‌ی GPU پین کنید — هرگز به‌ازای هر درخواست بارگذاری نکنید.',
'از continuous batching استفاده کنید تا درخواست‌های هم‌زمان یک forward pass را به اشتراک بگذارند.',
'درخواست‌های کند را hedge کنید: در p95 تلاش دوم را بفرستید و اولی که تمام شد را بردارید.',
] },
{ k: 'h2', t: 'تأخیر در شبکه پنهان است' },
{ k: 'p', t: 'پرش‌های بین‌AZ، مذاکره‌ی مجدد TLS و یک service mesh بیش‌ازحد مشتاق بی‌سروصدا میلی‌ثانیه اضافه می‌کنند. ترافیک استنتاج را درون‌ناحیه نگه دارید، اتصال‌ها را بازاستفاده کنید و پیش از آنکه فرض کنید mesh رایگان است، سربارش را اندازه بگیرید.' },
{ k: 'quote', t: 'تأخیر را با GPUهای بزرگ‌تر نمی‌خرید. با حذف انتظارهایی که کسی نگاهشان نمی‌کند به دستش می‌آورید.' },
{ k: 'h2', t: 'با یک بودجه اثباتش کنید' },
{ k: 'p', t: 'برای هر مرحله بودجه‌ی تأخیر صریح تعریف می‌کنم — صف، batch، forward pass، سریال‌سازی، شبکه — و وقتی هر مرحله منحرف شد هشدار می‌دهم. وقتی p95 پسرفت می‌کند، بودجه دقیقاً می‌گوید کدام مرحله را باز کنید، به‌جای حدس‌زدن.' },
],
},
},
'flutter-on-device-ai': {
date: '2026-02-19',
accent: 'electric',
en: {
lead: 'On-device AI is not a smaller version of cloud AI. It is a different engineering problem with a different reward: privacy, offline capability, and zero per-inference cost.',
blocks: [
{ k: 'h2', t: 'Pick the right tier' },
{ k: 'p', t: 'Not everything belongs on the device. The decision tree is simple: if the task is latency-critical, privacy-sensitive, or must work offline, it runs on-device. Everything else can call the cloud. Most real apps end up hybrid — a small local model for the common case, a cloud fallback for the hard one.' },
{ k: 'h2', t: 'Gemini Nano and LiteRT in Flutter' },
{ k: 'p', t: 'On Android, Gemini Nano gives you a capable on-device model through AICore. For custom models, LiteRT (formerly TFLite) runs quantized weights with hardware delegation. From Flutter you bridge to both through a thin platform channel — keep the inference on the native side and pass only structured results across.' },
{ k: 'ul', items: [
'Quantize to int8 — the quality loss is usually negligible, the speedup is not.',
'Warm the interpreter at app start, not on first use.',
'Stream tokens to the UI so perceived latency stays low even when total latency is not.',
] },
{ k: 'h2', t: 'The UX is the hard part' },
{ k: 'p', t: 'On-device models are smaller, so the product has to be honest about their limits. Constrain the task, give the model structure, and design graceful fallbacks. A focused local model that does one thing reliably beats a general one that occasionally embarrasses you.' },
{ k: 'quote', t: 'On-device AI rewards narrow scope. Ship the model that nails one job, not the one that attempts ten.' },
{ k: 'h2', t: 'Battery and binary size are product decisions' },
{ k: 'p', t: 'A 200MB model and a hot CPU are features your users feel. Measure energy per inference and ship the model on demand rather than in the initial bundle. The right size is the smallest one that clears your quality bar.' },
],
},
fa: {
lead: 'هوش مصنوعی on-device نسخه‌ی کوچک‌تر هوش مصنوعی ابری نیست. مسئله‌ی مهندسی متفاوتی با پاداش متفاوت است: حریم خصوصی، توان آفلاین و هزینه‌ی صفر به‌ازای هر استنتاج.',
blocks: [
{ k: 'h2', t: 'لایه‌ی درست را انتخاب کنید' },
{ k: 'p', t: 'همه‌چیز به دستگاه تعلق ندارد. درخت تصمیم ساده است: اگر کار حساس‌به‌تأخیر، حساس‌به‌حریم‌خصوصی یا نیازمند کار آفلاین است، روی دستگاه اجرا می‌شود. بقیه می‌توانند ابر را فرابخوانند. بیشتر اپ‌های واقعی ترکیبی می‌شوند — یک مدل محلی کوچک برای حالت رایج، یک fallback ابری برای حالت سخت.' },
{ k: 'h2', t: 'Gemini Nano و LiteRT در Flutter' },
{ k: 'p', t: 'در اندروید، Gemini Nano از طریق AICore یک مدل on-device توانمند می‌دهد. برای مدل‌های سفارشی، LiteRT (همان TFLite سابق) وزن‌های کوانتیزه را با واگذاری سخت‌افزاری اجرا می‌کند. از Flutter از طریق یک platform channel نازک به هردو پل می‌زنید — استنتاج را سمت native نگه دارید و فقط نتایج ساختارمند را عبور دهید.' },
{ k: 'ul', items: [
'به int8 کوانتیزه کنید — افت کیفیت معمولاً ناچیز است، شتاب نه.',
'مفسر را در شروع اپ گرم کنید، نه در اولین استفاده.',
'توکن‌ها را به UI استریم کنید تا تأخیر ادراک‌شده پایین بماند حتی اگر تأخیر کل نباشد.',
] },
{ k: 'h2', t: 'بخش سخت، UX است' },
{ k: 'p', t: 'مدل‌های on-device کوچک‌ترند، پس محصول باید درباره‌ی محدودیت‌هایشان صادق باشد. کار را محدود کنید، به مدل ساختار بدهید و fallbackهای مودبانه طراحی کنید. یک مدل محلی متمرکز که یک کار را قابل‌اتکا انجام دهد، از مدلی عمومی که گاهی شرمنده‌تان می‌کند بهتر است.' },
{ k: 'quote', t: 'هوش مصنوعی on-device به دامنه‌ی باریک پاداش می‌دهد. مدلی را منتشر کنید که یک کار را بی‌نقص انجام دهد، نه آنکه ده کار را امتحان کند.' },
{ k: 'h2', t: 'باتری و حجم باینری تصمیم‌های محصول‌اند' },
{ k: 'p', t: 'یک مدل ۲۰۰مگابایتی و CPU داغ، قابلیت‌هایی‌اند که کاربرانتان حس می‌کنند. انرژی به‌ازای هر استنتاج را اندازه بگیرید و مدل را به‌صورت on-demand منتشر کنید نه در بسته‌ی اولیه. اندازه‌ی درست، کوچک‌ترین اندازه‌ای است که از خط کیفیت شما رد شود.' },
],
},
},
'enterprise-ai-roadmap': {
date: '2026-01-30',
accent: 'electric',
en: {
lead: 'Most enterprise AI initiatives die in the gap between a board mandate and a shipped feature. This is the 90-day roadmap I build to cross it — discovery to first production deployment.',
blocks: [
{ k: 'h2', t: 'Days 030: discovery, not deck-building' },
{ k: 'p', t: 'The first month is spent finding the use cases that are both valuable and feasible. I interview the people doing the work, map the data that actually exists (not the data the org wishes it had), and score candidates on impact versus effort. The output is a shortlist of three, not a 40-slide strategy.' },
{ k: 'h2', t: 'Days 3060: one thin slice to production' },
{ k: 'p', t: 'We pick the single highest-leverage use case and ship it end-to-end for a small group of real users. Not a pilot in a sandbox — a thin slice in production, with monitoring, evaluation, and a rollback path. The goal is to learn what breaks when reality arrives.' },
{ k: 'ul', items: [
'Define success metrics before writing code.',
'Instrument cost and quality from request one.',
'Ship behind a flag to a controlled cohort.',
] },
{ k: 'h2', t: 'Days 6090: harden and templatize' },
{ k: 'p', t: 'With one real workload live, the last month turns the bespoke build into a repeatable pattern: shared eval harness, a reference architecture, and the platform pieces the next three use cases will reuse. The second project should take half the time of the first.' },
{ k: 'quote', t: 'A roadmap is not a list of features. It is the order in which you reduce uncertainty.' },
{ k: 'h2', t: 'What kills roadmaps' },
{ k: 'p', t: 'Boiling the ocean, optimizing a model nobody uses, and treating AI as a research project instead of a product. The antidote to all three is the same: get one real thing in front of real users fast, then let what you learn redraw the map.' },
],
},
fa: {
lead: 'بیشتر ابتکارهای هوش مصنوعی سازمانی در شکاف میان دستور هیئت‌مدیره و یک قابلیت منتشرشده می‌میرند. این نقشه‌ی راه ۹۰روزه‌ای است که برای عبور از آن می‌سازم — از کشف تا اولین استقرار تولید.',
blocks: [
{ k: 'h2', t: 'روز ۰ تا ۳۰: کشف، نه ساختن اسلاید' },
{ k: 'p', t: 'ماه اول صرف یافتن موارد کاربری‌ای می‌شود که هم ارزشمند و هم شدنی‌اند. با کسانی که کار را انجام می‌دهند مصاحبه می‌کنم، داده‌ای را که واقعاً وجود دارد نگاشت می‌کنم (نه داده‌ای که سازمان آرزویش را دارد) و گزینه‌ها را بر اساس اثر در برابر تلاش امتیاز می‌دهم. خروجی، فهرست کوتاهی از سه مورد است، نه یک راهبرد ۴۰اسلایدی.' },
{ k: 'h2', t: 'روز ۳۰ تا ۶۰: یک برش نازک تا تولید' },
{ k: 'p', t: 'تک‌مورد با بیشترین اهرم را برمی‌گزینیم و آن را سرتاسری برای گروه کوچکی از کاربران واقعی منتشر می‌کنیم. نه یک pilot در sandbox — یک برش نازک در تولید، با پایش، ارزیابی و مسیر بازگشت. هدف، یادگرفتن چیزی است که وقتی واقعیت می‌رسد می‌شکند.' },
{ k: 'ul', items: [
'معیارهای موفقیت را پیش از نوشتن کد تعریف کنید.',
'هزینه و کیفیت را از همان درخواست اول ابزارگذاری کنید.',
'پشت یک flag برای یک گروه کنترل‌شده منتشر کنید.',
] },
{ k: 'h2', t: 'روز ۶۰ تا ۹۰: تثبیت و قالب‌سازی' },
{ k: 'p', t: 'با یک بار کاری واقعی در حال اجرا، ماه آخر ساخت سفارشی را به الگویی تکرارپذیر تبدیل می‌کند: harness ارزیابی مشترک، یک معماری مرجع و قطعات پلتفرمی که سه مورد بعدی بازاستفاده خواهند کرد. پروژه‌ی دوم باید نصف زمان اولی را ببرد.' },
{ k: 'quote', t: 'نقشه‌ی راه فهرستی از قابلیت‌ها نیست. ترتیبی است که در آن عدم‌قطعیت را کاهش می‌دهید.' },
{ k: 'h2', t: 'چه چیزی نقشه‌ی راه را می‌کشد' },
{ k: 'p', t: 'جوشاندن اقیانوس، بهینه‌سازی مدلی که کسی استفاده نمی‌کند و رفتار با هوش مصنوعی به‌مثابه‌ی پروژه‌ی پژوهشی به‌جای محصول. پادزهر هر سه یکی است: یک چیز واقعی را سریع جلوی کاربران واقعی بگذارید، سپس بگذارید آنچه می‌آموزید نقشه را دوباره بکشد.' },
],
},
},
};
export const POST_SLUGS = Object.keys(POSTS);
+31
View File
@@ -0,0 +1,31 @@
/**
* The set of content sections exposed in the admin panel. Each key maps to a
* top-level key inside `dict`; editing one stores a `{ fa, en }` override that
* the content loader merges over the in-code default. Kept dependency-free so
* both client (sidebar, editor) and server (dashboard, API) can import it.
*/
export const EDITABLE_SECTIONS = [
{ key: 'hero', label: { en: 'Hero', fa: 'هیرو' }, desc: { en: 'Headline, roles, metrics, CTAs', fa: 'تیتر، نقش‌ها، اعداد، دکمه‌ها' } },
{ key: 'services', label: { en: 'Services', fa: 'خدمات' }, desc: { en: 'The six practice cards', fa: 'شش کارت خدمات' } },
{ key: 'dataflow', label: { en: 'Data Flow', fa: 'پایپ‌لاین داده' }, desc: { en: 'RAG pipeline diagram stages', fa: 'مراحل نمودار پایپ‌لاین RAG' } },
{ key: 'stack', label: { en: 'Stack', fa: 'استک' }, desc: { en: 'Tooling categories', fa: 'دسته‌های ابزار' } },
{ key: 'expertise', label: { en: 'Expertise', fa: 'تخصص' }, desc: { en: 'Skill bars', fa: 'نوارهای مهارت' } },
{ key: 'portfolio', label: { en: 'Portfolio', fa: 'نمونه‌کارها' }, desc: { en: 'Projects + galleries', fa: 'پروژه‌ها و گالری' } },
{ key: 'blog', label: { en: 'Journal', fa: 'بلاگ' }, desc: { en: 'Post cards (titles, excerpts)', fa: 'کارت‌های مقاله' } },
{ key: 'contact', label: { en: 'Contact', fa: 'تماس' }, desc: { en: 'Form copy + budgets', fa: 'متن فرم و بودجه‌ها' } },
{ key: 'nav', label: { en: 'Navigation', fa: 'ناوبری' }, desc: { en: 'Menu labels', fa: 'برچسب‌های منو' } },
{ key: 'footer', label: { en: 'Footer', fa: 'فوتر' }, desc: { en: 'Tagline + rights', fa: 'شعار و حقوق' } },
{ key: 'meta', label: { en: 'SEO / Meta', fa: 'سئو' }, desc: { en: 'Title + description', fa: 'عنوان و توضیحات' } },
] as const;
export type EditableSectionKey = (typeof EDITABLE_SECTIONS)[number]['key'];
export const EDITABLE_KEYS = EDITABLE_SECTIONS.map((s) => s.key) as EditableSectionKey[];
export function isEditableKey(key: string): key is EditableSectionKey {
return (EDITABLE_KEYS as string[]).includes(key);
}
export function sectionLabel(key: string): { en: string; fa: string } {
return EDITABLE_SECTIONS.find((s) => s.key === key)?.label ?? { en: key, fa: key };
}
+105
View File
@@ -0,0 +1,105 @@
import 'server-only';
import Database from 'better-sqlite3';
import { existsSync, mkdirSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
/**
* Persistent content store for the CMS.
*
* A single `sections` table holds JSON overrides keyed by section name
* (e.g. "hero", "services", "portfolio"). The stored JSON is a bilingual
* payload — `{ fa: <sectionObject>, en: <sectionObject> }` — that mirrors the
* shape of the matching key inside `dict`. At request time the content loader
* merges these overrides on top of the in-code `dict` defaults, so editing a
* section in the admin panel transparently rewrites what every public section
* renders without touching any component.
*
* The database file lives under DATA_DIR (default ./data) which on the
* self-hosted deployment is a mounted Docker volume, so content survives
* container rebuilds.
*/
export const DATA_DIR = resolve(process.env.DATA_DIR ?? join(process.cwd(), 'data'));
export const UPLOADS_DIR = join(DATA_DIR, 'uploads');
const DB_PATH = join(DATA_DIR, 'cms.db');
let _db: Database.Database | null = null;
function db(): Database.Database {
if (_db) return _db;
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
if (!existsSync(UPLOADS_DIR)) mkdirSync(UPLOADS_DIR, { recursive: true });
const handle = new Database(DB_PATH);
handle.pragma('journal_mode = WAL');
handle.exec(`
CREATE TABLE IF NOT EXISTS sections (
key TEXT PRIMARY KEY,
data TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
_db = handle;
return handle;
}
export type SectionRow = {
key: string;
/** JSON-encoded `{ fa, en }` payload. */
data: string;
updated_at: number;
};
export type SectionOverride = {
key: string;
data: unknown;
updatedAt: number;
};
/** Every stored override, used by the content loader to merge onto defaults. */
export function getAllSections(): SectionRow[] {
try {
return db()
.prepare('SELECT key, data, updated_at FROM sections')
.all() as SectionRow[];
} catch {
// A missing/locked DB must never crash a public render — fall back to dict.
return [];
}
}
/** A single override, or null when the section has never been edited. */
export function getSection(key: string): SectionOverride | null {
const row = db()
.prepare('SELECT key, data, updated_at FROM sections WHERE key = ?')
.get(key) as SectionRow | undefined;
if (!row) return null;
return { key: row.key, data: JSON.parse(row.data), updatedAt: row.updated_at };
}
/** Insert or replace a section override (admin only). */
export function setSection(key: string, data: unknown): void {
db()
.prepare(
`INSERT INTO sections (key, data, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`,
)
.run(key, JSON.stringify(data), Date.now());
}
/** Drop an override so the section reverts to its in-code default. */
export function resetSection(key: string): void {
db().prepare('DELETE FROM sections WHERE key = ?').run(key);
}
/** Map of key → updatedAt for showing edit status in the dashboard. */
export function sectionStatus(): Record<string, number> {
const out: Record<string, number> = {};
for (const row of getAllSections()) out[row.key] = row.updated_at;
return out;
}
export { dirname };
+855
View File
@@ -0,0 +1,855 @@
export type Locale = 'fa' | 'en';
export const LOCALES: Locale[] = ['fa', 'en'];
export const DEFAULT_LOCALE: Locale = 'fa';
/**
* Service identifiers stay locale-independent so they can key into
* routes (`/services/[slug]`), analytics events, and form values.
*/
export const SERVICE_IDS = [
'strategy',
'automation',
'llm-rag',
'architecture',
'mobile',
'google-stack',
] as const;
export type ServiceId = (typeof SERVICE_IDS)[number];
export const dict = {
fa: {
meta: {
title: 'سروش اسعدی — مهندس هوش مصنوعی، مشاور، معمار راهکار',
description:
'طراحی و پیاده‌سازی سامانه‌های هوش مصنوعی در مقیاس سازمانی — راهبرد، LLM و RAG، اتوماسیون عامل‌محور، زیرساخت ابری و استک گوگل.',
},
nav: {
services: 'خدمات',
stack: 'استک',
expertise: 'تخصص',
portfolio: 'نمونه‌کارها',
blog: 'بلاگ',
contact: 'تماس',
book: 'رزرو جلسه',
},
locale: {
switchTo: 'EN',
label: 'زبان',
},
hero: {
availability: 'پذیرش پروژه‌های منتخب فصل سوم ۲۰۲۶',
eyebrow: 'مهندس هوش مصنوعی · مشاور · معمار راهکار',
name: 'سروش اسعدی',
headlineLead: 'طراحی سامانه‌های',
headlineAccent: 'هوش مصنوعی',
headlineTrail: 'در مقیاس سازمانی.',
sub:
'از راهبرد تا تولید — ساخت پایپ‌لاین‌های LLM، عامل‌های خودکار، و معماری‌های ابری که در میلیون‌ها رویداد در روز پایدار می‌مانند.',
roles: [
'راهبرد هوش مصنوعی',
'مهندسی LLM و RAG',
'معماری راهکار',
'اتوماسیون عامل‌محور',
'استک گوگل کلود',
],
ctaPrimary: 'رزرو جلسه مشاوره',
ctaSecondary: 'مشاهده خدمات',
scroll: 'اسکرول',
metrics: [
{ value: '۱۸+', label: 'مدل هوش مصنوعی مستقر' },
{ value: '۴۰+', label: 'میکروسرویس تولید' },
{ value: '۱۲ms', label: 'تأخیر استنتاج' },
{ value: '۹۹٪', label: 'پایداری SLA' },
],
},
services: {
eyebrow: 'خدمات',
title: 'شش حوزه تخصصی',
sub:
'از اولین جلسه‌ی راهبرد تا استقرار تولید — یک شریک مهندسی برای کل چرخه‌ی عمر هوش مصنوعی شما.',
items: [
{
id: 'strategy',
title: 'راهبرد و نقشه راه هوش مصنوعی',
description:
'ارزیابی بلوغ سازمانی، شناسایی موارد کاربری با بیشترین بازده، و طراحی نقشه راه ۱۲–۱۸ ماهه با KPIهای روشن.',
tags: ['Discovery', 'ROI Mapping', 'Roadmap'],
color: 'electric',
},
{
id: 'automation',
title: 'اتوماسیون هوش مصنوعی',
description:
'ساخت عامل‌های خودکار و گردش‌کارهای n8n که فرایندهای دستی را به سامانه‌های قابل ممیزی تبدیل می‌کنند.',
tags: ['n8n', 'Agents', 'Workflows'],
color: 'violet',
},
{
id: 'llm-rag',
title: 'مهندسی LLM و RAG',
description:
'طراحی pipeline‌های RAG با پایگاه‌های برداری، evaluation framework، و سرویس‌دهی با تأخیر زیر ۵۰ میلی‌ثانیه.',
tags: ['RAG', 'Vector DB', 'Eval'],
color: 'magenta',
},
{
id: 'architecture',
title: 'معماری راهکار',
description:
'طراحی سامانه‌های توزیع‌شده روی Kubernetes با میکروسرویس‌ها، event streaming، و الگوهای پایداری در مقیاس بالا.',
tags: ['K8s', 'Microservices', 'Event-Driven'],
color: 'emerald',
},
{
id: 'mobile',
title: 'اپلیکیشن‌های موبایل هوش مصنوعی',
description:
'برنامه‌های Flutter، Swift و Kotlin با on-device inference، استریم LLM و تجربه‌ی کاربری بومی.',
tags: ['Flutter', 'Swift', 'Kotlin'],
color: 'electric',
},
{
id: 'google-stack',
title: 'تخصص استک گوگل',
description:
'استقرار روی Vertex AI، GKE و Gemini با بهینه‌سازی هزینه و الگوهای امنیتی سطح enterprise.',
tags: ['Vertex AI', 'GKE', 'Gemini'],
color: 'cyan',
},
],
},
dataflow: {
eyebrow: 'پایپ‌لاین',
title: 'از سند خام تا پاسخ قابل اتکا',
sub:
'مسیری که هر پرسش در یک سامانه‌ی RAG تولیدی طی می‌کند — هر مرحله قابل اندازه‌گیری، قابل ممیزی و بهینه‌شده برای تأخیر.',
caption: 'تأخیر سرتاسری زیر ۵۰ میلی‌ثانیه · هر مرحله مشاهده‌پذیر',
nodes: [
{
id: 'ingest',
label: 'دریافت',
desc: 'نرمال‌سازی، قطعه‌بندی و پاک‌سازی اسناد منبع',
accent: 'electric',
},
{
id: 'embed',
label: 'برداری‌سازی',
desc: 'تولید embedding و نمایه‌سازی در پایگاه برداری',
accent: 'violet',
},
{
id: 'retrieve',
label: 'بازیابی',
desc: 'جستجوی ترکیبی معنایی و کلیدواژه‌ای',
accent: 'cyan',
},
{
id: 'rerank',
label: 'بازرتبه‌بندی',
desc: 'مرتب‌سازی مجدد نامزدها با cross-encoder',
accent: 'magenta',
},
{
id: 'generate',
label: 'تولید',
desc: 'پاسخ مستند با ارجاع به منبع',
accent: 'emerald',
},
],
},
stack: {
eyebrow: 'استک',
title: 'ابزارهای روزانه',
sub:
'هر چه ساخته می‌شود از این پایه‌ها بیرون می‌آید — انتخاب‌شده برای عمر طولانی، نه ترند روز.',
categories: [
{
id: 'languages',
label: 'زبان‌ها',
items: ['Python', 'TypeScript', 'Go', 'Rust', 'SQL'],
},
{
id: 'mobile',
label: 'موبایل',
items: ['Flutter', 'Swift / SwiftUI', 'Kotlin', 'React Native'],
},
{
id: 'infra',
label: 'زیرساخت',
items: ['Kubernetes', 'Terraform', 'Postgres', 'Redis', 'Kafka', 'NATS'],
},
{
id: 'ai',
label: 'هوش مصنوعی',
items: ['Vertex AI', 'Gemini', 'OpenAI', 'Anthropic', 'LangGraph', 'Pinecone', 'pgvector'],
},
],
},
expertise: {
eyebrow: 'تخصص',
title: 'اعدادی که اهمیت دارند',
sub:
'سامانه‌هایی که در میلیون‌ها رویداد در روز پایدار می‌مانند — این‌ها معیارهایی هستند که اندازه می‌گیریم.',
bars: [
{ label: 'مهندسی LLM و RAG', value: 95 },
{ label: 'معماری ابری و Kubernetes', value: 92 },
{ label: 'سیستم‌های عامل‌محور و اتوماسیون', value: 90 },
{ label: 'استک گوگل کلود (Vertex / GKE)', value: 88 },
{ label: 'موبایل بومی و cross-platform', value: 82 },
],
},
blog: {
eyebrow: 'بلاگ',
title: 'یادداشت‌های مهندسی',
sub:
'یافته‌ها از پروژه‌های واقعی — نه ترجمه‌ی مقاله، نه فهرست hype.',
readMore: 'ادامه',
readTimeSuffix: 'دقیقه',
items: [
{
slug: 'rag-eval-framework',
category: 'LLM',
title: 'چارچوب ارزیابی RAG که در تولید کار می‌کند',
excerpt:
'چرا BLEU و ROUGE برای RAG ناکافی‌اند، و معیارهایی که در پروژه‌های واقعی تصمیم می‌سازند.',
readTime: 8,
},
{
slug: 'agentic-n8n-patterns',
category: 'Automation',
title: 'الگوهای عامل‌محور با n8n برای سازمان',
excerpt:
'چگونه n8n را با LangGraph ترکیب کنیم تا گردش‌کارهای قابل ممیزی بسازیم.',
readTime: 11,
},
{
slug: 'vertex-cost-control',
category: 'Google Stack',
title: 'کنترل هزینه روی Vertex AI در مقیاس بالا',
excerpt:
'سه ضدالگو که در ۸۰٪ پروژه‌های Vertex می‌بینم، و چگونه ۶۰٪ هزینه را کاهش دادیم.',
readTime: 6,
},
{
slug: 'k8s-llm-inference',
category: 'Infra',
title: 'استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ میلی‌ثانیه',
excerpt:
'الگوی استقرار با KEDA، GPU sharing، و request hedging برای سرویس‌دهی پایدار.',
readTime: 14,
},
{
slug: 'flutter-on-device-ai',
category: 'Mobile',
title: 'هوش مصنوعی on-device در Flutter',
excerpt:
'استفاده از Gemini Nano و LiteRT برای استنتاج آفلاین در اپلیکیشن‌های موبایل.',
readTime: 9,
},
{
slug: 'enterprise-ai-roadmap',
category: 'Strategy',
title: 'نقشه راه هوش مصنوعی سازمانی در ۹۰ روز',
excerpt:
'چارچوبی که برای CTOها می‌سازم — از کشف موارد کاربری تا اولین استقرار تولید.',
readTime: 7,
},
],
},
portfolio: {
eyebrow: 'نمونه‌کارها',
title: 'سامانه‌هایی که در تولید کار می‌کنند',
sub:
'گزیده‌ای از پروژه‌های واقعی — از پایپ‌لاین RAG تا مش داده رویدادمحور. روی هر کارت بزنید تا گالری و جزئیات معماری را ببینید.',
labels: {
role: 'نقش',
year: 'سال',
client: 'کارفرما',
stack: 'استک',
view: 'مشاهده پروژه',
gallery: 'گالری',
close: 'بستن',
next: 'بعدی',
prev: 'قبلی',
},
items: [
{
id: 'atlas-rag',
title: 'اطلس — پلتفرم RAG سازمانی',
client: 'بانک ردیف‌اول',
year: '۲۰۲۵',
role: 'مهندس ارشد هوش مصنوعی',
summary:
'دستیار دانش روی بیش از ۴ میلیون سند داخلی؛ بازیابی ترکیبی با pgvector و reranker، چارچوب ارزیابی اختصاصی و سرویس‌دهی با تأخیر زیر ۴۰ میلی‌ثانیه روی Vertex AI.',
accent: 'electric',
tags: ['RAG', 'pgvector', 'Vertex AI', 'Eval'],
metrics: [
{ value: '۴M+', label: 'سند نمایه‌شده' },
{ value: '۳۸ms', label: 'تأخیر p95' },
{ value: '۹۲٪', label: 'دقت پاسخ' },
],
cover: '/portfolio/atlas-rag/cover.svg',
gallery: [
'/portfolio/atlas-rag/01.svg',
'/portfolio/atlas-rag/02.svg',
'/portfolio/atlas-rag/03.svg',
],
},
{
id: 'sentinel-agents',
title: 'سنتینل — اتوماسیون عامل‌محور عملیات',
client: 'اسکیل‌آپ SaaS',
year: '۲۰۲۵',
role: 'معمار راهکار',
summary:
'سامانه پاسخ خودکار به رخدادها با ترکیب n8n و LangGraph؛ عامل‌های قابل ممیزی که هشدارها را دسته‌بندی، ریشه‌یابی و در صورت امکان ترمیم می‌کنند.',
accent: 'violet',
tags: ['n8n', 'LangGraph', 'Agents', 'Observability'],
metrics: [
{ value: '۷۰٪', label: 'کاهش MTTR' },
{ value: '۲۴/۷', label: 'پوشش on-call' },
{ value: '۱۵۰+', label: 'گردش‌کار خودکار' },
],
cover: '/portfolio/sentinel-agents/cover.svg',
gallery: [
'/portfolio/sentinel-agents/01.svg',
'/portfolio/sentinel-agents/02.svg',
'/portfolio/sentinel-agents/03.svg',
],
},
{
id: 'vertex-vision',
title: 'ورتکس ویژن — بینایی ماشین بلادرنگ',
client: 'زنجیره خرده‌فروشی',
year: '۲۰۲۴',
role: 'مهندس هوش مصنوعی',
summary:
'استنتاج بینایی بلادرنگ روی GKE با Triton و Vertex AI برای تحلیل قفسه و رفتار مشتری در صدها شعبه، با مقیاس‌پذیری خودکار مبتنی بر صف.',
accent: 'cyan',
tags: ['Vertex AI', 'GKE', 'Triton', 'Computer Vision'],
metrics: [
{ value: '۱.۲B', label: 'استنتاج در ماه' },
{ value: '۳۰۰+', label: 'شعبه' },
{ value: '۶۰٪', label: 'کاهش هزینه GPU' },
],
cover: '/portfolio/vertex-vision/cover.svg',
gallery: [
'/portfolio/vertex-vision/01.svg',
'/portfolio/vertex-vision/02.svg',
'/portfolio/vertex-vision/03.svg',
],
},
{
id: 'mirage-mobile',
title: 'میراژ — هوش مصنوعی روی دستگاه',
client: 'محصول مصرف‌کننده',
year: '۲۰۲۴',
role: 'سرپرست موبایل و هوش مصنوعی',
summary:
'اپلیکیشن Flutter با استنتاج کاملاً آفلاین به‌کمک Gemini Nano و LiteRT؛ تجربه‌ی استریم پاسخ بدون وابستگی به شبکه و با حفظ کامل حریم خصوصی.',
accent: 'magenta',
tags: ['Flutter', 'Gemini Nano', 'LiteRT', 'On-device'],
metrics: [
{ value: '۰', label: 'وابستگی شبکه' },
{ value: '<۸۰ms', label: 'پاسخ‌دهی' },
{ value: '۴.۸★', label: 'امتیاز کاربران' },
],
cover: '/portfolio/mirage-mobile/cover.svg',
gallery: [
'/portfolio/mirage-mobile/01.svg',
'/portfolio/mirage-mobile/02.svg',
'/portfolio/mirage-mobile/03.svg',
],
},
{
id: 'flux-stream',
title: 'فلاکس — مش داده رویدادمحور',
client: 'پلتفرم لجستیک',
year: '۲۰۲۳',
role: 'معمار پلتفرم',
summary:
'ستون فقرات استریمینگ با Kafka و NATS روی Kubernetes؛ بیش از ۴۰ میکروسرویس با الگوهای پایداری، tracing سراسری و تحویل دقیقاً یک‌بار.',
accent: 'emerald',
tags: ['Kafka', 'NATS', 'Kubernetes', 'Go'],
metrics: [
{ value: '۴۰+', label: 'میکروسرویس' },
{ value: '۲M/s', label: 'رویداد در ثانیه' },
{ value: '۹۹.۹٪', label: 'پایداری' },
],
cover: '/portfolio/flux-stream/cover.svg',
gallery: [
'/portfolio/flux-stream/01.svg',
'/portfolio/flux-stream/02.svg',
'/portfolio/flux-stream/03.svg',
],
},
{
id: 'oracle-forecast',
title: 'اوراکل — موتور پیش‌بینی تقاضا',
client: 'زنجیره تأمین',
year: '۲۰۲۳',
role: 'مهندس یادگیری ماشین',
summary:
'پایپ‌لاین پیش‌بینی سری‌زمانی روی BigQuery و dbt با بازآموزی خودکار؛ کاهش چشمگیر هدررفت موجودی و بهبود دقت برنامه‌ریزی تأمین.',
accent: 'electric',
tags: ['Forecasting', 'BigQuery', 'dbt', 'MLOps'],
metrics: [
{ value: '۲۳٪', label: 'کاهش هدررفت' },
{ value: '۸۹٪', label: 'دقت پیش‌بینی' },
{ value: 'روزانه', label: 'بازآموزی' },
],
cover: '/portfolio/oracle-forecast/cover.svg',
gallery: [
'/portfolio/oracle-forecast/01.svg',
'/portfolio/oracle-forecast/02.svg',
'/portfolio/oracle-forecast/03.svg',
],
},
],
},
contact: {
eyebrow: 'تماس',
title: 'یک جلسه ۳۰ دقیقه‌ای رزرو کنید',
sub:
'هیچ هزینه‌ای، هیچ تعهدی. کاربرد، چالش‌ها و قدم بعدی را با هم بررسی می‌کنیم.',
fields: {
name: 'نام',
company: 'شرکت',
service: 'خدمت مورد نظر',
budget: 'بودجه (تقریبی)',
message: 'پیام',
},
placeholders: {
name: 'نام و نام خانوادگی',
company: 'نام سازمان',
message: 'هدف، زمان‌بندی، و چالش‌های فعلی…',
},
budgets: ['کمتر از $10k', '$10k$50k', '$50k$200k', 'بیش از $200k'],
submit: 'ارسال درخواست',
note: 'پاسخ معمولاً ظرف ۲۴ ساعت کاری.',
},
footer: {
tagline: 'طراحی‌شده در تهران · ساخته‌شده برای enterprise',
rights: '© ۲۰۲۶ سروش اسعدی. تمام حقوق محفوظ است.',
},
},
en: {
meta: {
title: 'Soroush Asadi — AI Engineer · Consultant · Solution Architect',
description:
'Designing and shipping production-grade AI systems for the enterprise — strategy, LLM & RAG, agentic automation, cloud infrastructure, and the Google Stack.',
},
nav: {
services: 'Services',
stack: 'Stack',
expertise: 'Expertise',
portfolio: 'Work',
blog: 'Journal',
contact: 'Contact',
book: 'Book a call',
},
locale: {
switchTo: 'FA',
label: 'Language',
},
hero: {
availability: 'Available for select Q3 2026 engagements',
eyebrow: 'AI Engineer · Consultant · Solution Architect',
name: 'Soroush Asadi',
headlineLead: 'Architecting',
headlineAccent: 'production-grade AI',
headlineTrail: 'for the enterprise.',
sub:
'From strategy to deployment — building LLM pipelines, autonomous agents, and cloud architectures that hold up at millions of events per day.',
roles: [
'AI Strategy',
'LLM & RAG Engineering',
'Solution Architecture',
'Agentic Automation',
'Google Cloud Stack',
],
ctaPrimary: 'Book a consultation',
ctaSecondary: 'View services',
scroll: 'Scroll',
metrics: [
{ value: '18+', label: 'AI models in production' },
{ value: '40+', label: 'microservices shipped' },
{ value: '12ms', label: 'inference latency' },
{ value: '99%', label: 'SLA uptime' },
],
},
services: {
eyebrow: 'Services',
title: 'Six areas of practice',
sub:
'From the first strategy session to production rollout — one engineering partner for the full AI lifecycle.',
items: [
{
id: 'strategy',
title: 'AI Strategy & Roadmap',
description:
'Maturity assessment, highest-ROI use-case discovery, and a 1218 month roadmap with measurable KPIs.',
tags: ['Discovery', 'ROI Mapping', 'Roadmap'],
color: 'electric',
},
{
id: 'automation',
title: 'AI Automation',
description:
'Autonomous agents and n8n workflows that turn manual processes into auditable, observable systems.',
tags: ['n8n', 'Agents', 'Workflows'],
color: 'violet',
},
{
id: 'llm-rag',
title: 'LLM & RAG Engineering',
description:
'Production RAG pipelines with vector stores, evaluation frameworks, and sub-50ms serving.',
tags: ['RAG', 'Vector DB', 'Eval'],
color: 'magenta',
},
{
id: 'architecture',
title: 'Solution Architecture',
description:
'Distributed systems on Kubernetes — microservices, event streaming, and resilience patterns at scale.',
tags: ['K8s', 'Microservices', 'Event-Driven'],
color: 'emerald',
},
{
id: 'mobile',
title: 'Mobile AI Apps',
description:
'Flutter, Swift, and Kotlin apps with on-device inference, streaming LLM UX, and native polish.',
tags: ['Flutter', 'Swift', 'Kotlin'],
color: 'electric',
},
{
id: 'google-stack',
title: 'Google Stack Specialist',
description:
'Vertex AI, GKE, and Gemini deployments with cost optimization and enterprise security patterns.',
tags: ['Vertex AI', 'GKE', 'Gemini'],
color: 'cyan',
},
],
},
dataflow: {
eyebrow: 'Pipeline',
title: 'From raw document to trustworthy answer',
sub:
'The path every query takes through a production RAG system — each stage measurable, auditable, and tuned for latency.',
caption: 'Sub-50ms end-to-end · every stage observable',
nodes: [
{
id: 'ingest',
label: 'Ingest',
desc: 'Normalize, chunk, and clean source documents',
accent: 'electric',
},
{
id: 'embed',
label: 'Embed',
desc: 'Generate embeddings and index in the vector store',
accent: 'violet',
},
{
id: 'retrieve',
label: 'Retrieve',
desc: 'Hybrid semantic + keyword search',
accent: 'cyan',
},
{
id: 'rerank',
label: 'Rerank',
desc: 'Re-order candidates with a cross-encoder',
accent: 'magenta',
},
{
id: 'generate',
label: 'Generate',
desc: 'Grounded answer with source citations',
accent: 'emerald',
},
],
},
stack: {
eyebrow: 'Stack',
title: 'Daily tooling',
sub:
'Everything I ship sits on this foundation — chosen for longevity, not hype cycles.',
categories: [
{
id: 'languages',
label: 'Languages',
items: ['Python', 'TypeScript', 'Go', 'Rust', 'SQL'],
},
{
id: 'mobile',
label: 'Mobile',
items: ['Flutter', 'Swift / SwiftUI', 'Kotlin', 'React Native'],
},
{
id: 'infra',
label: 'Infrastructure',
items: ['Kubernetes', 'Terraform', 'Postgres', 'Redis', 'Kafka', 'NATS'],
},
{
id: 'ai',
label: 'AI / ML',
items: ['Vertex AI', 'Gemini', 'OpenAI', 'Anthropic', 'LangGraph', 'Pinecone', 'pgvector'],
},
],
},
expertise: {
eyebrow: 'Expertise',
title: 'The numbers that matter',
sub:
'Systems that survive millions of events per day — these are the metrics I optimize for.',
bars: [
{ label: 'LLM & RAG engineering', value: 95 },
{ label: 'Cloud architecture & Kubernetes', value: 92 },
{ label: 'Agentic systems & automation', value: 90 },
{ label: 'Google Cloud stack (Vertex / GKE)', value: 88 },
{ label: 'Native + cross-platform mobile', value: 82 },
],
},
blog: {
eyebrow: 'Journal',
title: 'Engineering notes',
sub:
'Findings from real engagements — not translated articles, not hype lists.',
readMore: 'Read',
readTimeSuffix: 'min',
items: [
{
slug: 'rag-eval-framework',
category: 'LLM',
title: 'A RAG evaluation framework that holds up in production',
excerpt:
'Why BLEU and ROUGE fall short for RAG, and the metrics that actually drive decisions in real projects.',
readTime: 8,
},
{
slug: 'agentic-n8n-patterns',
category: 'Automation',
title: 'Agentic patterns with n8n for the enterprise',
excerpt:
'How to combine n8n with LangGraph to build auditable, debuggable autonomous workflows.',
readTime: 11,
},
{
slug: 'vertex-cost-control',
category: 'Google Stack',
title: 'Vertex AI cost control at scale',
excerpt:
'Three anti-patterns I see in 80% of Vertex projects — and how we cut 60% of monthly spend.',
readTime: 6,
},
{
slug: 'k8s-llm-inference',
category: 'Infra',
title: 'Sub-50ms LLM inference on Kubernetes',
excerpt:
'Deployment pattern with KEDA, GPU sharing, and request hedging for stable serving.',
readTime: 14,
},
{
slug: 'flutter-on-device-ai',
category: 'Mobile',
title: 'On-device AI in Flutter',
excerpt:
'Using Gemini Nano and LiteRT for offline inference inside mobile apps.',
readTime: 9,
},
{
slug: 'enterprise-ai-roadmap',
category: 'Strategy',
title: 'A 90-day enterprise AI roadmap',
excerpt:
'The framework I build for CTOs — from use-case discovery to first production deployment.',
readTime: 7,
},
],
},
portfolio: {
eyebrow: 'Selected work',
title: 'Systems that run in production',
sub:
'A selection of real engagements — from RAG pipelines to event-driven data meshes. Tap any card for the gallery and the architecture behind it.',
labels: {
role: 'Role',
year: 'Year',
client: 'Client',
stack: 'Stack',
view: 'View project',
gallery: 'Gallery',
close: 'Close',
next: 'Next',
prev: 'Previous',
},
items: [
{
id: 'atlas-rag',
title: 'Atlas — Enterprise RAG Platform',
client: 'Tier-1 bank',
year: '2025',
role: 'Lead AI Engineer',
summary:
'A knowledge assistant over 4M+ internal documents — hybrid retrieval with pgvector and a reranker, a bespoke evaluation harness, and sub-40ms serving on Vertex AI.',
accent: 'electric',
tags: ['RAG', 'pgvector', 'Vertex AI', 'Eval'],
metrics: [
{ value: '4M+', label: 'docs indexed' },
{ value: '38ms', label: 'p95 latency' },
{ value: '92%', label: 'answer accuracy' },
],
cover: '/portfolio/atlas-rag/cover.svg',
gallery: [
'/portfolio/atlas-rag/01.svg',
'/portfolio/atlas-rag/02.svg',
'/portfolio/atlas-rag/03.svg',
],
},
{
id: 'sentinel-agents',
title: 'Sentinel — Agentic Ops Automation',
client: 'SaaS scale-up',
year: '2025',
role: 'Solution Architect',
summary:
'Autonomous incident response combining n8n and LangGraph — auditable agents that triage alerts, find root cause, and self-heal where it is safe to do so.',
accent: 'violet',
tags: ['n8n', 'LangGraph', 'Agents', 'Observability'],
metrics: [
{ value: '70%', label: 'MTTR reduction' },
{ value: '24/7', label: 'on-call coverage' },
{ value: '150+', label: 'automated flows' },
],
cover: '/portfolio/sentinel-agents/cover.svg',
gallery: [
'/portfolio/sentinel-agents/01.svg',
'/portfolio/sentinel-agents/02.svg',
'/portfolio/sentinel-agents/03.svg',
],
},
{
id: 'vertex-vision',
title: 'Vertex Vision — Realtime Vision Inference',
client: 'Retail chain',
year: '2024',
role: 'AI Engineer',
summary:
'Real-time vision inference on GKE with Triton and Vertex AI for shelf analytics and customer flow across hundreds of stores, autoscaled off a work queue.',
accent: 'cyan',
tags: ['Vertex AI', 'GKE', 'Triton', 'Computer Vision'],
metrics: [
{ value: '1.2B', label: 'inferences / mo' },
{ value: '300+', label: 'stores' },
{ value: '60%', label: 'GPU cost cut' },
],
cover: '/portfolio/vertex-vision/cover.svg',
gallery: [
'/portfolio/vertex-vision/01.svg',
'/portfolio/vertex-vision/02.svg',
'/portfolio/vertex-vision/03.svg',
],
},
{
id: 'mirage-mobile',
title: 'Mirage — On-device AI Suite',
client: 'Consumer product',
year: '2024',
role: 'Mobile + AI Lead',
summary:
'A Flutter app with fully offline inference via Gemini Nano and LiteRT — streaming response UX with zero network dependency and privacy preserved end to end.',
accent: 'magenta',
tags: ['Flutter', 'Gemini Nano', 'LiteRT', 'On-device'],
metrics: [
{ value: '0', label: 'network deps' },
{ value: '<80ms', label: 'response' },
{ value: '4.8★', label: 'user rating' },
],
cover: '/portfolio/mirage-mobile/cover.svg',
gallery: [
'/portfolio/mirage-mobile/01.svg',
'/portfolio/mirage-mobile/02.svg',
'/portfolio/mirage-mobile/03.svg',
],
},
{
id: 'flux-stream',
title: 'Flux — Event-Driven Data Mesh',
client: 'Logistics platform',
year: '2023',
role: 'Platform Architect',
summary:
'A streaming backbone on Kafka and NATS over Kubernetes — 40+ microservices with resilience patterns, end-to-end tracing, and exactly-once delivery.',
accent: 'emerald',
tags: ['Kafka', 'NATS', 'Kubernetes', 'Go'],
metrics: [
{ value: '40+', label: 'microservices' },
{ value: '2M/s', label: 'events / sec' },
{ value: '99.9%', label: 'uptime' },
],
cover: '/portfolio/flux-stream/cover.svg',
gallery: [
'/portfolio/flux-stream/01.svg',
'/portfolio/flux-stream/02.svg',
'/portfolio/flux-stream/03.svg',
],
},
{
id: 'oracle-forecast',
title: 'Oracle — Demand Forecasting Engine',
client: 'Supply chain',
year: '2023',
role: 'ML Engineer',
summary:
'A time-series forecasting pipeline on BigQuery and dbt with automated retraining — sharply reduced inventory waste and improved supply planning accuracy.',
accent: 'electric',
tags: ['Forecasting', 'BigQuery', 'dbt', 'MLOps'],
metrics: [
{ value: '23%', label: 'waste reduction' },
{ value: '89%', label: 'forecast accuracy' },
{ value: 'daily', label: 'retraining' },
],
cover: '/portfolio/oracle-forecast/cover.svg',
gallery: [
'/portfolio/oracle-forecast/01.svg',
'/portfolio/oracle-forecast/02.svg',
'/portfolio/oracle-forecast/03.svg',
],
},
],
},
contact: {
eyebrow: 'Contact',
title: 'Book a 30-minute call',
sub:
'No cost, no commitment. We map the use case, the constraints, and the next step together.',
fields: {
name: 'Name',
company: 'Company',
service: 'Service',
budget: 'Budget (rough)',
message: 'Message',
},
placeholders: {
name: 'Full name',
company: 'Organization',
message: 'Goal, timeline, current blockers…',
},
budgets: ['Under $10k', '$10k$50k', '$50k$200k', '$200k+'],
submit: 'Send request',
note: 'Typical reply within 24 working hours.',
},
footer: {
tagline: 'Designed in Tehran · Built for the enterprise',
rights: '© 2026 Soroush Asadi. All rights reserved.',
},
},
} as const;
export type Dict = typeof dict.en;
+95
View File
@@ -0,0 +1,95 @@
'use client';
import {
createContext,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from 'react';
import { DEFAULT_LOCALE, dict, type Dict, type Locale } from './dictionaries';
type Direction = 'rtl' | 'ltr';
type Ctx = {
locale: Locale;
dir: Direction;
t: Dict;
setLocale: (l: Locale) => void;
toggle: () => void;
};
const LocaleContext = createContext<Ctx | null>(null);
const STORAGE_KEY = 'sa.locale';
function dirFor(locale: Locale): Direction {
return locale === 'fa' ? 'rtl' : 'ltr';
}
export function LocaleProvider({
initialLocale,
content,
children,
}: {
initialLocale?: Locale;
/**
* Server-resolved content (dict defaults merged with admin overrides).
* When omitted we fall back to the in-code dictionary, so the provider keeps
* working in isolation (tests, storybook, etc.).
*/
content?: { fa: Dict; en: Dict };
children: ReactNode;
}) {
const [locale, setLocaleState] = useState<Locale>(initialLocale ?? DEFAULT_LOCALE);
const source = content ?? (dict as unknown as { fa: Dict; en: Dict });
// Hydrate from localStorage on the client.
useEffect(() => {
try {
const saved = window.localStorage.getItem(STORAGE_KEY) as Locale | null;
if (saved === 'fa' || saved === 'en') {
setLocaleState(saved);
}
} catch {
/* noop */
}
}, []);
// Reflect locale + direction on the html element without a reload.
useEffect(() => {
const html = document.documentElement;
html.lang = locale;
html.dir = dirFor(locale);
html.dataset.locale = locale;
}, [locale]);
const setLocale = (l: Locale) => {
setLocaleState(l);
try {
window.localStorage.setItem(STORAGE_KEY, l);
} catch {
/* noop */
}
};
const value = useMemo<Ctx>(
() => ({
locale,
dir: dirFor(locale),
t: source[locale],
setLocale,
toggle: () => setLocale(locale === 'fa' ? 'en' : 'fa'),
}),
[locale, source],
);
return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>;
}
export function useLocale() {
const ctx = useContext(LocaleContext);
if (!ctx) throw new Error('useLocale must be used inside <LocaleProvider>');
return ctx;
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}