# เอกสาร DevStack · ยินดีต้อนรับ สวัสดี และยินดีต้อนรับสู่เอกสารของ **DevStack** ([devstack.bid](https://devstack.bid)) เราสร้าง **NVX Stack Builder** ขึ้นมาเพราะเบื่อการเริ่มโปรเจกต์ใหม่แล้วต้องไล่ค้นคำสั่งจากสิบแท็บ ว่าต้องติดตั้งอะไรก่อนหลัง บน Windows ต้องพิมพ์ต่างกันตรงไหน เอกสารชุดนี้คือที่ที่เราเล่าทุกอย่างให้ชุมชนฟังอย่างตรงไปตรงมา ตั้งแต่วิธีใช้งานไปจนถึงเบื้องหลังที่ทำให้มันทำงานได้ > ภาษาหลักของเอกสารคือภาษาไทย และเราคงศัพท์เทคนิคเป็นภาษาอังกฤษ (เช่น template, composer, rate limit) เพื่อให้ค้นในโค้ดได้ตรงตัว หน้าที่มีฉบับภาษาอังกฤษจะบอกไว้ · Primary language: Thai with English technical terms. English sections are marked. ## DevStack กับ NVX Stack Builder **DevStack** คือชื่อโครงการและชุมชนของเรา ส่วน **NVX Stack Builder** คือผลิตภัณฑ์ตัวแรกที่เราเปิดให้ใช้ฟรีที่ [devstack.bid](https://devstack.bid) มันเปลี่ยน "คำสั่งตั้งค่าโปรเจกต์" ให้เป็น **Stack Template** ที่เรียงลำดับแล้ว ตัดคำสั่งซ้ำแล้ว และมีคำอธิบายสองภาษา คุณเลือกเทมเพลตเอง หรือให้ AI agent ช่วยวางแผนก็ได้ จากนั้นปรับลำดับ ลบ เพิ่มขั้นตอน แล้วคัดลอก แชร์ หรือดาวน์โหลดเป็น `setup.sh` / `setup.ps1` / `.zip` สิ่งที่เรายึดไว้ตั้งแต่วันแรกคือ **เซิร์ฟเวอร์ของเราไม่เคยรันคำสั่งใด ๆ แทนคุณ** ทุกคำสั่งเป็นข้อความให้คุณอ่าน ตรวจ แล้วรันเองบนเครื่องของคุณ เหตุผลอยู่ใน [Safety model](./concepts/safety-model.md) ## เอกสารชุดนี้ใช้งานได้จริง ไม่ใช่แค่ตัวอย่าง ทุกหน้าที่คุณอ่านบน [devstack.bid/docs](https://devstack.bid/docs) สร้างจากไฟล์ Markdown จริงในโฟลเดอร์ `docs/` ของ repo ตอน build ไม่มีการเขียนเนื้อหาซ้ำอีกชุด เราจึงทำให้หยิบไปใช้ต่อได้ทันที: - ปุ่ม **คัดลอก Markdown** ทุกหน้า ได้ไฟล์ต้นฉบับแบบไบต์ต่อไบต์ เอาไปวางใน wiki ของทีมหรือส่งให้ AI ได้เลย - **ดูไฟล์ดิบ** เติม `.md` ท้าย URL เช่น `/docs/user-guide/getting-started.md` จะได้ `text/markdown` ตรง ๆ - ทุกบล็อกโค้ดมีปุ่ม **คัดลอก** ของตัวเอง - สำหรับ AI agent เรามี `/llms.txt` เป็นสารบัญลิงก์ไฟล์ดิบทุกหน้า และ `/llms-full.txt` รวมทุกหน้าไว้ในไฟล์เดียว - ช่องค้นหาด้านซ้ายค้นได้ทั้งภาษาไทยและอังกฤษ และค้นจากในเบราว์เซอร์ทั้งหมด ถ้าอยากรู้ว่าระบบเอกสารนี้ทำงานอย่างไร อ่านต่อที่ [Docs site](./architecture/docs-site.md) ## เริ่มอ่านตรงไหนดี · Who should read what | คุณคือ | เริ่มที่ | จากนั้น | |---|---|---| | นักพัฒนาที่อยากได้คำสั่งตั้งค่าโปรเจกต์ | [Getting started](./user-guide/getting-started.md) | [Builder](./user-guide/builder.md), [AI agent](./user-guide/ai-agent.md), [Downloads](./user-guide/downloads.md), [FAQ](./user-guide/troubleshooting.md) | | ผู้ร่วมพัฒนา (contributor) | [Concepts: Stack Template](./concepts/stack-template.md) | [Add a template](./contributing/add-template.md), [Testing](./contributing/testing.md), [File map](./reference/file-map.md) | | ผู้ดูแลระบบ / DevOps | [Local dev](./operations/local-dev.md) | [Env & secrets](./operations/env-secrets.md), [Deploy](./operations/deploy-cloudflare.md), [Rollback](./operations/rollback.md), [Monitoring](./operations/monitoring.md) | | สถาปนิก / reviewer | [Architecture overview](./architecture/overview.md) | [Data flow](./architecture/data-flow.md), [Subsystem history](./history/SUBSYSTEM-HISTORY.md) | | AI agent ที่ต้องดูแลแพลตฟอร์ม | [เมนูฟังก์ชัน](./platform.md) | [NVX DEVELOPER BOTH](./agents/cursor-caretakers.md), [cursor-agents.json](./agents/cursor-agents.json), [Agent playbook](./agents/AGENT-PLAYBOOK.md) | ## แผนผังเอกสาร · Site map - **user-guide/** คู่มือผู้ใช้ - [getting-started.md](./user-guide/getting-started.md) เริ่มต้นใช้งานใน 5 นาที - [templates.md](./user-guide/templates.md) ค้นหาและใช้ Stack Template ทั้ง 10 แบบ - [builder.md](./user-guide/builder.md) ตัวสร้างสแต็ก 4 ขั้น (Start → Configure → Review & reorder → Export) - [ai-agent.md](./user-guide/ai-agent.md) AI planner: key ของเซิร์ฟเวอร์, key ของตัวเอง, offline fallback - [sharing.md](./user-guide/sharing.md) ลิงก์แชร์ `#s=` และ `#t=` - [downloads.md](./user-guide/downloads.md) `setup.sh`, `setup.ps1`, `README.md`, starter `.zip` - [language-theme.md](./user-guide/language-theme.md) ภาษาไทย/อังกฤษ, ธีมดำ/สว่าง, command palette, หน้าเอกสาร - [troubleshooting.md](./user-guide/troubleshooting.md) ปัญหาที่พบบ่อยและ FAQ - **concepts/** แนวคิดเบื้องหลัง - [stack-template.md](./concepts/stack-template.md), [composition.md](./concepts/composition.md), [safety-model.md](./concepts/safety-model.md), [design-system.md](./concepts/design-system.md) - **architecture/** สถาปัตยกรรม - [overview.md](./architecture/overview.md), [data-flow.md](./architecture/data-flow.md), [templates-and-composer.md](./architecture/templates-and-composer.md), [scripts-and-share.md](./architecture/scripts-and-share.md), [ai-planner.md](./architecture/ai-planner.md), [api-hardening.md](./architecture/api-hardening.md), [ui-framework.md](./architecture/ui-framework.md), [cloudflare-runtime.md](./architecture/cloudflare-runtime.md), [docs-site.md](./architecture/docs-site.md) - **history/** [SUBSYSTEM-HISTORY.md](./history/SUBSYSTEM-HISTORY.md) ไทม์ไลน์ของแต่ละ subsystem พร้อม commit - **operations/** การรันและดูแล - [local-dev.md](./operations/local-dev.md), [env-secrets.md](./operations/env-secrets.md), [deploy-cloudflare.md](./operations/deploy-cloudflare.md), [rollback.md](./operations/rollback.md), [monitoring.md](./operations/monitoring.md), [rate-limits-cost.md](./operations/rate-limits-cost.md), [custom-domain.md](./operations/custom-domain.md) - คู่มือ deploy ฉบับเต็มเดิม: [DEPLOY.md](./DEPLOY.md) (TH + EN) - **contributing/** การร่วมพัฒนา - [add-template.md](./contributing/add-template.md), [add-component.md](./contributing/add-component.md), [testing.md](./contributing/testing.md), [release-process.md](./contributing/release-process.md), [docs-maintenance.md](./contributing/docs-maintenance.md) - **reference/** ข้อมูลอ้างอิง - [api-plan.md](./reference/api-plan.md), [template-schema.md](./reference/template-schema.md), [config-env.md](./reference/config-env.md), [npm-scripts.md](./reference/npm-scripts.md), [file-map.md](./reference/file-map.md) - **[platform.md](./platform.md)** เมนูฟังก์ชันของทั้งแพลตฟอร์ม (ไฟล์ดิบ `/docs/platform.md`) - **agents/** สำหรับ AI agent: [AGENT-PLAYBOOK.md](./agents/AGENT-PLAYBOOK.md), [cursor-caretakers.md](./agents/cursor-caretakers.md), [cursor-agents.json](./agents/cursor-agents.json), [platform-manifest.json](./agents/platform-manifest.json) - เอกสารเดิม: [UI-FRAMEWORK.md](./UI-FRAMEWORK.md) (ระบบดีไซน์ NVX UI, TH + EN) ## แหล่งความจริง · Where truth lives เมื่อเอกสารกับโค้ดขัดกัน เราถือว่า **โค้ดคือความจริง** แล้วรีบแก้เอกสารให้ตรง แหล่งความจริงหลักคือ รายการเทมเพลต [`src/data/templates/index.ts`](../src/data/templates/index.ts), ชนิดข้อมูล [`src/lib/types.ts`](../src/lib/types.ts), ตรรกะการประกอบคำสั่ง [`src/lib/composer.ts`](../src/lib/composer.ts), ตัวแปรสภาพแวดล้อม [`src/lib/server/env.ts`](../src/lib/server/env.ts), การตั้งค่า Worker [`wrangler.jsonc`](../wrangler.jsonc), ค่าดีไซน์ [`src/app/tokens.css`](../src/app/tokens.css) และประวัติการเปลี่ยนแปลง [`CHANGELOG.md`](../CHANGELOG.md) + `git log` เอกสารสร้างจากโค้ดและ git history จริง (v0.1.0 → v0.5.1) ทุกข้อเท็จจริงด้านประวัติอ้างอิง commit hash และ tag ตรวจย้อนได้ และมี `npm run docs:check` คอยตรวจว่าเอกสารยังตรงกับโค้ด (ดู [docs-maintenance](./contributing/docs-maintenance.md)) ## ช่วยเราทำให้เอกสารดีขึ้น เราเพิ่งเริ่มต้น และเอกสารที่ดีเกิดจากคำถามของคนที่ลองใช้จริง ถ้าเจอจุดที่อ่านแล้วงง ขั้นตอนที่ไม่ตรงกับหน้าจอ หรืออยากให้มีเทมเพลตใหม่ บอกเราได้เสมอ ผู้ร่วมพัฒนาเริ่มได้จาก [add-template.md](./contributing/add-template.md) และ [docs-maintenance.md](./contributing/docs-maintenance.md) ขอบคุณที่มาเป็นส่วนหนึ่งของ DevStack --- # เมนูฟังก์ชัน · Platform functions หน้านี้คือสารบัญฟังก์ชันของ DevStack (NVX Stack Builder) ที่ [devstack.bid](https://devstack.bid) แพลตฟอร์มทั้งชุดสร้างและดูแลโดยบอท [NVX DEVELOPER BOTH](./agents/cursor-caretakers.md) บน Cursor เปิดไฟล์ดิบที่ [devstack.bid/docs/platform.md](https://devstack.bid/docs/platform.md) สารบัญสำหรับเอเจนต์อยู่ที่ [llms.txt](https://devstack.bid/llms.txt) *EN: This is the function index. NVX DEVELOPER BOTH built the platform and looks after all of it. Raw Markdown is `/docs/platform.md`. Agents start at `/llms.txt`.* ## เอกสารที่เอเจนต์ควรเปิดก่อน - [ดัชนี Markdown ทุกหน้า](https://devstack.bid/llms.txt) ใช้ค้นว่ามีหน้าไหนก่อนเปิดทีละหน้า - [รวมทุกหน้า](https://devstack.bid/llms-full.txt) เมื่อต้องการบริบททั้งก้อน - [คู่มือเอเจนต์](./agents/AGENT-PLAYBOOK.md) และ [รายชื่อ Cursor caretakers](./agents/cursor-caretakers.md) - ไฟล์เครื่องอ่าน [cursor-agents.json](./agents/cursor-agents.json) บอกว่าบอท NVX DEVELOPER BOTH ดูแลทั้งแพลตฟอร์ม และแต่ละพื้นที่ต้องอ่านหน้าไหน ## สร้างสแต็ก #### Stack Templates เลือกเทมเพลตที่เรียงคำสั่งแล้วจาก [แคตตาล็อก](https://devstack.bid/templates) รายละเอียดอยู่ใน [templates](./user-guide/templates.md) #### Builder จัดลำดับ ใส่ add-on แล้วส่งออก `setup.sh` / `setup.ps1` / zip ที่ [ตัวสร้าง](https://devstack.bid/builder) อ่าน [builder](./user-guide/builder.md) #### AI planner อธิบายแอปเป็นไทยหรืออังกฤษ แล้วได้แผนที่โหลดเข้าตัวสร้าง ที่ [ตัวสร้าง](https://devstack.bid/builder) คู่มืออยู่ที่ [AI agent](./user-guide/ai-agent.md) เซิร์ฟเวอร์ไม่รันคำสั่งเหล่านั้น ## แชตและโรงงาน #### User Chat แชตฟรีที่ [แชต](https://devstack.bid/chat) เลือกโมเดลรวมถึง Vercel AI Gateway ได้ อ่าน [chat](./user-guide/chat.md) #### Dev Chat และ Factory Orchestrator อยู่ที่ [Dev Chat](https://devstack.bid/chat/dev) และ Sandbox Factory อยู่ที่ [Factory](https://devstack.bid/factory) คำสั่งของเอเจนต์รันบนเครื่องผู้ใช้หรือ sandbox ที่ผู้ใช้ถือ key เอง ไม่รันบนเซิร์ฟเวอร์นี้ ## บัญชีและการส่งโค้ด #### เข้าสู่ระบบ หัวเว็บมีปุ่มเข้าสู่ระบบไปที่ [หน้าเข้าสู่ระบบ](https://devstack.bid/login) สี่วิธี: Cursor, Google, GitHub และบัญชี DevStack (อีเมลกับรหัสผ่าน) ประวัติแชตอยู่กับบัญชีนั้น #### ส่งไฟล์ขึ้น GitHub จากคำตอบในแชตที่ลงชื่อด้วย GitHub ส่งได้หนึ่งไฟล์ผ่าน GitHub API เซิร์ฟเวอร์ไม่รันไฟล์นั้น ## สิ่งที่เอเจนต์ห้ามทำเอง deploy, migrate D1 บน remote, ผูกโดเมน, แก้ DNS, ลบทรัพยากร Cloudflare, commit secret, หรือรันคำสั่งของผู้ใช้บนเซิร์ฟเวอร์ ต้องมีคนอนุมัติก่อน รายละเอียดอยู่ใน [playbook](./agents/AGENT-PLAYBOOK.md) และ [cursor caretakers](./agents/cursor-caretakers.md) --- # เริ่มต้นใช้งาน · Getting started หน้านี้คือจุดเริ่มต้นที่เราอยากให้ทุกคนได้อ่านก่อน เราจะพาคุณจาก "ยังไม่รู้จักแอป" ไปจนได้สคริปต์ตั้งค่าโปรเจกต์ที่รันได้จริงภายในไม่กี่นาที และระหว่างทางจะเล่าด้วยว่าทำไมเราถึงออกแบบแต่ละขั้นแบบนี้ ## NVX Stack Builder คืออะไร ตอนเริ่มโปรเจกต์ใหม่ พวกเราเองก็เคยต้องค้นคำสั่งจากหลายแหล่ง ทั้งติดตั้ง Node ผ่าน nvm, สร้างแอปด้วย `create-next-app`, ติดตั้ง dependency, เพิ่ม ESLint/Vitest/Docker แล้วยังต้องจำว่าคำสั่งไหนต้องรันก่อนหลัง และบน Windows ต้องเขียนต่างกันอย่างไร **NVX Stack Builder** ซึ่งเป็นผลิตภัณฑ์แรกของ DevStack ([devstack.bid](https://devstack.bid)) รวบรวมสิ่งเหล่านี้เป็น **Stack Template** คือชุดขั้นตอนที่เรียงลำดับแล้วและตัดคำสั่งซ้ำแล้ว แต่ละขั้นมีคำอธิบายไทย/อังกฤษ คำสั่งแบบ POSIX และ PowerShell หมายเหตุตามระบบปฏิบัติการ ผลลัพธ์ที่คาดหวัง และคำสั่งตรวจสอบ (verify) หลักที่เรายึดไว้คือ **แอปไม่รันคำสั่งแทนคุณ** ทุกอย่างเป็นข้อความที่คุณอ่าน ตรวจ แล้วรันเองบนเครื่อง เพราะคำสั่งตั้งค่าโปรเจกต์แตะไฟล์ระบบและติดตั้งซอฟต์แวร์ เราเชื่อว่าการให้คนตรวจก่อนเสมอทั้งปลอดภัยกว่าและโปร่งใสกว่า (อ่านเหตุผลเต็มได้ที่ [Safety model](../concepts/safety-model.md)) ## สามเส้นทางเริ่มต้น 1. **เลือกเทมเพลตเอง** ไปที่หน้า `/templates` ค้นด้วยคำ เช่น `python`, `game`, `react` หรือกรองตามหมวด แล้วเปิดดูขั้นตอน เหมาะเมื่อคุณรู้อยู่แล้วว่าอยากได้ stack แบบไหน ดู [templates.md](./templates.md) 2. **ให้ AI agent วางแผน** ไปที่ `/builder` แท็บ "Ask the AI agent" แล้วเล่าว่าอยากสร้างแอปอะไร จะพิมพ์ไทยหรืออังกฤษก็ได้ เช่น "แดชบอร์ดยอดขายพร้อม CI และเทสต์" agent จะเลือกเทมเพลตที่ใกล้ที่สุด เลือก add-on และอาจเพิ่มขั้นตอนพิเศษ ถึงเซิร์ฟเวอร์จะไม่มี API key ก็ยังใช้ได้ผ่าน offline planner ดู [ai-agent.md](./ai-agent.md) 3. **เปิดลิงก์ที่เพื่อนแชร์มา** ลิงก์รูปแบบ `/builder#s=...` จะพาไปยังขั้น Review พร้อมสแต็กเดิมทุกประการ ดู [sharing.md](./sharing.md) ## ลองครั้งแรกกับเรา 1. เปิดหน้าแรก กด **Ask the AI agent** หรือ **Browse templates** 2. ในตัวสร้าง (builder) ขั้น **Start** เลือก "Pick a template" แล้วเลือก *Next.js Analytics Dashboard* 3. ขั้น **Configure** ตั้งชื่อโปรเจกต์ เลือกเวอร์ชัน Node (LTS/24/22/20) ตัวจัดการแพ็กเกจ (npm/pnpm/yarn) และ add-on เช่น Testing หรือ Docker ลองสังเกตแถบสรุปสแต็ก (current stack) ด้านบนที่เปลี่ยนตามทันที 4. ขั้น **Review & reorder** อ่านแต่ละขั้น ลากเพื่อสลับลำดับ ลบขั้นที่ไม่ต้องการ หรือเพิ่มขั้นตอนของคุณเอง 5. ขั้น **Export** คัดลอกคำสั่งทั้งหมด หรือดาวน์โหลด `setup.sh` (macOS/Linux/WSL) / `setup.ps1` (Windows) / starter `.zip` 6. เปิดสคริปต์ อ่านทุกบรรทัด แล้วรันในโฟลเดอร์ว่าง ขั้นที่เป็น dev server หรือ publish เราตั้งใจคอมเมนต์ไว้ ให้คุณรันเองในเทอร์มินัลแยก ```bash mkdir -p ~/projects && cd ~/projects less setup.sh bash setup.sh ``` ## สิ่งที่ควรมีบนเครื่อง - **Node.js** ผ่าน nvm (macOS/Linux) หรือ nvm-windows สำหรับเทมเพลตฝั่ง JavaScript ขั้นติดตั้ง nvm เป็นขั้น `requires: nvm` ถ้าสคริปต์หา nvm ไม่เจอจะข้ามพร้อมคำเตือน - **Python 3.11–3.13** หรือ **uv** สำหรับเทมเพลตฝั่ง Python - Git และเทอร์มินัล (bash/zsh หรือ PowerShell) ## เคล็ดลับจากทีมเรา - เริ่มจากค่าเริ่มต้นของเทมเพลตก่อน แล้วค่อยเพิ่ม add-on ทีละตัว จะได้เห็นว่าแต่ละตัวเพิ่มขั้นตอนอะไร - อ่าน "Expected result" และรัน "Verify" หลังแต่ละขั้น จะจับปัญหาได้เร็วกว่ารันรวดเดียวแล้วค่อยมาหาว่าพังตรงไหน - เก็บลิงก์แชร์ไว้ในโน้ตของโปรเจกต์ เพื่อสร้างสแต็กเดิมซ้ำได้ภายหลัง - เอกสารทุกหน้ามีปุ่ม **คัดลอก Markdown** และ URL แบบ `.md` ถ้าอยากเก็บคู่มือนี้ไว้ใน wiki ของทีมก็หยิบไปได้เลย ## อ้างอิง - โค้ดหน้าแรก [`src/app/page.tsx`](../../src/app/page.tsx), ตัวสร้าง [`src/components/builder.tsx`](../../src/components/builder.tsx) - ภาพหน้าจอเวอร์ชันล่าสุด: `screenshots/v0.5/` และหน้าเอกสาร `screenshots/v0.6/` --- # การใช้ Stack Template · Using templates Stack Template คือ "สูตร" ตั้งค่าโปรเจกต์ที่เราคัดสรรและทดสอบมาแล้ว เราเขียนแต่ละเทมเพลตเป็นไฟล์ข้อมูล TypeScript ใน [`src/data/templates/`](../../src/data/templates/index.ts) แทนการซ่อนไว้ในคอมโพเนนต์ ทุกคนในชุมชนจึงเปิดอ่าน ตรวจทาน ทดสอบ และเสนอเทมเพลตใหม่ได้ง่าย หน้านี้จะพาไปรู้จักทั้ง 10 แบบที่เรามีตอนนี้ และวิธีเลือกให้เหมาะกับงานของคุณ แนวคิดเต็มอยู่ที่ [concepts/stack-template.md](../concepts/stack-template.md) ## เทมเพลตทั้ง 10 แบบ (ณ v0.5) | id | ชื่อ | runtime | add-on ที่รองรับ | ค่าเริ่มต้น | |---|---|---|---|---| | `nextjs-dashboard` | Next.js Analytics Dashboard | Node | typescript, tailwind, eslint, testing, docker, ci | typescript, tailwind, eslint | | `vite-react-game` | Vite + React Canvas Game | Node | typescript, tailwind, eslint, testing, docker, ci | typescript | | `express-api` | Express REST API | Node | testing, docker, ci | — | | `fastapi-ai` | FastAPI + Python AI App | Python | eslint (Ruff), testing, docker, ci | — | | `streamlit-dashboard` | Streamlit Data Dashboard | Python | eslint (Ruff), testing, docker, ci | — | | `node-cli-npx` | Node CLI Tool (run via npx) | Node | testing, ci | — | | `pypi-package` | Python Package for PyPI | Python | eslint (Ruff), testing, ci | testing | | `collab-workspace` | Realtime Collaborative Workspace | Node | typescript, tailwind, eslint, testing, docker, ci | typescript, tailwind, eslint | | `react-admin-tool` | Internal Tool (React Admin) | Node | typescript, eslint, testing, docker, ci | typescript | | `nextjs-ai-chat` | Next.js AI Chat App | Node | typescript, tailwind, eslint, testing, docker, ci | typescript, tailwind, eslint | เราไม่อยากให้ตารางนี้ล้าสมัย จึงให้ `npm run docs:check` ตรวจกับโค้ดอัตโนมัติ (ทุก template id ต้องปรากฏในเอกสาร) ถ้ามีคนเพิ่มเทมเพลตใหม่แล้วลืมอัปเดตเอกสาร CI จะล้มทันที ## หน้า catalog (`/templates`) - **ช่องค้นหา** ค้นจากชื่อ คำอธิบาย และแท็ก เช่น `python`, `game`, `react` - **ชิปหมวดหมู่** (เช่น Dashboard, Game, API, AI app, Data, CLI, Package, Collaborative, Internal tool ตาม `CATEGORY_LABELS` ใน [`src/lib/i18n.ts`](../../src/lib/i18n.ts)) ใช้กรองผล ถ้าไม่พบอะไรจะมีปุ่ม "Clear filters" - การ์ดแต่ละใบแสดงไทล์โลโก้เครื่องมือหลัก (เช่น Next.js, FastAPI) หมวด runtime แท็ก และปุ่ม **View steps** / **Open in builder** ## หน้ารายละเอียดเทมเพลต (`/templates/`) หน้านี้แสดงทุกขั้นตอนตามลำดับที่ composer สร้างจากค่าเริ่มต้น (ค่าเริ่มต้น: npm, Node LTS, Python 3.12 และ add-on เริ่มต้นของเทมเพลต) แต่ละขั้นมี: - **ชื่อและคำอธิบาย** เราตั้งใจเขียนว่าทำไมต้องทำขั้นนี้ ไม่ใช่แค่ทำอะไร - **คำสั่ง** สลับดูแบบ macOS/Linux หรือ Windows PowerShell ได้ (ปุ่มด้านบนรายการ) - **OS notes** ข้อควรระวังเฉพาะระบบ เช่น nvm-windows - **Expected result** และ **Verify** คำสั่งพิสูจน์ว่าขั้นนั้นสำเร็จ - ป้าย **run manually / dev server** สำหรับขั้นที่ไม่ควรรันอัตโนมัติ แถบด้านข้างมี file tree โดยประมาณ ขั้นตอนถัดไป (next steps) และลิงก์เอกสารทางการ id ที่ไม่มีอยู่จะได้ HTTP 404 จริง (ตั้งแต่ v0.3.0, commit `b88a5c5`) ## เราแนะนำให้เลือกแบบนี้ - ต้องการ UI + API ใน repo เดียว → `nextjs-dashboard` หรือ `nextjs-ai-chat` - API ล้วน → `express-api` (Node) หรือ `fastapi-ai` (Python) - งานข้อมูลเร็ว ๆ → `streamlit-dashboard` - เผยแพร่แพ็กเกจ → `node-cli-npx` (npm) หรือ `pypi-package` (PyPI) ขั้น publish จะถูกคอมเมนต์ไว้เสมอ - ปุ่ม **Open in builder** เปิด `/builder#t=` ที่ขั้น Configure เพื่อปรับต่อ ## อ้างอิง [`src/components/catalog.tsx`](../../src/components/catalog.tsx), [`src/components/template-detail.tsx`](../../src/components/template-detail.tsx), [`src/app/templates/[id]/page.tsx`](../../src/app/templates/[id]/page.tsx), schema: [reference/template-schema.md](../reference/template-schema.md) --- # ตัวสร้างสแต็ก 4 ขั้น · The builder ตัวสร้าง (`/builder`) คือหัวใจของ NVX Stack Builder มันรับ "การตั้งค่า" (`BuilderConfig`) กับ "การแก้ไขของคุณ" (`BuilderEdits`) แล้วคำนวณลำดับคำสั่งสุดท้ายแบบเรียลไทม์ ในหน้านี้เราจะเล่าทั้งวิธีใช้และเหตุผลของแต่ละขั้น เวอร์ชันแรก (v0.1.0) เป็นหน้าเดียวยาว ๆ ซึ่งผู้ใช้หลายคนบอกเราว่าต้องคิดหลายเรื่องพร้อมกัน ใน v0.2.0 (commit `853feac`) เราจึงแยกเป็น wizard 4 ขั้นให้ตัดสินใจทีละเรื่อง ## ภาพรวมขั้นตอน (Stepper) | ขั้น | ชื่อ | ทำอะไร | ทำไมต้องแยกขั้น | |---|---|---|---| | 1 | **Start** | ถาม AI หรือเลือกเทมเพลต | ตัดสินใจเรื่องใหญ่ที่สุด (ประเภทแอป) ก่อน | | 2 | **Configure** | ชื่อโปรเจกต์, เวอร์ชัน Node/Python, npm/pnpm/yarn, pip/uv, add-on | ตัวเลือกพวกนี้เปลี่ยนคำสั่งทุกขั้น จึงต้องตั้งก่อนจัดลำดับ | | 3 | **Review & reorder** | อ่าน ลาก ลบ กู้คืน เพิ่มขั้นตอนเอง | ปรับแต่งบนลำดับที่ถูกต้องอยู่แล้ว แทนการเริ่มจากศูนย์ | | 4 | **Export** | คัดลอก แชร์ ดาวน์โหลด | ส่งมอบผลลัพธ์ในรูปแบบที่เหมาะกับแต่ละ OS | แถบสรุป **Current stack** ด้านบนจะแสดงเทมเพลต ชื่อโปรเจกต์ runtime ตัวจัดการแพ็กเกจ add-on และจำนวนขั้นตลอดเวลา พร้อมปุ่ม Copy ปุ่ม Back/Next ด้านล่างเลื่อนขั้น และโฟกัสคีย์บอร์ดจะย้ายไปที่หัวข้อขั้นใหม่เพื่อการเข้าถึง (accessibility) ## ขั้น 1: Start - แท็บ **Ask the AI agent** อธิบายแอปแล้วกด *Plan my stack* หรือ *Use offline planner* (ดู [ai-agent.md](./ai-agent.md)) เมื่อได้แผนแล้วกด **Load into builder** เทมเพลต add-on และขั้นตอนพิเศษจาก agent จะถูกโหลดเข้า - แท็บ **Pick a template** เลือกการ์ดเทมเพลตจากรายการ เหมือนหน้า catalog ## ขั้น 2: Configure - **Project name** เราทำให้ปลอดภัยด้วย `sanitizeProjectName` (ตัวพิมพ์เล็ก a-z 0-9 . _ - ยาวไม่เกิน 50) เพราะชื่อนี้จะกลายเป็นชื่อโฟลเดอร์และชื่อแพ็กเกจ npm/PyPI - **Node.js version (nvm)**: LTS, 24, 22, 20 และ **JS package manager**: npm / pnpm / yarn เลือก pnpm หรือ yarn จะเพิ่มขั้นเปิดใช้ตัวจัดการนั้นอัตโนมัติ - **Python version**: 3.13, 3.12, 3.11 และ **Python package manager**: pip / uv เลือก uv จะเพิ่มขั้นติดตั้ง uv และติดตั้ง Python ผ่าน uv - **Add-ons**: TypeScript, Tailwind CSS, Linting (ESLint / Ruff), Testing (Vitest / pytest), Docker, GitHub Actions CI add-on ที่เทมเพลตไม่รองรับจะถูกปิดและมีข้อความ "not available for this template" เบื้องหลัง ทุกตัวเลือกถูกแปลงเป็น placeholder เช่น `{{install}}`, `{{add}}`, `{{nextFlags}}` ใน composer จึงไม่ต้องเขียนเทมเพลตซ้ำสำหรับแต่ละตัวจัดการแพ็กเกจ (ดู [composition.md](../concepts/composition.md)) ## ขั้น 3: Review & reorder - **ลากที่จับ** เพื่อสลับลำดับ หรือใช้คีย์บอร์ด: โฟกัสที่จับ กด Space เพื่อยก กด ↑/↓ เพื่อย้าย กด Space อีกครั้งเพื่อวาง มีปุ่มเลื่อนขึ้น/ลงสำรองด้วย (การย้ายด้วยคีย์บอร์ดถูกแก้ให้ขยับทีละหนึ่งช่องเสมอใน commit `ae35a20`) - **ลบ** ขั้นตอนแล้วกู้คืนได้จากส่วน "Removed steps" มี toast ยืนยันทุกครั้ง - **Add a custom step** ใส่ชื่อและคำสั่ง (บรรทัดละคำสั่ง) คำสั่งผ่าน `sanitizeStep` เสมอ คำสั่งอันตราย เช่น `rm -rf /` ถูกบล็อก ส่วนคำสั่งเสี่ยง เช่น `curl ... | sh` หรือ `sudo` ถูกทำเครื่องหมายเป็น manual - **Reset edits** ย้อนกลับเป็นลำดับเริ่มต้นของเทมเพลต ขั้นตอนที่คุณหรือ AI เพิ่มจะถูกแทรก **ก่อน** ขั้นแรกที่เป็น long-running/publish/manual เพื่อให้ dev server อยู่ท้ายสุดเสมอ (ตรรกะใน `applyEdits`) ## ขั้น 4: Export - สลับ **Shell**: POSIX (bash/zsh) หรือ Windows PowerShell แล้วดูคำสั่งทั้งหมดในหน้าต่างโค้ด - **Copy all commands**, **Copy share link**, ดาวน์โหลด `setup.sh`, `setup.ps1`, `README.md`, **Starter .zip** (ดู [downloads.md](./downloads.md)) ## การจำสถานะ เราไม่อยากให้งานของคุณหาย ทุกการเปลี่ยนแปลงจึงถูกเข้ารหัสลง URL hash (`/builder#s=...`) และ `localStorage` คีย์ `nvx-builder-state` ทันที รีเฟรชหน้าแล้วได้สแต็กเดิม และ URL ในแถบที่อยู่คือ share link เสมอ ## อ้างอิง [`src/components/builder.tsx`](../../src/components/builder.tsx), [`src/components/sortable-steps.tsx`](../../src/components/sortable-steps.tsx), [`src/lib/composer.ts`](../../src/lib/composer.ts), [`src/lib/placeholders.ts`](../../src/lib/placeholders.ts) --- # AI agent planner AI agent ของเราช่วยแปลง "คำอธิบายแอปเป็นภาษาคน" ให้เป็นแผนสแต็ก (`AgentPlan`) ที่โหลดเข้าตัวสร้างได้ทันที เล่าเป็นไทยหรืออังกฤษก็ได้ ในหน้านี้เราจะอธิบายสามโหมดการทำงาน เหตุผลที่เราออกแบบแบบนี้ และสิ่งที่คุณควรรู้เรื่องความเป็นส่วนตัวและค่าใช้จ่าย ## หลักการออกแบบ เราตั้งใจให้ AI **ไม่เขียนคำสั่งตั้งค่าหลักเอง** มันเลือกเทมเพลตที่ใกล้ที่สุดจาก catalog (ส่ง `templateId` เป็น enum), เลือก add-on, ตัวจัดการแพ็กเกจ แล้ว *อาจ* เพิ่ม `extraSteps` (0–6 ขั้น) สำหรับงานนอกเทมเพลต เช่น ติดตั้ง database client คำสั่งหลักยังมาจาก composer ซึ่งผ่านการทดสอบแล้ว เหตุผลของเราคือ ลด hallucination และทำให้ผลลัพธ์ทำซ้ำได้ ส่วนที่มาจาก AI ทั้งหมดผ่าน zod validation และ sanitizer ก่อนถึงหน้าจอ ขั้นตอนที่ซ้ำกับคำสั่งของเทมเพลตจะถูกตัดออก ## สามโหมด 1. **ใช้ key ของเซิร์ฟเวอร์ (Server AI)** ถ้าผู้ดูแลตั้ง `OPENAI_API_KEY` ไว้ ป้ายบนแผงจะแสดง "Server AI: key configured · " (ข้อมูลจาก `GET /api/plan`) คำขอเรียก OpenAI Responses API แบบ Structured Outputs ด้วยโมเดลจาก `OPENAI_MODEL` (ค่าเริ่มต้น `gpt-6.1-sol`) โหมดนี้มี rate limit เข้มกว่า: 5 ครั้งต่อนาทีต่อ IP เพื่อให้เราเปิดบริการฟรีต่อไปได้โดยบิลไม่บานปลาย 2. **ใช้ key ของตัวเอง (own key)** ติ๊ก "Use my own OpenAI API key" แล้ววาง key key จะถูกส่งไปใน header `x-openai-key` ของคำขอนั้นเท่านั้น เซิร์ฟเวอร์ของเราไม่เก็บ ไม่ log และ redact ออกจากข้อความ error โหมดนี้ไม่ติด AI limiter (ติดเฉพาะ limiter ทั่วไป 20 ครั้งต่อนาที) เพราะใช้เงินของผู้ใช้เอง 3. **Offline planner (rule-based fallback)** กด "Use offline planner" หรือจะถูกใช้อัตโนมัติเมื่อไม่มี key, OpenAI error, timeout หรือผลลัพธ์ไม่ผ่าน validation ตัววางแผนนี้ให้คะแนนเทมเพลตจาก `keywords` (ไทย+อังกฤษ) และคำใบ้ runtime เช่น "python", "fastapi" จับ add-on จากคำ เช่น "docker", "ทดสอบ", "github actions" และดึงชื่อโปรเจกต์จากวลี "called ..." หรือ "ชื่อ ..." ผลลัพธ์ระบุ `source: "fallback"` และ `fallbackReason` ## เขียน prompt อย่างไรให้ได้ผลดี จากที่เราลองมาเยอะ prompt ที่ดีมักมีสามส่วน: - บอก **ประเภทแอป** + **runtime ที่ต้องการ** + **add-on** เช่น "FastAPI service ที่เรียก OpenAI พร้อม pytest และ Docker ใช้ uv" - ระบุชื่อโปรเจกต์ด้วยวลี `named my-app` หรือ `ชื่อ my-app` - ไม่ต้องสั่งให้ AI ทำสิ่งนอกขอบเขต คำสั่งในข้อความผู้ใช้ที่ขัดกับกฎระบบจะถูกเพิกเฉย (prompt injection guard ใน system instructions) - prompt ยาวได้ไม่เกิน 2,000 ตัวอักษรหลัง sanitize และต้องยาวอย่างน้อย 3 ตัวอักษร ## อ่านผลลัพธ์ แผงผลลัพธ์แสดง: ป้าย *AI plan* หรือ *Rule-based plan (offline)*, เหตุผล fallback, เทมเพลตที่เลือก, คำสั่งตามลำดับ, ขั้นตอนพิเศษจาก agent, ไฟล์เริ่มต้นสำคัญ และ **Safety notes** เช่น "Removed 1 unsafe or invalid command(s)" หรือขั้นที่ต้องตรวจเอง กด **Load into builder** เพื่อโหลดเข้าตัวสร้าง (ไฟล์เริ่มต้นจาก AI จะกลายเป็นขั้น `custom-ai-files`) ## ข้อผิดพลาดที่พบบ่อย - **429 Too many requests** รอประมาณ 60 วินาที (header `Retry-After: 60`) หรือใช้ offline planner - แผงขึ้น error state: กด *Try again* หรือ *Use offline planner* - ได้ fallback ทั้งที่มี key: ดู `fallbackReason` เช่น `OpenAI 401` (key ผิด) หรือ `AI request timed out` ## อ้างอิง [`src/components/agent-panel.tsx`](../../src/components/agent-panel.tsx), [`src/app/api/plan/route.ts`](../../src/app/api/plan/route.ts), [`src/lib/ai/openai.ts`](../../src/lib/ai/openai.ts), [`src/lib/ai/fallback.ts`](../../src/lib/ai/fallback.ts), [`src/lib/ai/plan.ts`](../../src/lib/ai/plan.ts), API: [reference/api-plan.md](../reference/api-plan.md), สถาปัตยกรรม: [architecture/ai-planner.md](../architecture/ai-planner.md) --- # Chat (User Chat) หน้า [`/chat`](https://devstack.bid/chat) คือแชต AI ฟรีของเรา ถามได้ทุกเรื่องทั้งภาษาไทยและอังกฤษ ให้ช่วยเขียนโค้ด หรือพิมพ์คำบรรยายแล้วให้มันวาดภาพให้ ไม่ต้องมี key การเข้าสู่ระบบเป็นทางเลือก ถ้ายังไม่เข้าสู่ระบบ ประวัติแชตอยู่กับเบราว์เซอร์นี้ ## เลือกโมเดล แถบเหนือช่องพิมพ์มีโมเดลให้เลือก เราจำตัวเลือกล่าสุดไว้ในเบราว์เซอร์ของคุณ (ยกเว้นโหมดภาพ) | ปุ่ม | โมเดล | เหมาะกับ | |---|---|---| | ทั่วไป · Gemma 4 (CF) | `@cf/google/gemma-4-26b-a4b-it` | คำถามทั่วไป สรุป อธิบาย | | ภาษาไทย · SEA-LION (CF) | `@cf/aisingapore/gemma-sea-lion-v4-27b-it` | ภาษาไทยที่เป็นธรรมชาติ บริบทเอเชียตะวันออกเฉียงใต้ | | โค้ด · Qwen Coder (CF) | `@cf/qwen/qwen2.5-coder-32b-instruct` | เขียน/อธิบาย/แก้โค้ด | | เก่งขึ้น · DeepSeek V3 (HF) | `deepseek-ai/DeepSeek-V3` ผ่าน Hugging Face | งานที่ต้องคิดหลายขั้น | | สร้างภาพ · FLUX (CF) | `@cf/black-forest-labs/flux-1-schnell` | ภาพจากคำบรรยาย (1024px, 4 steps) | | เกตเวย์ · GPT-5 mini | `openai/gpt-5-mini` | คำถามทั่วไปผ่าน Vercel AI Gateway | | เกตเวย์ · GPT-5 Codex | `openai/gpt-5-codex` | เขียนโค้ด | | เกตเวย์ · Claude Haiku 4.5 | `anthropic/claude-haiku-4.5` | ตอบเร็ว | | เกตเวย์ · Gemini 2.5 Flash | `google/gemini-2.5-flash` | คำถามทั่วไป | | เกตเวย์ · Qwen3 Coder | `alibaba/qwen3-coder` | เขียนโค้ด | | เกตเวย์ · DeepSeek V4 Flash | `deepseek/deepseek-v4-flash` | งานที่ต้องคิดหลายขั้น | CF = Cloudflare Workers AI, HF = Hugging Face, Vercel = [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) ปุ่มเกตเวย์กดได้เมื่อเซิร์ฟเวอร์มี `AI_GATEWAY_API_KEY` รายการ id มาจาก `GET https://ai-gateway.vercel.sh/v1/models` (ตรวจ 14 ต.ค. 2026) ถ้าโมเดลที่เลือกล่มหรือตอบไม่ได้ เราสลับไปอีกค่ายให้อัตโนมัติ (Workers AI ↔ DeepSeek V3, เกตเวย์ → Gemma 4 หรือ DeepSeek V3) และบันทึกไว้ว่าข้อความนั้นตอบด้วยโมเดลไหน ## เข้าสู่ระบบ ปุ่ม **เข้าสู่ระบบ** อยู่ที่หัวเว็บทุกหน้า และบนแถบประวัติแชต หน้า [`/login`](https://devstack.bid/login) มีสี่วิธี | วิธี | ใช้ทำอะไร | สิ่งที่เซิร์ฟเวอร์ต้องมี | |---|---|---| | Cursor | ติดตั้งแอป Origin ของ DevStack แล้วเข้าสู่ระบบด้วยผู้ใช้ที่กดติดตั้ง ใบเสร็จติดตั้งถูกตรวจลายเซ็น ไม่ถูกเก็บ | `CURSOR_APP_ID` และ callback `{origin}/api/auth/cursor/callback` | | Google | เข้าสู่ระบบด้วยบัญชี Google (openid, email, profile) ไม่เก็บ access token | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, callback `{origin}/api/auth/google/callback` | | GitHub | เข้าสู่ระบบและส่งโค้ดขึ้น repo ได้ ขอ `read:user`, `user:email`, `repo` token ถูกเข้ารหัสลง D1 | `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, callback `{origin}/api/auth/github/callback` | | บัญชี DevStack | อีเมลกับรหัสผ่านอย่างน้อย 10 ตัวอักษร เก็บเฉพาะแฮช PBKDF2 | `CHAT_COOKIE_SECRET` และ D1 | - อีเมลเดียวกันบนคนละวิธียังเป็นคนละบัญชี ไม่มีการผูกอัตโนมัติ - แชตที่สร้างไว้บนเบราว์เซอร์นี้ก่อนเข้าสู่ระบบครั้งแรกจะย้ายไปอยู่กับบัญชีนั้น ถ้าแถวนั้นยังไม่เคยเข้าสู่ระบบ - เปิดเบราว์เซอร์อื่นแล้วเข้าสู่ระบบบัญชีเดิม จะเห็นประวัติชุดเดียวกัน - ออกจากระบบจะลบ token GitHub ที่เข้ารหัสไว้ รหัสผ่านยังอยู่เพื่อเข้าใหม่ แชตยังอยู่ที่บัญชี - ถ้ายังไม่ตั้งค่าของวิธีนั้น หน้าเข้าสู่ระบบจะบอกว่าวิธีนั้นยังไม่เปิด บน production callback ใช้ origin `https://devstack.bid` ## ส่งโค้ดขึ้น GitHub เมื่อคำตอบมีบล็อกโค้ด จะมีปุ่ม **ส่งขึ้น GitHub** (หรือลิงก์ให้เข้าสู่ระบบก่อน) เลือก repo, พาธ, สาขา แล้ว commit ไฟล์เดียวผ่าน GitHub API เซิร์ฟเวอร์ไม่รันโค้ดนั้น ถ้าอยากกำหนดพาธจากในคำตอบ ให้ขึ้นบรรทัดแรกในบล็อกด้วย `// file: src/app.ts` หรือ `# file: src/hello.py` ## โควตาฟรี - **20 ข้อความ และ 5 ภาพ ต่อวัน** ต่อเบราว์เซอร์ (นับจาก cookie `nvx_chat_uid`) การสร้างภาพไม่กินโควตาข้อความ - รีเซ็ตตอน **เที่ยงคืนเวลาไทย** - มีเพดานต่อเครือข่าย (IP) = 3 เท่าของโควตาต่อคน และเพดานรวมทั้งเว็บต่อวัน เพื่อให้เราเปิดบริการฟรีต่อไปได้ - ส่งได้ไม่เกิน 4 ข้อความต่อนาที ตัวเลขที่เหลืออยู่แสดงใต้ช่องพิมพ์ เมื่อใช้ครบ เราจะแสดงข้อความแจ้งเป็นไทยและอังกฤษพร้อมเวลารีเซ็ต ## ไฟล์แนบ ปุ่มคลิปหนีบกระดาษยังกดไม่ได้ เพราะเรายังไม่เปิดที่เก็บไฟล์ (R2) ฝั่งเซิร์ฟเวอร์รองรับไฟล์ไม่เกิน 10 MB ไว้แล้ว เมื่อเปิด R2 ปุ่มนี้จะใช้ได้ทันที ## ประวัติและความเป็นส่วนตัว แชตของคุณเก็บใน D1 ถ้ายังไม่เข้าสู่ระบบ มันผูกกับ cookie ของเบราว์เซอร์นี้ ถ้าเข้าสู่ระบบด้วย GitHub มันผูกกับบัญชีนั้นแทน เปิดดูย้อนหลังได้จากแถบซ้าย (มือถือกดปุ่ม "ประวัติแชต") ลบได้ทีละแชต อย่าพิมพ์รหัสผ่านหรือข้อมูลส่วนตัวที่อ่อนไหวลงในแชต เราไม่เก็บ GitHub token แบบอ่านได้: ที่อยู่ในฐานข้อมูลเป็น ciphertext AES-GCM ## อยากได้มากกว่านี้ นักพัฒนาที่อยากให้ AI รันโค้ดใน sandbox ให้ดู ไปที่ [Dev Chat](./dev-chat.md) ถ้าอยากใช้ไม่จำกัดด้วย OpenAI key ของตัวเอง ไปที่ [Sandbox Factory](./sandbox-factory.md) --- # Dev Chat หน้า [`/chat/dev`](https://devstack.bid/chat/dev) สำหรับนักพัฒนา เราให้คุณคุยกับ **Image and Research Orchestrator** agent ที่สร้างด้วย OpenAI Agents API มันทำงานร่วมกับ subagent ผู้เชี่ยวชาญ ค้นคว้าเอกสาร Agents SDK และเอกสาร DevStack, เขียนและรันโค้ดใน sandbox Linux จริง, วางแผนเว็บไซต์ AI agent และสร้างภาพ ## แผง Sandbox ด้านขวา (มือถือคือแท็บ "Sandbox") แสดงสิ่งที่ agent กำลังทำแบบสด: สถานะ sandbox, คำสั่งที่รัน, subagent ที่ถูกเรียก, การค้นเอกสาร, ไฟล์ที่สร้าง และ token ที่ใช้ เรารัน sandbox บน OpenAI-hosted environment (`openai_hosted`, ขนาด small) แต่ละแชตใช้ session เดิมต่อเนื่อง ## โควตา - **5 turn ต่อวัน** ต่อเบราว์เซอร์ และ **30 turn ต่อวันรวมทั้งเว็บ** รีเซ็ตเที่ยงคืนเวลาไทย - ภาพจาก agent: 2 ภาพต่อคนต่อวัน (20 รวมทั้งเว็บ) - งบรวมต่อวัน $1 (ประมาณจาก usage) ถ้าเกิน agent จะพักจนถึงวันถัดไป เราตั้งเพดานต่ำเพราะ turn ของ agent ใช้ token มาก (turn ง่ายๆ ราว 50–70k input token ส่วนใหญ่เป็น cache) ถ้าครบโควตา ใช้ OpenAI key ของตัวเองใน [Sandbox Factory](./sandbox-factory.md) หรือกลับไปคุยใน [User Chat](./chat.md) ## เขียนคำสั่งให้ได้ผล - บอกให้ชัดว่าต้องการให้ "รันใน sandbox" และอยากเห็นผลแบบไหน - ถามเรื่อง Agents SDK หรือ DevStack ได้ agent จะค้นเอกสารและใส่ลิงก์อ้างอิง - ขอภาพได้ แต่ภาพนับโควตาแยก ## สิ่งที่ควรรู้ - คำตอบเป็น Markdown แบบปลอดภัย (ลิงก์ http(s) เท่านั้น รูปภาพเฉพาะจากเซิร์ฟเวอร์เรา) - ไฟล์ใน sandbox ดาวน์โหลดได้จากแผง Sandbox เมื่อ agent สร้างไฟล์ - ประวัติ Dev Chat แยกจาก User Chat (คอลัมน์ `channel = 'dev'`) --- # Sandbox Factory (ใช้ key ของตัวเอง) หน้า [`/factory`](https://devstack.bid/factory) ให้คุณสร้าง agent และ sandbox ของตัวเองบนบัญชี OpenAI ของคุณ แล้วคุยกับมันผ่านหน้าแชตเดียวกับ Dev Chat เราไม่คิดโควตาวันละกี่ครั้ง เพราะคุณจ่ายเอง เหลือเพียงเพดานกันสแปม ## key ของคุณ - วาง OpenAI API key ในช่องด้านบน key ถูกส่งไปกับแต่ละคำขอใน header `x-openai-key` เท่านั้น เซิร์ฟเวอร์ของเรา **ไม่เก็บและไม่ log** - ติ๊ก "จำไว้ในแท็บนี้" ถ้าอยากให้เบราว์เซอร์จำไว้ใน `sessionStorage` (หายเมื่อปิดแท็บ) - key ต้องมีสิทธิ์ `api.agents.read`, `api.agents.write`, `api.responses.write` ## ตั้งค่า 1. **Template**: Orchestrator (ชุด subagent และเครื่องมือเหมือน Dev Chat) หรือ Blank (agent เปล่าพร้อม sandbox) 2. **Model**: `gpt-6.1-sol` (ค่าเริ่มต้น), `gpt-6-astra`, `gpt-6-luna` 3. **Environment**: - **OpenAI-hosted** (ค่าเริ่มต้น): OpenAI รัน sandbox ให้ ใช้ได้ทันที - **Self-hosted**: sandbox รันบนเครื่องของคุณ เราจะแสดง environment id, remote URL และคำสั่งพร้อมคัดลอก: ```bash CODEX_API_KEY='' codex exec-server --remote '' --environment-id '' ``` แทน `` ด้วย environment key ของคุณ (เราไม่ใส่ key ลงในคำสั่ง) หน้าเว็บจะแสดงสถานะ pending → connected (หรือ failed) และมีปุ่มเฝ้าดูอีกครั้ง ## เพดานกันสแปม session ใหม่ 10 ต่อชั่วโมง, 60 turn ต่อชั่วโมง และ 10 คำขอต่อนาที ต่อ IP ## ข้อจำกัด - ไฟล์ที่ agent สร้างใน Factory ยังดาวน์โหลดผ่านเราไม่ได้ (ดูได้ในบัญชี OpenAI ของคุณ) - ข้อความแจ้งเตือนจาก OpenAI (key ผิด, สิทธิ์ไม่พอ, เครดิตหมด) แปลเป็นข้อความไทย/อังกฤษให้อ่านง่าย --- # ลิงก์แชร์ · Sharing links เราอยากให้การส่งต่อสแต็กง่ายเหมือนส่งลิงก์บทความ ลิงก์แชร์จึงทำให้คุณส่ง "สแต็กที่ปรับแต่งแล้ว" ให้เพื่อนร่วมทีม หรือเก็บไว้สร้างซ้ำในอนาคตได้ โดยไม่ต้องมีบัญชีผู้ใช้ และเราก็ไม่ต้องเก็บข้อมูลของคุณไว้ในฐานข้อมูลใด ๆ ## รูปแบบลิงก์ | รูปแบบ | ตัวอย่าง | เปิดแล้วไปที่ | |---|---|---| | Share link | `/builder#s=eyJ2Ijox...` | ขั้น **Review & reorder** พร้อม config และการแก้ไขทั้งหมด และมี toast "Loaded a shared stack from the link." | | Template link | `/builder#t=fastapi-ai` | ขั้น **Configure** ของเทมเพลตนั้นด้วยค่าเริ่มต้น (ปุ่ม Open in builder ใช้รูปแบบนี้) | | Pick tab | `/builder#pick` | ขั้น Start โดยเปิดแท็บ Pick a template | ถ้าไม่มี hash ตัวสร้างจะโหลดสถานะล่าสุดจาก `localStorage` (`nvx-builder-state`) หรือค่าเริ่มต้น (`nextjs-dashboard`) ## สิ่งที่อยู่ในลิงก์ และทำไมใช้ URL hash token หลัง `#s=` คือ JSON ที่เข้ารหัสแบบ base64url มีฟิลด์: `v` (เวอร์ชันรูปแบบ = 1), `c` (config), `o` (ลำดับ id ที่ผู้ใช้จัด), `r` (id ที่ลบ), `x` (ขั้นตอนที่เพิ่มเอง) ส่วนที่ว่างจะไม่ถูกใส่เพื่อให้ลิงก์สั้น เหตุผลที่เราเลือกใช้ **fragment (`#`)** แทน query string: เบราว์เซอร์ไม่ส่ง fragment ไปยังเซิร์ฟเวอร์ จึงไม่มีข้อมูลสแต็กไปโผล่ใน log ของเซิร์ฟเวอร์หรือ CDN และไม่ต้องมีฐานข้อมูลเก็บลิงก์ ข้อเสียคือลิงก์อาจยาวถ้ามีขั้นตอนเพิ่มเองเยอะ (จำกัด token ไม่เกิน 60,000 ตัวอักษร) ## ความปลอดภัยของลิงก์ที่ได้รับ ลิงก์แชร์ถือเป็น **ข้อมูลไม่น่าเชื่อถือ (untrusted)** เพราะใครก็สร้างได้ ตอนเปิด `decodeShare` จะ: - ตรวจรูปแบบด้วย zod schema (จำกัดจำนวน: order/removed ≤ 100 รายการ, custom ≤ 20 ขั้น) - ทำ config ให้ถูกต้องด้วย `normalizeConfig` (ค่าที่ไม่รู้จักกลับเป็นค่าเริ่มต้น, ชื่อโปรเจกต์ถูก sanitize) - ส่งทุกขั้นตอนที่เพิ่มเองผ่าน `sanitizeStep` คำสั่งทำลายระบบถูกตัดทิ้ง คำสั่งเสี่ยงกลายเป็น manual (คอมเมนต์ไว้ในสคริปต์) - token เสียหรือไม่ถูกต้อง → คืน `null` และตัวสร้างใช้ค่าเริ่มต้นแทน ไม่ crash ถึงเราจะป้องกันไว้หลายชั้น ก็ขอฝากให้ **อ่านสคริปต์ก่อนรันทุกครั้ง** โดยเฉพาะลิงก์จากคนที่คุณไม่รู้จัก ## วิธีแชร์ 1. ไปขั้น Export แล้วกด **Copy share link** (หรือคัดลอก URL จากแถบที่อยู่ ซึ่งอัปเดตตลอด) 2. ส่งลิงก์ ผู้รับเปิดแล้วจะได้สแต็กเดียวกันทุกประการ 3. ไฟล์ `README.md` และ `nvx-stack.json` ในไฟล์ zip ก็มีลิงก์นี้ด้วย ## ความเข้ากันได้ข้ามเวอร์ชัน เราสัญญากับตัวเองว่าลิงก์เก่าต้องเปิดได้เสมอ รูปแบบ `v: 1` จึงไม่เปลี่ยนเลยตั้งแต่ v0.1.0 (commit `a452dc5`) ถ้า id ของขั้นตอนในเทมเพลตเปลี่ยนภายหลัง ลำดับที่อ้าง id เก่าจะถูกข้าม และขั้นใหม่ถูกแทรกต่อจากขั้นก่อนหน้าตามธรรมชาติ (`applyEdits`) ถ้าวันหนึ่งต้องเปลี่ยนรูปแบบ เราจะเพิ่ม `v: 2` และคงตัวถอดรหัส `v: 1` ไว้ ## อ้างอิง [`src/lib/share.ts`](../../src/lib/share.ts), [`src/lib/sanitize.ts`](../../src/lib/sanitize.ts), [`src/components/builder.tsx`](../../src/components/builder.tsx), สถาปัตยกรรม: [scripts-and-share.md](../architecture/scripts-and-share.md) --- # การดาวน์โหลด · Downloads (setup.sh / setup.ps1 / README / zip) ขั้น **Export** ของตัวสร้างมีไฟล์ให้ดาวน์โหลด 4 แบบ ทั้งหมดสร้างในเบราว์เซอร์ของคุณเอง (client-side) จากขั้นตอนที่แสดงอยู่บนหน้าจอ ไม่มีอะไรถูกส่งมาที่เซิร์ฟเวอร์ของเรา ในหน้านี้เราจะเปิดให้ดูว่าสคริปต์ข้างในหน้าตาเป็นอย่างไร และทำไมเราเขียนแบบนั้น ## ไฟล์แต่ละแบบ | ไฟล์ | สำหรับ | สร้างโดย | |---|---|---| | `setup.sh` | macOS / Linux / WSL (bash) | `toBash()` | | `setup.ps1` | Windows PowerShell | `toPowerShell()` (บรรทัดลงท้าย CRLF) | | `README.md` | เอกสารโปรเจกต์ไทยหรืออังกฤษตามภาษาที่เลือก | `toReadme()` | | `-starter.zip` | ทั้งหมดข้างบน + `nvx-stack.json` + โฟลเดอร์ `starter-files/` | `buildStarterZip()` | ## ข้างใน setup.sh มีอะไร และทำไมเราเขียนแบบนี้ 1. **โหลด nvm ก่อน** (`. "$NVM_DIR/nvm.sh"`) เพราะ nvm เป็น shell function ไม่ใช่ไฟล์โปรแกรม สคริปต์ที่ไม่ได้ source จะหา `nvm` ไม่เจอ 2. `set -eo pipefail` หยุดทันทีเมื่อคำสั่งใดล้มเหลว เพื่อไม่ให้ขั้นต่อไปทำงานบนสถานะที่พัง 3. ฟังก์ชัน `step` พิมพ์หัวข้อ `==> 3/7 ...` และ `verify` รันคำสั่งตรวจสอบ ถ้าไม่ผ่านจะเตือน `[warn]` แต่ไม่หยุดสคริปต์ 4. ไฟล์เริ่มต้นเขียนด้วย heredoc แบบ `<<'NVX_EOF'` (มี quote) จึงไม่มีการแทนค่าตัวแปร sanitizer ปฏิเสธไฟล์ที่มีบรรทัด `NVX_EOF` เพื่อกันการหลุดออกจาก heredoc 5. ขั้นที่มี `requires` (เช่น nvm) ถูกห่อด้วย `if command -v nvm ...` ถ้าไม่พบจะข้ามพร้อมคำเตือน 6. ขั้นชนิด **longRunning** (dev server), **publish** (npm publish, twine upload) และ **manual** ถูก **คอมเมนต์ไว้** พร้อมหมายเหตุ เพราะ dev server จะค้างสคริปต์ไว้ และการ publish เป็นการกระทำสาธารณะที่ย้อนกลับไม่ได้ `setup.ps1` ทำแบบเดียวกันด้วย `$ErrorActionPreference = "Stop"`, ฟังก์ชัน `Step`, `Assert-Ok` (ตรวจ `$LASTEXITCODE` หลังทุกคำสั่ง), `Write-NvxFile` (เขียนไฟล์ด้วย here-string `@' ... '@`) และ `Verify` ## วิธีรันอย่างปลอดภัย เราแนะนำให้อ่านก่อนรันเสมอ คัดลอกคำสั่งด้านล่างไปใช้ได้เลย: ```bash mkdir ~/projects && cd ~/projects less setup.sh # อ่านทุกบรรทัดก่อน bash setup.sh ``` ```powershell Get-Content .\setup.ps1 | more powershell -ExecutionPolicy Bypass -File .\setup.ps1 ``` `-ExecutionPolicy Bypass` ใช้เฉพาะการรันครั้งนี้ ไม่เปลี่ยน policy ของเครื่อง ## ภายใน starter zip ``` -starter/ setup.sh (สิทธิ์ 755) setup.ps1 README.md (มี share link) nvx-stack.json (generator, shareUrl, config, steps[] แบบอ่านด้วยเครื่องได้) starter-files/... (ทุกไฟล์ที่ขั้นตอนต่าง ๆ จะเขียน ณ path เดิม) ``` เราใส่ `nvx-stack.json` ไว้เผื่อเครื่องมืออัตโนมัติหรือ AI agent ที่ต้องการอ่านลำดับขั้นตอนโดยไม่ต้อง parse shell script ## เคล็ดลับจากทีมเรา - รันในโฟลเดอร์ว่างเสมอ ขั้นแรกของเทมเพลตส่วนใหญ่สร้างโฟลเดอร์ `{{name}}` แล้ว `cd` เข้าไป - หลังสคริปต์จบ ให้รันขั้นที่คอมเมนต์ไว้ (เช่น `npm run dev`) ในเทอร์มินัลแยก - ผู้ร่วมพัฒนาสร้างสคริปต์จาก CLI ได้ด้วย `npm run export -- ` (ดู [npm-scripts.md](../reference/npm-scripts.md)) ## อ้างอิง [`src/lib/scripts.ts`](../../src/lib/scripts.ts), [`src/lib/download.ts`](../../src/lib/download.ts), [`scripts/export-template.ts`](../../scripts/export-template.ts) --- # ภาษา ธีม และการนำทาง · Language, theme & navigation DevStack เริ่มจากชุมชนนักพัฒนาไทย เราจึงทำให้ NVX Stack Builder รองรับภาษาไทยและอังกฤษเต็มรูปแบบตั้งแต่วันแรก มีธีมดำ (ค่าเริ่มต้น) และธีมสว่าง มี command palette สำหรับคนที่ชอบใช้คีย์บอร์ด และมีหน้าเอกสารที่คุณกำลังอ่านอยู่นี้ หน้านี้เล่าวิธีใช้และเหตุผลของการออกแบบ ## ภาษา (TH / EN) - ปุ่ม **ภาษาไทย / English** (ไอคอน Languages) บนแถบด้านบน สลับภาษาของทั้งแอปทันที ค่าถูกจำใน `localStorage` คีย์ `nvx-lang` ถ้ายังไม่เคยเลือก แอปจะใช้ภาษาไทยเมื่อภาษาของเบราว์เซอร์ (`navigator.language`) ขึ้นต้นด้วย `th` และใช้อังกฤษในกรณีอื่น - ข้อความ UI ทั้งหมดอยู่ใน [`src/lib/i18n.ts`](../../src/lib/i18n.ts) ส่วนเนื้อหาของเทมเพลตเป็นชนิด `L10n` (`{ en, th }`) ที่ต้องมีทั้งสองภาษาเสมอ - ภาษาที่เลือกมีผลกับ: คำอธิบายขั้นตอน, หัวข้อใน `setup.sh`/`setup.ps1`, `README.md` ที่ดาวน์โหลด และภาษาของคำตอบจาก AI agent (ส่ง `lang` ไปกับคำขอ) - คำสั่งและโค้ดเป็นภาษาอังกฤษเสมอ เพราะต้องรันได้จริง - ฟอนต์ไทยคือ **IBM Plex Sans Thai** (ตั้งแต่ v0.2.0) หัวข้อภาษาไทยใช้ letter-spacing ปกติและ line-height สูงขึ้น เพื่อไม่ให้สระและวรรณยุกต์ชนกัน ## ธีม (Dark / Light) - **ธีมดำเป็นค่าเริ่มต้นสำหรับทุกคน** ไม่ขึ้นกับการตั้งค่า OS (v0.1–v0.3 ใช้ `prefers-color-scheme` ของ OS; เปลี่ยนใน v0.4, commit `2210920`, ตามความต้องการของเจ้าของที่อยากได้ลุคเครื่องมือนักพัฒนา) ปุ่มพระอาทิตย์/พระจันทร์สลับเป็นธีมสว่าง ค่าถูกจำใน `localStorage` คีย์ `nvx-theme` - เซิร์ฟเวอร์เรนเดอร์ `` ไว้ก่อน และสคริปต์ pre-paint เล็ก ๆ ใน `layout.tsx` ถอดคลาสออกเฉพาะเมื่อผู้ใช้เลือก light จึงไม่มีการกะพริบสี (flash) ตอนโหลด - ดีไซน์ v0.5 อ้างอิง developer.apple.com: พื้นดำ ตัวหนังสือใหญ่ ปุ่มทรงแคปซูล สีฟ้าใช้อย่างจำกัด และโลโก้เครื่องมือจริงเป็นไทล์ (ดู [concepts/design-system.md](../concepts/design-system.md)) - ผู้ใช้ที่ตั้ง **prefers-reduced-motion** จะได้แอนิเมชันเกือบเป็นศูนย์ และเคอร์เซอร์ในหน้าต่างโค้ดหยุดกะพริบ ## Command palette - กด **Ctrl + K** (Windows/Linux) หรือ **⌘ + K** (macOS) หรือคลิกช่อง "Search or jump to…" - พิมพ์เพื่อค้นหา: หน้าต่าง ๆ (Home, Templates, Builder, Docs, Design system), เทมเพลตทุกตัว (เปิดหน้ารายละเอียดหรือเปิดในตัวสร้าง), ทุกหน้าในเอกสาร (กลุ่ม "Docs / เอกสาร") และ action สลับธีม/ภาษา - ↑↓ เลื่อน, Enter เปิด, Esc ปิด ## การนำทางบนมือถือ บนจอเล็ก เมนูจะยุบเป็นปุ่ม hamburger ที่เปิด sheet ด้านข้าง ในนั้นมีลิงก์ **เอกสาร (Docs)** ด้วย เราทดสอบทุกหน้าที่ความกว้าง 390px ว่าไม่มี horizontal scroll ทั้งภาษาไทยและอังกฤษ ## หน้าเอกสาร (`/docs`) - เปิดได้จากเมนูด้านบน (**เอกสาร / Docs**), เมนู hamburger บนมือถือ หรือ command palette - เดสก์ท็อปมี navigation drawer ด้านซ้ายแบ่งตามหมวด และ "ในหน้านี้" ด้านขวา บนมือถือกดปุ่มเมนูในแถบเอกสารเพื่อเปิด drawer - กด `/` เพื่อไปที่ช่องค้นหาในเอกสาร ค้นได้ทั้งไทยและอังกฤษ - ทุกหน้ามี **คัดลอก Markdown**, **ดูไฟล์ดิบ** (`.md`) และปุ่มคัดลอกบนทุกบล็อกโค้ด รายละเอียดเบื้องหลังอยู่ที่ [docs-site.md](../architecture/docs-site.md) ## อ้างอิง [`src/components/prefs.tsx`](../../src/components/prefs.tsx), [`src/app/layout.tsx`](../../src/app/layout.tsx), [`src/components/header.tsx`](../../src/components/header.tsx), [`src/components/command-palette.tsx`](../../src/components/command-palette.tsx), [`src/components/docs/docs-shell.tsx`](../../src/components/docs/docs-shell.tsx) --- # แก้ปัญหาและคำถามที่พบบ่อย · Troubleshooting & FAQ เรารวบรวมปัญหาที่ผู้ใช้และผู้ดูแลเจอบ่อยไว้ที่นี่ พร้อมสาเหตุและวิธีแก้ หลายข้อมาจากคำถามที่คนในชุมชนถามเราจริง ๆ ถ้ายังไม่พบคำตอบ ดูหน้า [operations](../operations/local-dev.md) สำหรับฝั่งเซิร์ฟเวอร์ หรือบอกเรา แล้วเราจะเพิ่มคำตอบไว้ในหน้านี้ให้คนถัดไป ## ฝั่งผู้ใช้ **`nvm: command not found` หรือขั้นติดตั้ง Node ถูกข้าม** nvm เป็น shell function ต้องติดตั้งก่อน (ดู OS notes ในขั้นแรก) แล้วเปิดเทอร์มินัลใหม่ สคริปต์จะพยายาม source `$NVM_DIR/nvm.sh` ให้อัตโนมัติ ถ้าไม่พบจะพิมพ์ `[warn] nvm not found — skipping` แทนการล้มทั้งสคริปต์ บน Windows ใช้ nvm-windows (คำสั่ง `nvm install lts`) **สคริปต์หยุดกลางทาง** `setup.sh` ใช้ `set -eo pipefail` และ `setup.ps1` ใช้ `Assert-Ok` จึงหยุดที่คำสั่งแรกที่ล้มเหลว ดูหัวข้อ `==> n/total` ล่าสุดเพื่อรู้ว่าขั้นไหน แก้สาเหตุ (เช่น เน็ตหลุด, เวอร์ชัน Node ไม่ตรง) แล้วรันคำสั่งที่เหลือด้วยมือ หรือลบโฟลเดอร์โปรเจกต์แล้วรันใหม่ **`[warn] verification failed`** คำสั่ง verify ไม่ผ่าน แต่สคริปต์ยังทำต่อ ให้อ่าน "Expected result" ของขั้นนั้นแล้วตรวจด้วยมือ บางครั้งเป็นเพราะ PATH ยังไม่อัปเดตจนกว่าจะเปิดเทอร์มินัลใหม่ **PowerShell ไม่ยอมรันสคริปต์** ใช้ `powershell -ExecutionPolicy Bypass -File .\setup.ps1` ซึ่งข้าม policy เฉพาะครั้งนี้ **ทำไม `npm run dev` ไม่ถูกรัน** ขั้น dev server เป็นชนิด `longRunning` ถูกคอมเมนต์ไว้โดยตั้งใจ เพราะจะค้างสคริปต์ ให้รันเองในเทอร์มินัลแยก เช่นเดียวกับขั้น publish **คำสั่งที่ฉันเพิ่มเองหายไป / มีข้อความ "That command was blocked"** sanitizer บล็อกคำสั่งทำลายระบบ (เช่น `rm -rf /`, `mkfs`, `dd if=`, fork bomb, `shutdown`) และคำสั่งยาวเกิน 400 ตัวอักษร ส่วนคำสั่งเสี่ยง (`curl | sh`, `sudo`, `eval`, `npm publish`, `git push --force`) ยังอยู่แต่ถูกทำเป็น manual ดู [safety-model.md](../concepts/safety-model.md) **AI ตอบ "Too many requests"** เกิน rate limit (20 POST/นาที/IP ทุกโหมด และ 5 ครั้ง/นาที/IP เมื่อใช้ key ของเซิร์ฟเวอร์) รอ 60 วินาที หรือใช้ offline planner หรือใช้ key ของตัวเอง **ได้แผน "Rule-based (offline)" ทั้งที่อยากได้ AI** ดู "Fallback reason" ในแผง: `no OpenAI API key configured` (เซิร์ฟเวอร์ไม่มี key), `OpenAI 401` (key ผิด), `AI request timed out`, `invalid AI output` **ลิงก์แชร์เปิดแล้วได้ค่าเริ่มต้น** token เสีย (ถูกตัดตอนคัดลอก) หรือเกิน 60,000 ตัวอักษร ให้คัดลอกใหม่ทั้งบรรทัด ## FAQ - **แอปรันคำสั่งบนเซิร์ฟเวอร์ไหม?** ไม่ ไม่มีโค้ดเส้นทางใดของเราที่ execute คำสั่ง ทุกอย่างเป็นข้อความ - **key ของฉันถูกเก็บไหม?** ไม่ ใช้เฉพาะคำขอเดียว ไม่ log และ redact ออกจาก error - **ข้อมูลสแต็กถูกส่งไปไหน?** share link อยู่ใน URL fragment ซึ่งไม่ถูกส่งไปเซิร์ฟเวอร์ prompt ถูกส่งไปที่ `/api/plan` (และ OpenAI ถ้าใช้ AI โดยตั้ง `store: false`) และไม่ถูก log - **รองรับ Windows ไหม?** รองรับผ่าน `setup.ps1` คำสั่งที่ไม่มี override เฉพาะ Windows จะถูกแปลงแบบ best-effort (`curl` → `curl.exe`, `python3` → `py -3.12`, `&&` → `;`) - **เพิ่มเทมเพลตได้ไหม?** ได้ และเรายินดีมาก ดู [add-template.md](../contributing/add-template.md) - **เอาเอกสารไปใช้ต่อได้ไหม?** ได้ ทุกหน้ามีปุ่มคัดลอก Markdown และ URL `.md` ส่วน AI agent อ่านสารบัญได้ที่ `/llms.txt` ## อ้างอิง [`src/lib/sanitize.ts`](../../src/lib/sanitize.ts), [`src/lib/scripts.ts`](../../src/lib/scripts.ts), [`src/lib/composer.ts`](../../src/lib/composer.ts) --- # แนวคิด Stack Template · The Stack Template concept ## นิยาม **Stack Template** คือข้อมูล (data) ที่อธิบายวิธีตั้งค่าโปรเจกต์ประเภทหนึ่งตั้งแต่โฟลเดอร์ว่างจนถึงรันได้ ประกอบด้วยรายการ **ขั้นตอน (StepDef)** ที่เรียงลำดับแล้ว พร้อมข้อมูลประกอบ (metadata) เช่น runtime, add-on ที่รองรับ, keyword สำหรับค้นหา, file tree และลิงก์เอกสารทางการ ชนิดข้อมูลกำหนดใน [`src/lib/types.ts`](../../src/lib/types.ts) (`StackTemplate`, `StepDef`) ## ทำไมเป็น "ข้อมูล" ไม่ใช่ "โค้ด" ตั้งแต่ v0.1.0 (commit `a452dc5`) เทมเพลตทุกตัวเป็นไฟล์ `.ts` ที่ default-export อ็อบเจกต์ธรรมดา เหตุผล: 1. **ตรวจทานง่าย** reviewer อ่านคำสั่งได้ตรง ๆ ไม่ต้องไล่ตรรกะ 2. **ทดสอบได้** unit test ประกอบคำสั่งของทุกเทมเพลตในทุกตัวเลือกได้ (`tests/composer.test.ts`) 3. **ใช้ซ้ำได้หลายที่** ข้อมูลเดียวกันใช้ทั้งหน้า catalog, ตัวสร้าง, AI prompt (catalog ถูกฝังใน system instructions), offline planner, CLI export และ JSON Schema ของ Structured Outputs (`templateId` enum) 4. **type-safe** TypeScript บังคับให้มีทั้ง `en` และ `th` ในทุกข้อความ (`L10n`) ## องค์ประกอบของขั้นตอน (StepDef) | ฟิลด์ | ความหมาย | เหตุผล | |---|---|---| | `id` | id คงที่ | ใช้ตัดซ้ำ (dedup), อ้างอิงในลิงก์แชร์ และการจัดลำดับ | | `title`, `explanation` | ชื่อ + เหตุผลของขั้น (TH/EN) | ผู้ใช้ควรเข้าใจว่า *ทำไม* ไม่ใช่แค่ *ทำอะไร* | | `commands` | คำสั่ง POSIX (มี placeholder) | แหล่งเดียวสำหรับทุกตัวจัดการแพ็กเกจ | | `windows` | override สำหรับ PowerShell | บางคำสั่งแปลงอัตโนมัติไม่ได้ (activate venv, ตัวแปร env) | | `files` | ไฟล์เริ่มต้นที่ขั้นนี้เขียน | ได้โค้ดที่รันได้ทันที ไม่ใช่แค่โครงเปล่า | | `osNotes` | หมายเหตุตาม OS | เช่น nvm-windows, WSL | | `expected`, `verify` | ผลที่คาดหวัง + คำสั่งพิสูจน์ | ตรวจสอบได้ทีละขั้น | | `kind` | setup / longRunning / publish / manual | ควบคุมว่าสคริปต์รันอัตโนมัติหรือคอมเมนต์ไว้ | | `requires` | คำสั่งที่ต้องมี เช่น `nvm` | สคริปต์ข้ามพร้อมคำเตือนแทนการล้ม | | `when` | เงื่อนไข add-on เช่น `{ testing: true }` | เทมเพลตเดียวรองรับหลายรูปแบบ | ## Placeholder: เขียนครั้งเดียว ใช้ได้ทุกตัวเลือก คำสั่งในเทมเพลตเขียนด้วย placeholder เช่น `{{add}} express cors`, `{{run}} dev`, `{{createNext}} {{name}} {{nextFlags}}` composer แทนค่าตามตัวเลือกของผู้ใช้ (`npm install` / `pnpm add` / `yarn add`) ข้อความในไฟล์เริ่มต้นที่เป็น TypeScript-only ห่อด้วย `⟨ ⟩` และจะถูกตัดออกเมื่อปิด TypeScript รายการ placeholder ทั้งหมดอยู่หัวไฟล์ [`src/lib/placeholders.ts`](../../src/lib/placeholders.ts) และใน [template-schema.md](../reference/template-schema.md) ## ขั้นตอนร่วม (shared steps) ขั้นที่ใช้หลายเทมเพลตอยู่ใน [`src/data/common-steps.ts`](../../src/data/common-steps.ts) (เช่น `mkdirAndCd`, `npmInit`, `createNextApp`, `pyVenv`), ขั้นตั้งค่า runtime อยู่ใน [`src/data/runtime-steps.ts`](../../src/data/runtime-steps.ts) (nvm, pnpm, yarn, python check, uv) และขั้นของ add-on อยู่ใน [`src/data/addon-steps.ts`](../../src/data/addon-steps.ts) การใช้ id เดียวกันทำให้ dedup ทำงานถูกต้องเมื่อขั้นเดียวกันมาจากหลายแหล่ง ## คำแนะนำในการออกแบบเทมเพลต - คำสั่งต้อง **ถูกต้อง ทันสมัย และ non-interactive** (ใช้ `--yes`, `-y`) อ้างอิงเอกสารทางการและใส่ลิงก์ใน `docs` - ใส่ `verify` ทุกขั้นที่ทำได้ ใช้คำสั่งอ่านอย่างเดียว เช่น `node -v`, `{{list}} express` - dev server เป็น `longRunning` เสมอ, การเผยแพร่เป็น `publish`, ตัวติดตั้งหรือการเปลี่ยนแปลงระดับเครื่องเป็น `manual` - ใส่ `keywords` ทั้งไทยและอังกฤษ เพื่อให้ offline planner เลือกถูก ## ดูเพิ่ม [composition.md](./composition.md) (การประกอบและเรียงลำดับ), [add-template.md](../contributing/add-template.md) (ตัวอย่างเต็ม), [template-schema.md](../reference/template-schema.md) --- # การประกอบคำสั่ง ลำดับ และการตัดซ้ำ · Command composition, ordering & dedup หน้านี้อธิบายตรรกะใน [`src/lib/composer.ts`](../../src/lib/composer.ts) ซึ่งเปลี่ยน `BuilderConfig` เป็นรายการ `ResolvedStep` ที่พร้อมแสดงและส่งออก ตรรกะนี้ไม่ได้เปลี่ยนเลยตั้งแต่ v0.1.0 (commit `a452dc5`) และมี unit test ครอบคลุมใน `tests/composer.test.ts` ## ขั้นตอนของ composeSteps() 1. **normalizeConfig** ทำ config ที่อาจไม่น่าเชื่อถือ (จากลิงก์แชร์หรือ AI) ให้ถูกต้อง: template ที่ไม่รู้จักกลับเป็น `nextjs-dashboard`, เวอร์ชัน/ตัวจัดการที่ไม่รู้จักกลับเป็นค่าเริ่มต้น (`lts`, `3.12`, `npm`, `pip`), add-on กรองเหลือเฉพาะใน `ADDON_IDS`, ชื่อโปรเจกต์ผ่าน `sanitizeProjectName` 2. **buildContext** สร้างตาราง placeholder จาก config + template เช่น `install`, `add`, `run`, `nextFlags` (`--ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm --yes`), `venv`, `pip` 3. **ขั้น runtime** ถ้าเทมเพลตใช้ Node: `nvmStep` (+ `pnpmStep` หรือ `yarnStep`) ถ้าใช้ Python: `uvInstallStep` + `uvPythonStep` (เมื่อเลือก uv) หรือ `pythonCheckStep` 4. **ขั้นของเทมเพลต** กรองด้วย `when` (`matchesWhen`: add-on ต้องทั้งถูกเลือกและเทมเพลตรองรับ) 5. **ขั้นของ add-on** (`addonSteps`) ถูก **แทรกก่อนขั้นแรกที่เป็น longRunning / publish / manual** ของเทมเพลต 6. **resolveStep** แทนค่า placeholder ทุกฟิลด์ สร้างคำสั่ง Windows (ใช้ `windows` override หรือ `adaptForPowerShell`) และกำหนด `source` (runtime / template / addon) 7. **dedupeSteps** ตัดซ้ำ ### ทำไมแทรก add-on ก่อน dev server ลำดับที่ถูกคือ ติดตั้งทุกอย่าง → ตั้งค่า lint/test/Docker/CI → เริ่ม dev server ถ้าแทรก add-on ท้ายสุด ขั้นติดตั้ง Vitest จะอยู่หลัง `npm run dev` ซึ่งในสคริปต์ถูกคอมเมนต์ไว้ ผู้ใช้ที่รันตามลำดับในเอกสารจะติดอยู่ที่ dev server ก่อนติดตั้งเครื่องมือครบ ## กฎการตัดซ้ำ (dedup) `dedupeSteps` เก็บขั้นแรกที่พบ และตัดขั้นหลังที่: - มี **id ซ้ำ** หรือ - มี **signature ซ้ำ** คือ JSON ของ `[commands, paths ของ files]` เหมือนกัน (ขั้นที่ไม่มีทั้งคำสั่งและไฟล์ใช้ id เป็น signature) เหตุผล: ขั้นเดียวกันอาจมาจากหลายแหล่ง เช่น เทมเพลตและ add-on ต่างก็ต้อง `cd` เข้าโปรเจกต์ หรือ AI เสนอขั้นที่ทำสิ่งเดียวกับเทมเพลตภายใต้ id อื่น การเทียบ signature จับกรณีหลังได้ นอกจากนี้ `sanitizeAiPlan` ยังตัดคำสั่งของ AI ที่ตรงกับคำสั่งเทมเพลตออกก่อนด้วย ## การแก้ไขของผู้ใช้ (applyEdits) `BuilderEdits` มีสามส่วน: `order` (ลำดับ id ที่ผู้ใช้จัด), `removed` (id ที่ลบ) และ `custom` (ขั้นที่เพิ่มเอง / จาก AI) 1. แทรก `custom` **ก่อนขั้นแรกที่ไม่ใช่ setup** แล้ว dedup อีกรอบ ได้ลำดับ "ธรรมชาติ" 2. เริ่มจาก `order` ที่ผู้ใช้จัด (ตัด id ที่ไม่มีอยู่แล้ว ที่ถูกลบ และที่ซ้ำ) 3. ขั้นที่ยังไม่อยู่ใน `order` (เช่น เพิ่งเปิด add-on ใหม่) ถูกแทรก **ต่อจากขั้นก่อนหน้าตามธรรมชาติที่อยู่ในผลลัพธ์แล้ว** ไม่ใช่ท้ายสุด ผลคือผู้ใช้จัดลำดับเองแล้วยังเปลี่ยน add-on ต่อได้โดยลำดับไม่พัง และลิงก์แชร์เก่าที่อ้าง id ซึ่งไม่มีแล้วก็ยังใช้ได้ ## ฟังก์ชันอื่นที่เกี่ยวข้อง - `moveStep(ids, id, ±1)` สลับตำแหน่งสำหรับปุ่มขึ้น/ลง - `flattenCommands(steps, os)` คำสั่งทั้งหมดแบบเรียบ สำหรับ "Copy all" - `configForTemplate(id, base)` config ของเทมเพลตพร้อม add-on เริ่มต้นและชื่อเริ่มต้น ## ข้อควรระวังเมื่อแก้ composer - ห้ามเปลี่ยนความหมายของ id เดิมโดยไม่จำเป็น เพราะลิงก์แชร์อ้าง id - ทุกการเปลี่ยนต้องมี test ใน `tests/composer.test.ts` และรัน `npm run export -- ` เพื่อดูสคริปต์จริง ## ดูเพิ่ม [architecture/templates-and-composer.md](../architecture/templates-and-composer.md), [stack-template.md](./stack-template.md) --- # โมเดลความปลอดภัย · Safety model NVX Stack Builder จัดการกับ "คำสั่ง shell" ซึ่งเป็นข้อมูลอันตรายโดยธรรมชาติ หน้านี้อธิบายหลักการ ชั้นป้องกัน และขอบเขตของสิ่งที่ระบบป้องกันได้และไม่ได้ ## หลักการข้อที่ 1: ไม่รันคำสั่งเด็ดขาด (text-only) เซิร์ฟเวอร์ **ไม่มีโค้ดเส้นทางใด** ที่ execute คำสั่ง ไม่มี `child_process`, ไม่มี sandbox รันโค้ด คำสั่งทั้งหมดเป็นสตริงที่แสดงผลและส่งออกเป็นไฟล์ให้ผู้ใช้ตรวจและรันเอง นี่คือกฎที่ระบุใน [CONTRIBUTING.md](../../CONTRIBUTING.md) และต้องคงไว้ตลอด เพราะถ้าเซิร์ฟเวอร์รันคำสั่งได้ ทุกช่องโหว่ใน sanitizer จะกลายเป็น remote code execution ทันที ## หลักการข้อที่ 2: ข้อมูลภายนอกไม่น่าเชื่อถือทั้งหมด แหล่งที่ไม่น่าเชื่อถือมีสามแหล่ง: **ผลลัพธ์จาก AI**, **ลิงก์แชร์**, และ **ขั้นตอนที่ผู้ใช้พิมพ์เอง** ทั้งหมดผ่าน [`src/lib/sanitize.ts`](../../src/lib/sanitize.ts) ก่อนแสดงผล ### ชั้นของ sanitizer | ฟังก์ชัน | ทำอะไร | |---|---| | `cleanText` | ตัด ANSI escape และ control characters, trim, จำกัดความยาว (ข้อความ 1,200 ตัวอักษร) | | `cleanCommand` | บังคับบรรทัดเดียว, จำกัด 400 ตัวอักษร, **ทิ้ง** คำสั่งที่อยู่ในรายการ BLOCKED | | `cleanPath` | ต้องเป็น path สัมพัทธ์ ไม่มี `..` ไม่ขึ้นต้นด้วย `/` หรือ `C:` และอักขระอยู่ในชุดที่อนุญาต | | `cleanFile` | ไฟล์ ≤ 12,000 ตัวอักษร, ปฏิเสธเนื้อหาที่มีบรรทัด `NVX_EOF` หรือ `'@` (กันการหลุดจาก heredoc/here-string) | | `sanitizeStep` | รวมทุกอย่าง: ≤ 12 คำสั่ง, ≤ 8 ไฟล์, ต้องมีชื่อ และต้องมีคำสั่งหรือไฟล์อย่างน้อยหนึ่ง | **BLOCKED (ทิ้งทั้งคำสั่ง):** `rm -rf /`, `~`, `$HOME`, `*`; `sudo rm`; `mkfs`; `dd if=`; fork bomb; `shutdown/reboot/halt/poweroff`; `chmod 777 /`; เขียนทับ `/dev/sd*`; `format C:`; `Remove-Item -Recurse` ที่ราก; `del /s` ที่ไดรฟ์ **RISKY (เก็บไว้แต่เปลี่ยนเป็น manual):** `curl|wget|iwr ... | sh/bash/python/node/iex`, `sudo`, `eval`, `npm/pnpm/yarn publish`, `twine upload`, `git push --force` ขั้นชนิด manual จะถูกคอมเมนต์ใน `setup.sh`/`setup.ps1` และ AI plan จะแสดง safety note ## หลักการข้อที่ 3: มนุษย์ตัดสินใจขั้นที่ย้อนกลับไม่ได้ ขั้นชนิด `longRunning`, `publish`, `manual` **ไม่ถูกรันอัตโนมัติ** ในสคริปต์ที่ดาวน์โหลด การ publish ไปยัง npm/PyPI ต้องให้คนตรวจแล้วรันเองเสมอ ## ชั้นป้องกันฝั่ง API (ตั้งแต่ v0.3.0) - จำกัด body 16 KB (413), รับเฉพาะ JSON (415), timeout ต่อคำขอ - rate limit สองชั้น (ทุก POST 20/นาที/IP และการใช้ key ของเซิร์ฟเวอร์ 5/นาที/IP; IPv6 นับต่อ /64) - prompt ถูก clean และจำกัด 2,000 ตัวอักษร; system instructions สั่งให้ถือข้อความผู้ใช้เป็นคำอธิบายผลิตภัณฑ์เท่านั้น (กัน prompt injection) - Structured Outputs แบบ strict + zod (`sanitizeAiPlan`) — template ต้องอยู่ใน catalog, add-on ต้องรองรับ - ไม่ log prompt, key หรือ header; error ผ่าน `redactSecrets` (`sk-***`, `Bearer ***`, `cfat_***`) - security headers: CSP, HSTS, `X-Frame-Options: DENY`, COOP/CORP ฯลฯ (ดู [api-hardening.md](../architecture/api-hardening.md)) ## ขอบเขตที่ป้องกันไม่ได้ regex blocklist ไม่มีทางครบทุกกรณี คำสั่งที่ "ดูปกติ" แต่เป็นอันตราย (เช่น ติดตั้งแพ็กเกจที่ถูก typosquat) ยังผ่านได้ ดังนั้น **การอ่านสคริปต์ก่อนรันคือการป้องกันชั้นสุดท้ายที่สำคัญที่สุด** UI และ README ที่สร้างให้ย้ำข้อนี้ทุกที่ ถ้าพบวิธีหลบ sanitizer ให้รายงานผ่าน security advisory แบบส่วนตัว ไม่เปิด issue สาธารณะ ## ดูเพิ่ม [architecture/ai-planner.md](../architecture/ai-planner.md), [user-guide/sharing.md](../user-guide/sharing.md), test: `tests/scripts-share-ai.test.ts` (describe "sanitizer") --- # ระบบดีไซน์ · Design system (NVX UI) NVX UI คือระบบดีไซน์ของแอป ประกอบด้วย **design tokens** (ตัวแปร CSS → ธีม Tailwind v4), **ไลบรารีคอมโพเนนต์** ใน `src/components/ui` และ **style guide แบบมีชีวิต** ที่ `/design` หน้านี้อธิบายแนวคิด ส่วนรายละเอียดเชิงเทคนิคทั้งหมดอยู่ใน [UI-FRAMEWORK.md](../UI-FRAMEWORK.md) และ [architecture/ui-framework.md](../architecture/ui-framework.md) ## แนวคิดหลัก 1. **Semantic tokens เท่านั้น** คอมโพเนนต์ใช้ utility ตามความหมาย เช่น `bg-surface`, `text-muted`, `bg-primary`, `text-link` ไม่ใช้สีดิบ (`bg-zinc-900`) เหตุผล: เปลี่ยนธีมทั้งแอปได้จากไฟล์เดียว ([`src/app/tokens.css`](../../src/app/tokens.css)) และไม่ต้องใช้ `dark:` ในคอมโพเนนต์เลย 2. **Accessibility เป็นค่าเริ่มต้น** คอมโพเนนต์ที่โต้ตอบได้สร้างบน Radix UI (คีย์บอร์ด, focus trap, ARIA ครบ), ทุกคู่สีข้อความผ่าน WCAG AA ทั้งสองธีมและมี test บังคับ (`tests/tokens.test.ts`), เคารพ `prefers-reduced-motion` 3. **ปรับสไตล์ได้โดยไม่แตะโครงสร้าง** ธีมเปลี่ยนมาแล้วสามครั้ง (v0.2 → v0.4 → v0.5) โดยโครงสร้างหน้าและคอมโพเนนต์คงเดิม เพราะดีไซน์ถูกแยกเป็น token ## วิวัฒนาการ (สรุป) | เวอร์ชัน | ลุค | commit หลัก | |---|---|---| | v0.1.0 | Tailwind พื้นฐาน, utility class ส่วนกลาง (`.card`, `.btn-*`), ฟอนต์ Noto Sans Thai | `a452dc5` | | v0.2.0 | NVX UI: tokens + คอมโพเนนต์ + `/design`, Geist + IBM Plex Sans Thai, light/dark ตาม OS | `3e5a8c2`, `4ac2756`, `1b0f105` | | v0.4 | Terminal theme: ดำเป็นค่าเริ่มต้น, เขียวฟอสฟอร์, IBM Plex Mono ทั่ว UI, สัญลักษณ์ DOS | `2210920`, `1219529` | | v0.5 | อ้างอิง developer.apple.com: ดำล้วน, Inter, ปุ่มแคปซูล, ฟ้า `#0071e3` + link `#2997ff`, โลโก้เครื่องมือจริง, ไม่มีอีโมจิ | `b6e61a5`, `382750c`, `3dad906` | รายละเอียดเหตุผลแต่ละครั้งดู [SUBSYSTEM-HISTORY.md](../history/SUBSYSTEM-HISTORY.md) ## หลักการของ v0.5 ที่ควรรักษา - **พื้นดำเป็นค่าเริ่มต้น** สำหรับทุกคน ธีมสว่างเลือกได้จากปุ่มสลับ - **สีฟ้าสองบทบาท**: `primary` (#0071e3) สำหรับปุ่มทึบที่มีตัวอักษรขาว และ `link` สำหรับข้อความสีฟ้า เพราะ #0071e3 บนพื้นดำไม่ผ่าน AA สำหรับข้อความ ห้ามใช้ `text-primary` กับข้อความ - **ฟอนต์**: Inter สำหรับ UI, IBM Plex Sans Thai สำหรับไทย, IBM Plex Mono เฉพาะโค้ด/คำสั่ง/ป้ายเล็ก - **รูปทรง**: มุมโค้ง 6/10/12/18/22px, ปุ่มและชิปเป็นแคปซูล (`rounded-full`) - **ไอคอน**: โลโก้เครื่องมือจริงจาก simple-icons (`ToolTile`, `ToolIcon`) + ไอคอน UI จาก lucide-react ห้ามใช้อีโมจิ (มี test บังคับใน `tests/tool-icons.test.tsx`) โลโก้เป็นเครื่องหมายการค้าของเจ้าของ ใช้เพื่อระบุเครื่องมือเท่านั้น (ดู [NOTICE](../../NOTICE)) - **มาสคอตสีชมพู**: โลโก้ (ก้อนสีชมพูมีตาสองข้าง) เป็นตัวละครที่ขยับได้ ใน header ลอยและกะพริบตาอย่างสุภาพ ใน hero ตามองตามเมาส์และเด้งเมื่อคลิก เหตุผลคือให้แบรนด์มีบุคลิกโดยไม่แตะระบบสีหลัก สีชมพู `#fb0fab` (วัดจากโลโก้) ใช้เป็นสีตกแต่งเท่านั้น ส่วนปุ่ม ลิงก์ และโฟกัสยังเป็นสีฟ้า เพื่อไม่ให้ผู้ใช้สับสนว่าอะไรกดได้ ดูตัวอย่างที่ `/design#mascot` - **กลิ่นอายเทอร์มินัล** เหลือเฉพาะหน้าต่างโค้ด: จุดสามสีแบบ macOS, prompt `$` สีเขียว และเคอร์เซอร์กะพริบใน hero ## ซิกเนเจอร์ DevStack (terminal signature) ซิกเนเจอร์คือหน้าต่างเทอร์มินัลสีดำขอบขาวหนา มีไทล์เครื่องมือ 4 ช่อง (JavaScript, curl, npm, PyPI) และ prompt ที่พิมพ์คำสั่งวนไปเรื่อย ๆ เราวางไว้ใน **ส่วนท้ายของทุกหน้า** และในหน้าแรกของ `/docs` โดยไม่เปลี่ยน layout ของหน้าอื่น คอมโพเนนต์อยู่ที่ [`src/components/brand/terminal-signature.tsx`](../../src/components/brand/terminal-signature.tsx) สไตล์อยู่ใน `globals.css` (คลาส `.sig*`) และดูตัวอย่างได้ที่ `/design#signature` - สร้างด้วย HTML + SVG แบบ inline (โลโก้ JS, npm, Python จาก simple-icons ส่วน curl เป็นข้อความ `curl://`) ขนาดทั้งหมดเป็นหน่วย container query (`cqw`) จึงคมชัดทุกขนาด - **แสง**: ขอบสว่างตามตำแหน่งเมาส์ (CSS vars `--sig-x`, `--sig-y`, `--sig-o` กับ radial gradient ที่ mask ให้เหลือเฉพาะขอบ), ตัวอักษรเรืองแสงเมื่อชี้, เคอร์เซอร์เต้นเบา ๆ ตอนว่าง บนจอสัมผัส แตะแล้วขอบจะสว่างตรงจุดที่แตะแล้วจางลง - **prefers-reduced-motion**: ไม่พิมพ์ ไม่ตามเมาส์ ไม่เต้น แสดงคำสั่ง curl นิ่ง ๆ - **คำสั่งที่แสดงต้องเป็นคำสั่งจริงที่ใช้ได้วันนี้เท่านั้น**: `npm create vite@latest my-app`, `curl -fsSL https://devstack.bid/llms.txt`, `npx create-next-app@latest my-app`, `pip install fastapi` ### เปิดใช้คำสั่ง nvxdev เมื่อจดแพ็กเกจแล้ว คำสั่ง `npx nvxdev init` และ `pip install nvxdev` อยู่ใน `SIGNATURE_COMMAND_CONFIG` ใต้คอมเมนต์ `// MARK: nvxdev` พร้อม `published: false` จึงไม่แสดงบนหน้าเว็บ เพราะแพ็กเกจยังไม่มีบน npm และ PyPI เมื่อจดแพ็กเกจและทดสอบว่าคำสั่งใช้ได้จริงแล้ว: 1. เปลี่ยนเป็น `published: true` (ถ้าอยากให้พิมพ์ก่อน ให้ย้ายขึ้นไปไว้ต้นรายการ) 2. แก้ test ใน `tests/signature.test.tsx` ที่ตรวจว่า nvxdev ยังซ่อนอยู่ ให้ตรงกับรายการใหม่ 3. อัปเดตรายการคำสั่งในหัวข้อนี้ แล้วรัน `npm test` และ `npm run docs:check` ## ข้อตกลงกับเจ้าของผลิตภัณฑ์ การเปลี่ยน **โครงสร้างหรือ layout** ของหน้า (เพิ่ม/ลบ section, ย้ายลำดับ) ต้องได้รับอนุญาตก่อนเสมอ การปรับสไตล์ทำผ่าน token และ class ได้ แต่ต้องรักษา contrast AA และผ่าน test ## ดูเพิ่ม `/design` (เปิดในแอป), [add-component.md](../contributing/add-component.md), [`src/lib/design-tokens.ts`](../../src/lib/design-tokens.ts) --- # ภาพรวมสถาปัตยกรรม · Architecture overview NVX Stack Builder เป็นแอป Next.js 16.4 (App Router, React 19.3, TypeScript, Tailwind v4) ที่ **เกือบทั้งหมดทำงานฝั่ง client** มี endpoint ฝั่งเซิร์ฟเวอร์เพียงตัวเดียวคือ `/api/plan` และ deploy เป็น Cloudflare Worker ผ่าน OpenNext adapter (`@opennextjs/cloudflare`) ตั้งแต่ v0.3.0 ## หลักการออกแบบ 1. **Data-driven** เทมเพลตเป็นข้อมูล การประกอบคำสั่งเป็นฟังก์ชันบริสุทธิ์ (pure function) ทดสอบได้โดยไม่ต้องมีเบราว์เซอร์ 2. **Client-first, stateless server** การประกอบคำสั่ง สร้างสคริปต์ zip และลิงก์แชร์ทำในเบราว์เซอร์ เซิร์ฟเวอร์ไม่มีฐานข้อมูล ไม่มี session ไม่มีบัญชีผู้ใช้ 3. **Text-only commands** ไม่มีการรันคำสั่งที่ใดเลย (ดู [safety-model](../concepts/safety-model.md)) 4. **Graceful degradation** AI ล้มเหลวเมื่อไร ใช้ rule-based planner แทนเสมอ; rate-limit binding ใช้ไม่ได้ ใช้ in-memory แทน 5. **Prerendered pages** ทุกหน้า prerender ตอน build (`cacheComponents: true`) Worker จึงเสิร์ฟ static assets เป็นหลัก ## แผนภาพระบบ ```mermaid flowchart LR subgraph Browser["เบราว์เซอร์ (client)"] UI["Pages & components
src/app, src/components"] Builder["Builder state
BuilderConfig + BuilderEdits"] Composer["composer.ts
placeholders.ts"] Scripts["scripts.ts / download.ts
setup.sh, setup.ps1, zip"] Share["share.ts
#s= token"] LS[("localStorage
nvx-builder-state, nvx-lang, nvx-theme")] end subgraph Worker["Cloudflare Worker: nvx-stack-builder"] Assets["ASSETS binding
prerendered HTML/JS/CSS"] API["/api/plan route.ts"] RL["Rate limit bindings
NVX_PLAN_LIMITER / NVX_PLAN_AI_LIMITER"] Fallback["fallback.ts (rule-based)"] end OpenAI[("OpenAI Responses API")] Data["src/data/templates
(bundled into client + server)"] UI --> Builder --> Composer --> Scripts Builder <--> Share Builder <--> LS Data --> Composer UI -- "GET pages" --> Assets UI -- "POST /api/plan" --> API API --> RL API -- "Structured Outputs" --> OpenAI API --> Fallback Data --> API ``` ## Subsystems | Subsystem | ไฟล์หลัก | หน้าเอกสาร | |---|---|---| | Templates data | `src/data/templates/*`, `src/data/*-steps.ts`, `src/data/template-ids.ts` | [templates-and-composer.md](./templates-and-composer.md) | | Composer + placeholders | `src/lib/composer.ts`, `src/lib/placeholders.ts`, `src/lib/types.ts` | [templates-and-composer.md](./templates-and-composer.md) | | Script generators + downloads | `src/lib/scripts.ts`, `src/lib/download.ts`, `scripts/export-template.ts` | [scripts-and-share.md](./scripts-and-share.md) | | Share encoding | `src/lib/share.ts` | [scripts-and-share.md](./scripts-and-share.md) | | Sanitizer | `src/lib/sanitize.ts` | [safety-model](../concepts/safety-model.md) | | AI route + planner + validation | `src/app/api/plan/route.ts`, `src/lib/ai/*` | [ai-planner.md](./ai-planner.md) | | Rate limiting, env, HTTP limits, security headers | `src/lib/server/*`, `src/lib/rate-limit.ts`, `src/lib/security-headers.ts`, `next.config.ts`, `public/_headers` | [api-hardening.md](./api-hardening.md) | | UI framework | `src/app/tokens.css`, `src/app/globals.css`, `src/components/ui/*`, `src/components/brand/*` | [ui-framework.md](./ui-framework.md) | | Cloudflare / OpenNext runtime | `wrangler.jsonc`, `open-next.config.ts`, `scripts/patch-opennext.mjs`, `cloudflare-env.d.ts` | [cloudflare-runtime.md](./cloudflare-runtime.md) | | Chat data model (D1, schema only) | `migrations/*.sql`, binding `DB` | [chat-data-model.md](./chat-data-model.md) | ## เส้นทาง (routes) | Route | ประเภท | หมายเหตุ | |---|---|---| | `/` | prerendered | หน้าแรก hero + how it works + เทมเพลต + features | | `/templates` | prerendered | catalog (ค้นหา/กรองฝั่ง client) | | `/templates/[id]` | prerendered ต่อ id | id ไม่รู้จัก → rewrite ไป `/__nvx_not_found` เพื่อ HTTP 404 จริง | | `/builder` | shell prerendered, builder render ฝั่ง client | อ่าน URL hash + localStorage จึงต้องเป็น client-only (`builder-loader.tsx`) | | `/design` | prerendered | style guide | | `GET /api/plan` | dynamic | `{ serverKeyConfigured, model }` | | `POST /api/plan` | dynamic | สร้าง AgentPlan | ## ดูเพิ่ม [data-flow.md](./data-flow.md), [file-map.md](../reference/file-map.md), [SUBSYSTEM-HISTORY.md](../history/SUBSYSTEM-HISTORY.md) --- # การไหลของข้อมูล · Data flow หน้านี้ติดตามข้อมูลผ่านระบบในสามสถานการณ์หลัก: เลือกเทมเพลตแล้วส่งออก, ขอแผนจาก AI, และเปิดลิงก์แชร์ เพื่อให้ผู้แก้โค้ดรู้ว่าการเปลี่ยนที่จุดหนึ่งกระทบส่วนไหน ## 1. เลือกเทมเพลต → ปรับ → ส่งออก ```mermaid sequenceDiagram participant U as ผู้ใช้ participant B as Builder (builder.tsx) participant C as composer.ts participant P as placeholders.ts participant S as scripts.ts / download.ts U->>B: เลือกเทมเพลต + ตัวเลือก (Configure) B->>C: composeSteps(config) C->>C: normalizeConfig C->>P: buildContext(config, template) C->>C: runtime + template(when) + add-ons + resolveStep + dedupe C-->>B: ResolvedStep[] (composed) U->>B: ลาก/ลบ/เพิ่ม (Review) B->>C: applyEdits(composed, edits) C-->>B: ลำดับสุดท้าย B->>B: encodeShare -> URL #s= + localStorage U->>B: Export B->>S: toBash / toPowerShell / toReadme / buildStarterZip S-->>U: ไฟล์ดาวน์โหลด (Blob ในเบราว์เซอร์) ``` ข้อสังเกต: `composeSteps` ถูก memo ตาม `config` และ `applyEdits` ตาม `[composed, edits]` การเปลี่ยน config จึงคำนวณใหม่ทั้งหมด แต่การลากสลับคำนวณเฉพาะ `applyEdits` ## 2. ขอแผนจาก AI ```mermaid sequenceDiagram participant A as AgentPanel participant R as POST /api/plan participant L as checkRateLimit participant O as OpenAI Responses API participant F as planFallback participant V as sanitizeAiPlan A->>R: { prompt, lang, mode } (+ x-openai-key) R->>L: NVX_PLAN_LIMITER (ทุก POST) alt เกิน R-->>A: 429 + Retry-After: 60 end R->>R: readJsonBody (≤16KB, JSON) + cleanText(prompt, 2000) alt mode = fallback หรือไม่มี key R->>F: planFallback(prompt, lang, reason) F-->>A: AgentPlan (source: fallback) else มี key opt ใช้ key ของเซิร์ฟเวอร์ R->>L: NVX_PLAN_AI_LIMITER end R->>O: Structured Outputs (strict JSON Schema, store: false) alt สำเร็จ O-->>R: JSON R->>V: zod + sanitizeStep + dedupe กับคำสั่งเทมเพลต V-->>A: AgentPlan (source: openai, warnings) else error / timeout / invalid R->>F: planFallback(reason ที่ redact แล้ว) F-->>A: AgentPlan (source: fallback) end end A->>A: Load into builder -> config + edits.custom (+ custom-ai-files) ``` ทุกเส้นทางยกเว้น 400/413/415/429 ได้ HTTP 200 พร้อม AgentPlan เสมอ client จึงจัดการรูปแบบเดียว ## 3. เปิดลิงก์แชร์ 1. `readInitialState()` อ่าน `#s=` → `decodeShare` (base64url → JSON → zod → `normalizeConfig` → `sanitizeStep` ต่อขั้น) 2. สำเร็จ → stage = Review และแสดง toast; ล้มเหลว → ลอง `#t=` → ลอง localStorage → ค่าเริ่มต้น 3. ระหว่างใช้งาน event `hashchange` ทำให้การวางลิงก์ใหม่ในแถบที่อยู่โหลดสแต็กใหม่ได้ทันที ## ขอบเขตของข้อมูล (data boundaries) | ข้อมูล | อยู่ที่ไหน | ออกจากเบราว์เซอร์ไหม | |---|---|---| | config + edits | URL fragment, localStorage | ไม่ (fragment ไม่ถูกส่งไปเซิร์ฟเวอร์) | | prompt | body ของ POST | ไป Worker และ OpenAI (ถ้าใช้ AI); ไม่ถูก log | | user API key | header `x-openai-key` | ไป Worker → OpenAI ในคำขอนั้นเท่านั้น | | server API key | Worker secret | ไม่ออกจาก Worker ยกเว้นใน Authorization header ไป OpenAI | | ไฟล์ดาวน์โหลด | Blob ในเบราว์เซอร์ | ไม่ | ## ดูเพิ่ม [overview.md](./overview.md), [ai-planner.md](./ai-planner.md), [scripts-and-share.md](./scripts-and-share.md) --- # Subsystem: Templates data + Composer ## หน้าที่ แปลง "สิ่งที่ผู้ใช้เลือก" (`BuilderConfig`) เป็น "ลำดับขั้นตอนที่แทนค่าแล้ว" (`ResolvedStep[]`) นี่คือแกนกลางที่ทุกส่วนใช้ร่วม: หน้า template detail, builder, offline planner, การตรวจผล AI และ CLI export ## โครงสร้างไฟล์ | ไฟล์ | บทบาท | |---|---| | [`src/lib/types.ts`](../../src/lib/types.ts) | ชนิดข้อมูลทั้งหมด: `StackTemplate`, `StepDef`, `ResolvedStep`, `BuilderConfig`, `BuilderEdits`, `ADDON_IDS` | | [`src/data/templates/index.ts`](../../src/data/templates/index.ts) | registry: `TEMPLATES`, `TEMPLATE_IDS`, `getTemplate()` | | `src/data/templates/.ts` | ข้อมูลเทมเพลตแต่ละตัว (10 ไฟล์) | | [`src/data/template-ids.ts`](../../src/data/template-ids.ts) | รายการ id แบบเบา ใช้ใน `next.config.ts` (404 rewrite) โดยไม่ต้อง bundle ข้อมูลเทมเพลต; test ตรวจว่าตรงกับ registry | | [`src/data/common-steps.ts`](../../src/data/common-steps.ts) | ขั้นที่ใช้ร่วม: `mkdirAndCd`, `cdProject`, `npmInit`, `createNextApp`, `createViteApp`, `pmInstall`, `pyVenv`, `pyFreeze` | | [`src/data/runtime-steps.ts`](../../src/data/runtime-steps.ts) | `nvmStep`, `pnpmStep`, `yarnStep`, `pythonCheckStep`, `uvInstallStep`, `uvPythonStep` | | [`src/data/addon-steps.ts`](../../src/data/addon-steps.ts) | `ADDON_LABELS` และ `addonSteps(template, config)`: Tailwind (Vite), Ruff, Vitest, pytest, Docker (Dockerfile ตาม `template.docker.kind`), CI (GitHub Actions ตาม `template.ci`) | | [`src/lib/placeholders.ts`](../../src/lib/placeholders.ts) | `sanitizeProjectName`, `addonOn`, `buildContext`, `resolveText` | | [`src/lib/composer.ts`](../../src/lib/composer.ts) | `DEFAULT_CONFIG`, `configForTemplate`, `normalizeConfig`, `adaptForPowerShell`, `dedupeSteps`, `composeSteps`, `applyEdits`, `moveStep`, `flattenCommands` | ## ขั้นตอนการทำงาน ดูคำอธิบายเชิงแนวคิดใน [composition.md](../concepts/composition.md) สรุปสั้น: `normalizeConfig` → `buildContext` → runtime steps → template steps (กรอง `when`) → add-on steps แทรกก่อนขั้น non-setup แรก → `resolveStep` (แทนค่า + สร้างคำสั่ง Windows) → `dedupeSteps` ## การแปลงคำสั่งเป็น PowerShell ถ้าขั้นไม่มี `windows` override, `adaptForPowerShell` แปลงแบบ best-effort สามกฎ: `curl ` → `curl.exe ` (เลี่ยง alias ของ Invoke-WebRequest), `python3 ` ต้นบรรทัด → `py - `, ` && ` → `; ` คำสั่งที่ซับซ้อนกว่านี้ (activate venv, `cp`, `chmod`, ตัวแปร env) ต้องเขียน `windows` เอง ข้อนี้อยู่ใน checklist ของ [add-template.md](../contributing/add-template.md) ## add-on ทำงานอย่างไร - add-on มีผลเฉพาะเมื่อ **ถูกเลือก และ** อยู่ใน `template.supportedAddons` (`addonOn`) - TypeScript/Tailwind/ESLint ส่วนใหญ่มีผลผ่าน placeholder (`{{nextFlags}}`, `{{viteTemplate}}`, `{{viteLint}}`, `{{ext}}`, `⟨ ⟩`) ไม่ใช่ขั้นแยก - Testing, Docker, CI, Ruff, Tailwind-on-Vite เพิ่มขั้นของตัวเองจาก `addonSteps` - `eslint` บนเทมเพลต Python หมายถึง Ruff; `testing` หมายถึง pytest ## Invariants ที่ test บังคับ (ชื่อ test จริงใน `tests/composer.test.ts`) - เทมเพลต Node เริ่มด้วย nvm และไม่มี placeholder ค้าง (`{{...}}`) — *starts Node templates with nvm and resolves every placeholder* - flags ของ create-next-app และตัวคั่น `--` ของ npm ถูกต้องตามตัวจัดการแพ็กเกจ - syntax เฉพาะ TypeScript (`⟨ ⟩`) ถูกตัดเมื่อเป็น JavaScript; Python ใช้ pip หรือ uv ถูกต้อง - ขั้น add-on ถูกแทรกก่อนขั้น long-running และ add-on ที่ไม่รองรับถูกละเลย - ทุกเทมเพลตได้ลำดับที่ไม่มีขั้นซ้ำ; `applyEdits` คงขั้นใหม่ไว้ในตำแหน่งธรรมชาติ - `KNOWN_TEMPLATE_IDS` ตรงกับ `TEMPLATE_IDS` (`tests/production.test.ts`) - `npm run docs:check` ตรวจว่าทุก template id ถูกระบุในเอกสาร ## จุดขยาย - เพิ่มเทมเพลต: ไฟล์ใหม่ + ลงทะเบียน + เพิ่ม id ใน `template-ids.ts` + เอกสาร ([add-template.md](../contributing/add-template.md)) - เพิ่ม add-on ใหม่: แก้ `ADDON_IDS` (types), `ADDON_LABELS` + `addonSteps`, JSON Schema ของ AI ใช้ `ADDON_IDS` อัตโนมัติ, `ADDON_HINTS` ใน fallback, UI builder, `addonTool()` ใน `tool-icon.tsx` และ test - เพิ่มตัวจัดการแพ็กเกจ: แก้ชนิด `JsPm`/`PyPm`, ตาราง `PM` ใน placeholders, `normalizeConfig`, schema ของ AI ## ประวัติ สร้างใน `a452dc5` (v0.1.0) และคงที่จนถึง v0.5 ยกเว้นการเพิ่ม `template-ids.ts` ใน `b88a5c5` (v0.3.0) ดู [SUBSYSTEM-HISTORY.md](../history/SUBSYSTEM-HISTORY.md) --- # Subsystem: Script generators, downloads & share encoding ## Script generators — [`src/lib/scripts.ts`](../../src/lib/scripts.ts) ฟังก์ชันบริสุทธิ์สามตัวที่รับ `ResolvedStep[]` + `BuilderConfig` + `lang`: - **`toBash`** → `setup.sh` (LF) ส่วนหัว: shebang, คอมเมนต์ template/project, โหลด nvm, `set -eo pipefail`, ฟังก์ชัน `step()` และ `verify()` แต่ละขั้น: หัวคอมเมนต์ `# ─── i/total. title ───` + คำอธิบาย; ขั้นที่ kind อยู่ใน `SKIP_KINDS` (longRunning, publish, manual) ถูกคอมเมนต์ทั้งหมดพร้อม `kindNote`; ไฟล์เขียนด้วย heredoc `<<'NVX_EOF'`; ขั้นที่มี `requires` ห่อด้วย `if command -v` (heredoc ไม่ถูก indent เพราะ terminator ต้องอยู่คอลัมน์ 0) - **`toPowerShell`** → `setup.ps1` (CRLF) ใช้ `$ErrorActionPreference = "Stop"`, `Step`, `Assert-Ok` (ตรวจ `$LASTEXITCODE` หลังทุกคำสั่ง เพราะ native command ไม่ throw เอง), `Write-NvxFile` (here-string `@' '@` + `[IO.File]::WriteAllText`), `Verify { }` - **`toReadme`** → Markdown ไทยหรืออังกฤษ: quick start, คำเตือนให้ตรวจสคริปต์, สรุป runtime/add-on, share link, ทุกขั้น (คำสั่ง, ไฟล์, OS notes, expected, verify) และ next steps การ quote: `shSingleQuote` (`'` → `'\''`) และ `psSingleQuote` (`'` → `''`) ใช้กับข้อความที่ไม่ใช่คำสั่ง (ชื่อขั้น, path, verify) เพื่อไม่ให้ชื่อขั้นที่มีอักขระพิเศษกลายเป็นโค้ด **ทำไมสคริปต์ไม่รันขั้น longRunning:** dev server ไม่จบเอง สคริปต์จะค้างและขั้นหลังจากนั้นไม่ถูกรัน การคอมเมนต์ไว้พร้อมข้อความ "run it manually in a separate terminal" ชัดเจนกว่า ## Downloads — [`src/lib/download.ts`](../../src/lib/download.ts) - `downloadBlob` / `downloadText` สร้าง object URL แล้วคลิกลิงก์ชั่วคราว (revoke หลัง 1 วินาที) - `shareUrlFor(config, edits)` = `${origin}/builder#s=` - `buildStarterZip` ใช้ JSZip สร้างโฟลเดอร์ `-starter/` มี `setup.sh` (unix mode 755), `setup.ps1`, `README.md`, `nvx-stack.json` (generator, shareUrl, config, steps แบบย่อ) และ `starter-files/` ทุกไฟล์ จากนั้น `generateAsync({ platform: "UNIX" })` เพื่อให้สิทธิ์ไฟล์ติดไปด้วย ## CLI export — [`scripts/export-template.ts`](../../scripts/export-template.ts) `npm run export -- [--name] [--pm] [--py] [--addons a,b] [--out] [--lang]` เขียน `setup.sh`, `setup.ps1`, `README.md` ลงดิสก์ ใช้ทดสอบเทมเพลตแบบ end-to-end บนโฟลเดอร์สะอาด ## Share encoding — [`src/lib/share.ts`](../../src/lib/share.ts) ```ts payload = { v: 1, c: BuilderConfig, o?: string[], r?: string[], x?: ResolvedStep[] } token = base64url(UTF-8(JSON.stringify(payload))) // ไม่มี padding "=" ``` - ใช้ `TextEncoder`/`TextDecoder` เพื่อรองรับภาษาไทยใน custom step (btoa ตรง ๆ รับเฉพาะ Latin-1) - `decodeShare` ปฏิเสธ token ว่างหรือยาวเกิน 60,000, ตรวจ zod (`o`/`r` ≤ 100 รายการ สมาชิก ≤ 80 ตัวอักษร, `x` ≤ 20), `normalizeConfig(c)`, `sanitizeStep` ทุกขั้นใน `x` (id ใหม่ `custom-share-n`) และคืน `null` เมื่อมีข้อผิดพลาดใด ๆ - เหตุผลของ fragment: ไม่ถูกส่งไปเซิร์ฟเวอร์ ไม่ต้องมี storage ไม่มีข้อมูลผู้ใช้ใน log (ดู [user-guide/sharing.md](../user-guide/sharing.md)) ## Tests `tests/scripts-share-ai.test.ts` ครอบคลุม: script generation (bash มี heredoc และขั้น long-running ถูกคอมเมนต์, PowerShell ใช้คำสั่ง Windows, ขั้น nvm ถูกห่อด้วยการตรวจว่ามีคำสั่ง, README ภาษาไทย), share round-trip และการปฏิเสธ token เสีย, sanitizer (บล็อกคำสั่งทำลายและทำเครื่องหมายคำสั่งเสี่ยง), fallback planner และ AI plan sanitising ## ข้อควรระวังเมื่อแก้ - ถ้าเปลี่ยนรูปแบบ payload ให้เพิ่ม `v: 2` และคงการถอด `v: 1` เพื่อไม่ให้ลิงก์เก่าเสีย - ถ้าเปลี่ยน heredoc marker ต้องแก้ `cleanFile` ใน sanitizer ให้ตรงกัน - ทดสอบสคริปต์จริงด้วย `npm run export` แล้ว `bash -n setup.sh` (syntax check) อย่างน้อย ## ประวัติ ทั้งหมดสร้างใน `a452dc5` (v0.1.0) ปุ่มดาวน์โหลดมี toast ตั้งแต่ `853feac` (v0.2.0) และไอคอน lucide ตั้งแต่ `382750c` (v0.5) ตรรกะการสร้างไฟล์ไม่เปลี่ยน --- # Subsystem: AI route, planner & validation ## ส่วนประกอบ | ไฟล์ | บทบาท | |---|---| | [`src/app/api/plan/route.ts`](../../src/app/api/plan/route.ts) | `GET` (สถานะ key + model) และ `POST` (สร้างแผน) พร้อม rate limit, body limit, timeout, log แบบมีโครงสร้าง | | [`src/lib/ai/openai.ts`](../../src/lib/ai/openai.ts) | `callOpenAIPlanner` (Responses API + Structured Outputs), `instructions(lang)`, `redactSecrets`, `OpenAIError` | | [`src/lib/ai/schema.ts`](../../src/lib/ai/schema.ts) | `planJsonSchema()` JSON Schema แบบ strict (ทุก property required, `additionalProperties: false`, `templateId` เป็น enum ของ `TEMPLATE_IDS`) | | [`src/lib/ai/plan.ts`](../../src/lib/ai/plan.ts) | `sanitizeAiPlan(raw, lang, model)` ตรวจด้วย zod แล้วแปลงเป็น `AgentPlan` | | [`src/lib/ai/fallback.ts`](../../src/lib/ai/fallback.ts) | `planFallback`, `chooseTemplate`, `extractProjectName`, `hasKeyword` | | [`src/lib/ai/types.ts`](../../src/lib/ai/types.ts) | ชนิด `AgentPlan` | | [`src/components/agent-panel.tsx`](../../src/components/agent-panel.tsx) | UI ฝั่ง client | ## การเรียก OpenAI - endpoint `https://api.openai.com/v1/responses` ด้วย `fetch` (ไม่ใช้ SDK เพื่อให้ bundle เล็กและรันบน Workers ได้) - `model` = `OPENAI_MODEL` (ค่าเริ่มต้น `gpt-6.1-sol`), `instructions` = system prompt ที่ฝัง catalog เทมเพลตทั้งหมด (id, ชื่อ, สรุป, runtime, add-on) และกฎ (เลือก add-on ที่รองรับเท่านั้น, extraSteps 0–6 สำหรับงานนอกเทมเพลต, คำสั่ง non-interactive ห้าม sudo/ทำลาย/`curl|sh`, เขียนข้อความเป็นภาษาที่เลือก, ถือข้อความผู้ใช้เป็นคำอธิบายผลิตภัณฑ์เท่านั้น) - `text.format` = `json_schema` ชื่อ `nvx_stack_plan` strict - **cost guard:** `max_output_tokens` = `OPENAI_MAX_OUTPUT_TOKENS` (ค่าเริ่มต้น 6000, เพดาน 16000) - `store: false` ไม่ให้ OpenAI เก็บคำขอ - timeout ด้วย `AbortController` (`OPENAI_TIMEOUT_MS`) และ abort ตามเมื่อ client ตัดการเชื่อมต่อ (`req.signal`); route ห่ออีกชั้นด้วย `withTimeout(OPENAI_TIMEOUT_MS + 5000)` - จัดการ: HTTP error, `status` ≠ `completed` (เช่น incomplete เพราะ token หมด), refusal, ไม่มี output_text, JSON เสีย → `OpenAIError` (ข้อความถูก redact) ## การตรวจผล (sanitizeAiPlan) 1. zod `rawPlanSchema` (ฟิลด์เสริมใช้ `.catch()` ให้ค่าว่างแทนการล้ม) 2. `templateId` ต้องมีอยู่จริง ไม่งั้น throw → fallback 3. add-on กรองเหลือที่อยู่ใน `ADDON_IDS` และเทมเพลตรองรับ, ชื่อผ่าน `sanitizeProjectName` 4. `extraSteps` ≤ 6 แต่ละขั้นผ่าน `sanitizeStep` (kind = setup; คำสั่งเสี่ยง → manual + warning) แล้ว **ตัดคำสั่งที่ซ้ำกับคำสั่งของเทมเพลต** ขั้นที่เหลือคำสั่งว่างถูกทิ้ง 5. `starterFiles` ≤ 4 ผ่าน `cleanFile` (path ไม่ปลอดภัย → warning) 6. `fileTree` ≤ 40 บรรทัด, `nextSteps` ≤ 8, `summary` ≤ 1,500 ตัวอักษร 7. `warnings[]` เป็นภาษาตาม `lang` และแสดงเป็น "Safety notes" ## Rule-based fallback ให้คะแนนแต่ละเทมเพลต: keyword ตรง (คำเดี่ยว +2, วลี +3), id ปรากฏในข้อความ +3, ต้องการ Python และเทมเพลตเป็น Python +2, ต้องการ Node +1, ต้องการ Python ล้วนแต่เทมเพลตไม่ใช่ Python −2 ถ้าคะแนนสูงสุด ≤ 0 เลือก `fastapi-ai` (ถ้าพูดถึง Python) หรือ `nextjs-dashboard` keyword ภาษาอังกฤษจับที่ขอบคำ ("ai" ไม่ match "email") ส่วนภาษาไทยใช้ substring เพราะภาษาไทยไม่เว้นวรรคระหว่างคำ add-on จาก `ADDON_HINTS`, ตัด TypeScript เมื่อเจอ "no typescript"/"ไม่ใช้ typescript", เลือก pnpm/yarn/uv จากคำที่พบ ## ทำไมออกแบบเช่นนี้ - **AI เลือก ไม่ใช่ AI เขียนทุกอย่าง** คำสั่งหลักมาจากเทมเพลตที่ทดสอบแล้ว AI เติมเฉพาะส่วนต่าง → ผลลัพธ์น่าเชื่อถือและสอดคล้องกับ UI - **Fallback เสมอ** แอปใช้งานได้ 100% โดยไม่มี key และ AI ล่มไม่ทำให้ผู้ใช้ติด - **HTTP 200 + `source`** แทนการส่ง 5xx ทำให้ client มีเส้นทางเดียว และ `fallbackReason` บอกสาเหตุอย่างโปร่งใส ## การเปลี่ยนโมเดลหรือ prompt - เปลี่ยนโมเดล: แก้ `vars.OPENAI_MODEL` ใน [`wrangler.jsonc`](../../wrangler.jsonc) (production) หรือ env (local) ค่าต้องตรง regex `^[\w.:-]{1,80}$` ค่าเริ่มต้นในโค้ดคือ `DEFAULT_MODEL` ใน [`src/lib/server/env.ts`](../../src/lib/server/env.ts) - เปลี่ยน prompt: แก้ `instructions()` ใน `openai.ts`; ถ้าเปลี่ยนรูปแบบผลลัพธ์ต้องแก้ทั้ง `schema.ts` และ `rawPlanSchema` ใน `plan.ts` ให้ตรงกัน แล้วเพิ่ม test ใน `tests/scripts-share-ai.test.ts` - ดูสูตรใน [AGENT-PLAYBOOK.md](../agents/AGENT-PLAYBOOK.md) ## ประวัติ สร้างใน `a452dc5` (v0.1.0: Responses API, Structured Outputs, user key, zod, sanitizer, in-memory rate limit, fallback) → hardening ใน `97f14d9` (v0.3.0: env validation, distributed rate limit, body/timeout limits, redacted logs, cost guard) → IPv6 /64 ใน `712eb8e` --- # Subsystem: API hardening — rate limiting, env, HTTP limits, security headers ชุดการป้องกันฝั่งเซิร์ฟเวอร์ที่เพิ่มใน v0.3.0 เพื่อเตรียม production (commit `97f14d9`, `b88a5c5`, `712eb8e`) เป้าหมายคือปกป้อง **บิล OpenAI ของผู้ดูแล**, ปกป้อง **ผู้ใช้** (headers, ไม่ log ข้อมูลส่วนตัว) และทำให้ระบบ **ไม่ล่มเพราะ config ผิด** ## Env validation — [`src/lib/server/env.ts`](../../src/lib/server/env.ts) - อ่านค่าด้วย zod ครั้งเดียวต่อ isolate (cache จนกว่าค่าต้นทางจะเปลี่ยน) - ค่าที่ไม่ถูกต้อง **ไม่ทำให้ crash** แต่ใช้ค่าเริ่มต้นแทน และ log เฉพาะ *ชื่อ* ตัวแปร (`[env] ignoring invalid values for: ...`) ไม่ log ค่า - `OPENAI_API_KEY` ต้องตรง `^[\w-]{20,400}$` ไม่งั้นถือว่าไม่มี key - ตัวแปรทั้งหมด: `OPENAI_API_KEY`, `OPENAI_MODEL`, `OPENAI_MAX_OUTPUT_TOKENS` (512–16000, ค่าเริ่มต้น 6000), `OPENAI_TIMEOUT_MS` (5000–120000, 60000), `PLAN_AI_LIMIT_PER_MIN` (1–600, 5), `PLAN_LIMIT_PER_MIN` (1–6000, 20) ตารางเต็มที่ [config-env.md](../reference/config-env.md) ## HTTP limits — [`src/lib/server/http.ts`](../../src/lib/server/http.ts) - `readJsonBody(req, 16 KB)`: Content-Type ต้องเป็น `application/json` (ไม่งั้น **415**), ปฏิเสธเร็วจาก `Content-Length` และ **อ่านแบบ stream แล้วยกเลิกทันทีเมื่อเกิน** (**413**) เพราะ `Content-Length` อาจโกหกหรือไม่มี, JSON เสีย → **400** - `withTimeout` → `HttpError(504)` ซึ่ง route แปลงเป็น fallback plan - `clientIp`: `cf-connecting-ip` → `x-forwarded-for` (ตัวแรก) → `x-real-ip` → `"local"`; IPv6 ถูกย่อเป็น **/64** (`ipv6Prefix64`) เพราะผู้ใช้หนึ่งรายมักคุมทั้ง /64 และหมุนที่อยู่หลบ limit ได้ (พบจริงตอนทดสอบ live ผ่าน WARP ที่ IPv6 เปลี่ยนทุก connection — commit `712eb8e`) - `NO_STORE` header `Cache-Control: no-store` ในทุกคำตอบของ API ## Rate limiting — [`src/lib/server/rate-limit.ts`](../../src/lib/server/rate-limit.ts) | Limiter | ใช้กับ | ค่าใน wrangler.jsonc | ค่า fallback (memory) | |---|---|---|---| | `NVX_PLAN_LIMITER` | ทุก POST `/api/plan` (รวม offline และ user key) | 20 ครั้ง / 60 วินาที / IP (namespace `4712`) | `PLAN_LIMIT_PER_MIN` = 20 | | `NVX_PLAN_AI_LIMITER` | เฉพาะคำขอที่ใช้ key ของเซิร์ฟเวอร์ | 5 ครั้ง / 60 วินาที / IP (namespace `4711`) | `PLAN_AI_LIMIT_PER_MIN` = 5 | บน Workers ใช้ **Workers Rate Limiting API binding** (นับร่วมกันทุก isolate ใน location เดียว อยู่รอดข้าม restart) นอก Workers (`next dev`, tests) หรือเมื่อ binding error ใช้ in-memory sliding window ใน [`src/lib/rate-limit.ts`](../../src/lib/rate-limit.ts) (v0.1.0) เกินแล้วได้ **429** + `Retry-After: 60` และข้อความสองภาษา หมายเหตุ: ตัวนับของ Cloudflare เป็นต่อ location และ eventually consistent burst เร็วมากอาจผ่านเกินได้เล็กน้อย ซึ่งยอมรับได้สำหรับการกันการใช้ในทางที่ผิด ## Logging `log()` ใน route พิมพ์ JSON บรรทัดเดียว `{ at: "api/plan", event, ray, ... }` เหตุการณ์: `rate_limited` (limiter, backend), `body_read_failed`, `ai_fallback` (reason ที่ redact แล้ว, status, keySource: user/server) **ไม่เคย log prompt, key หรือ header** `redactSecrets` แทน key ที่รู้จัก, `sk-...`, `Bearer ...`, `cfat_/cfut_...` ด้วย `***` ## Security headers — [`src/lib/security-headers.ts`](../../src/lib/security-headers.ts) ใส่ทุก route ผ่าน `headers()` ใน [`next.config.ts`](../../next.config.ts) และ mirror ให้ static assets ใน [`public/_headers`](../../public/_headers): - **CSP**: `default-src 'self'`; `script-src 'self' 'unsafe-inline'` (+`'unsafe-eval'` เฉพาะ dev); `connect-src 'self'`; `object-src 'none'`; `frame-ancestors 'none'`; `base-uri 'self'`; `form-action 'self'` — ต้องยอม `'unsafe-inline'` เพราะหน้าถูก prerender (ใส่ nonce ต่อคำขอไม่ได้) และมีสคริปต์ pre-paint ของธีม - `Strict-Transport-Security: max-age=31536000` (production เท่านั้น ไม่มี `includeSubDomains`/`preload` ตั้งแต่ v0.5.1 เพราะแอปอยู่บน apex ของ zone devstack.bid ดู [custom-domain.md](../operations/custom-domain.md)) - `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` (ปิดกล้อง ไมค์ ตำแหน่ง ฯลฯ), `Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Resource-Policy: same-origin` - `poweredByHeader: false` - `/_next/static/*` ได้ `Cache-Control: public, max-age=31536000, immutable` ## Real 404 ด้วย Cache Components + Partial Prefetching, `notFound()` ใน `/templates/[id]` ถูก stream หลัง static shell จึงได้ status 200 ("soft 404") แก้ด้วย `beforeFiles` rewrite ใน `next.config.ts` ส่ง id ที่ไม่อยู่ใน `KNOWN_TEMPLATE_IDS` ไป `/__nvx_not_found` ซึ่งไม่มี route ตรง จึงได้ **HTTP 404** จริงโดยไม่ต้องใช้ middleware ## Tests `tests/production.test.ts`: serverEnv, readJsonBody, withTimeout, redactSecrets, security headers, template id list, POST hardening (415/413/400, 429 + Retry-After, AI limiter ไม่ใช้กับ user key), clientIp (/64) --- # Subsystem: UI framework (NVX UI) เอกสารเทคนิคเต็มของระบบดีไซน์อยู่ใน [UI-FRAMEWORK.md](../UI-FRAMEWORK.md) (TH + EN) หน้านี้สรุปโครงสร้างในมุมสถาปัตยกรรม และวิธีที่ชิ้นส่วนเชื่อมกัน แนวคิดดู [concepts/design-system.md](../concepts/design-system.md) ## ชั้นของระบบ ```mermaid flowchart TB T["tokens.css
--nvx-* (:root = light, .dark = default)"] --> G["globals.css
@theme inline → Tailwind utilities
+ utilities: link, focus-ring, motion-*, hero-glow, tool-tile, nav-glass, term-cursor"] G --> P["src/components/ui/*
Button, Card, Badge, Tabs, Dialog, Toast, CodeBlock, Stepper ..."] B["src/components/brand/*
ToolIcon, ToolTile (simple-icons data)"] --> F L["lucide-react
UI icons"] --> F P --> F["Feature components
header, catalog, template-card/detail, builder, agent-panel, command-palette"] F --> R["Routes: src/app/*"] M["src/lib/design-tokens.ts
(manifest)"] --> D["/design style guide
design-system.tsx"] M --> X["tests/tokens.test.ts
(WCAG AA)"] ``` ## ไฟล์สำคัญ | ไฟล์ | หน้าที่ | |---|---| | [`src/app/tokens.css`](../../src/app/tokens.css) | ค่าสีทั้งหมด (`--nvx-bg`, `--nvx-primary`, `--nvx-link`, code-*, chrome-*, tile-*, glow-*), เงา, motion; `.dark` override; reduced motion ตั้ง duration เป็น 0.01ms | | [`src/app/globals.css`](../../src/app/globals.css) | map token → Tailwind (`--color-*`), ฟอนต์ (`--font-sans` = Inter → Plex Sans Thai, `--font-mono` = Plex Mono), radius, keyframes, utilities | | [`src/app/layout.tsx`](../../src/app/layout.tsx) | โหลดฟอนต์ด้วย `next/font`, ``, สคริปต์ pre-paint (ธีม + ภาษา), providers | | [`src/components/prefs.tsx`](../../src/components/prefs.tsx) | `useLang`, ธีม/ภาษาผ่าน `useSyncExternalStore` (SSR snapshot: dark) | | [`src/components/ui/index.ts`](../../src/components/ui/index.ts) | barrel export ของคอมโพเนนต์ทั้งหมด | | [`src/components/ui/button-classes.ts`](../../src/components/ui/button-classes.ts) | `buttonClasses()` variant × size (ใช้ร่วมกับ Link ผ่าน `asChild`) | | [`src/components/brand/tool-icon.tsx`](../../src/components/brand/tool-icon.tsx) | `ToolIcon`, `ToolTile`, `templateTool`, `tagTool`, `commandTool`, `addonTool` | | [`src/components/brand/brand-icons.data.ts`](../../src/components/brand/brand-icons.data.ts) | path SVG 31 โลโก้ สร้างโดย [`scripts/gen-brand-icons.mjs`](../../scripts/gen-brand-icons.mjs) | | [`src/components/brand/mascot.tsx`](../../src/components/brand/mascot.tsx) | มาสคอต `Mascot`: ภาพตัว + ตา SVG, ตามองตามเมาส์, squish; CSS อยู่ใน `globals.css` (`.nvx-mascot*`) | | [`scripts/gen-brand-assets.py`](../../scripts/gen-brand-assets.py) | สร้างโลโก้ทุกขนาด, favicon, app icon, OG image และภาพตัวมาสคอตที่ลบตาออก จาก [`assets/brand/nvx-logo-master.png`](../../assets/brand/nvx-logo-master.png) | | [`src/lib/design-tokens.ts`](../../src/lib/design-tokens.ts) | manifest ของ token (ชื่อ คำอธิบายสองภาษา) สำหรับ `/design` และ test | | [`src/components/design-system.tsx`](../../src/components/design-system.tsx) | style guide `/design` | ## การตัดสินใจเชิงสถาปัตยกรรม - **Radix UI สำหรับส่วนที่โต้ตอบ** (dialog, tabs, tooltip, toast, switch, checkbox, slot) ได้ accessibility ครบโดยไม่ต้องเขียนเอง (`f673199`, `4ac2756`) - **cmdk** สำหรับ command palette, **dnd-kit** สำหรับลากจัดลำดับพร้อมคีย์บอร์ดและประกาศสำหรับ screen reader สองภาษา - **ธีมผ่าน class `.dark`** ที่เซิร์ฟเวอร์เรนเดอร์ไว้ก่อน → ไม่มี flash, ไม่ต้องรอ JS - **โลโก้แบบ generate** แทนการ import simple-icons ทั้งแพ็กเกจ (ใหญ่มาก) เพื่อให้ Worker bundle เล็ก simple-icons เป็น devDependency เท่านั้น - **มาสคอตแบบเลเยอร์** (ภาพตัว + ตา SVG) แทน Lottie หรือไลบรารีแอนิเมชัน: ไม่เพิ่ม dependency ใช้แค่ transform จึงไม่เกิด layout/paint ซ้ำ ภาพตัวขนาด 2–22 KB และตาวาดด้วยพิกัดเดียวกับสคริปต์ที่ลบตาออก (test ตรวจว่าตรงกัน) - **ไม่มีอีโมจิ** ใน `src/` (test บังคับ) เพราะแสดงผลไม่เหมือนกันในแต่ละ OS และดูไม่เป็นเครื่องมือมืออาชีพ ## Accessibility invariants - คู่สีข้อความผ่าน WCAG AA (4.5:1) ทั้งสองธีม, non-text (ปุ่ม primary, ring) ผ่าน 3:1 — 32 คู่ใน `tests/tokens.test.ts` - ปุ่มไอคอนอย่างเดียวต้องมี `aria-label`; ไอคอนตกแต่งต้อง `aria-hidden` - โฟกัสมองเห็นได้เสมอ (`focus-ring`), toast ประกาศแบบ polite (error แบบ assertive) - แอนิเมชันมาสคอตหยุดทั้งหมดเมื่อ `prefers-reduced-motion: reduce` และเป็น `aria-hidden` เว้นแต่ส่ง `label` - ทุกหน้าไม่มี horizontal scroll ที่ 390px ทั้งไทยและอังกฤษ (ตรวจด้วย Playwright ทุกครั้งที่ปรับดีไซน์) ## ประวัติ v0.2.0 สร้างทั้งระบบ (`3e5a8c2` tokens, `4ac2756` components, `7506507` app shell, `1b0f105` /design, `cd427e6` tests) → v0.4 terminal (`2210920`, `1219529`) → v0.5 Apple-inspired (`3dad906`, `b6e61a5`, `382750c`) รายละเอียดใน [SUBSYSTEM-HISTORY.md](../history/SUBSYSTEM-HISTORY.md) --- # Subsystem: Cloudflare Workers / OpenNext runtime ตั้งแต่ v0.3.0 (commit `f9ef054`) แอปถูก build เป็น Cloudflare Worker ด้วย [`@opennextjs/cloudflare`](https://opennext.js.org/cloudflare) 1.20.x และ Wrangler 4 คู่มือ deploy ทีละขั้นอยู่ที่ [DEPLOY.md](../DEPLOY.md) และ [operations/deploy-cloudflare.md](../operations/deploy-cloudflare.md) หน้านี้อธิบายว่า runtime ประกอบกันอย่างไรและทำไม ## ทำไม Cloudflare Workers + OpenNext - แอปเกือบทั้งหมดเป็นหน้า prerendered + API เล็ก ๆ หนึ่งตัว เหมาะกับ edge runtime ที่เสิร์ฟ static ได้เร็วและคิดเงินตามคำขอ - Workers มี **Rate Limiting API** ในตัว (ไม่ต้องสร้าง KV/Redis) ใช้ปกป้อง `/api/plan` - OpenNext แปลงผล build ของ Next.js เป็น Worker ที่รองรับ App Router ได้โดยไม่ต้องเขียน adapter เอง - ไม่ผูกกับ Cloudflare: `npm run build && npm start` ยังรันบน Node ได้ (rate limit ใช้ in-memory) ## ไฟล์ config | ไฟล์ | สาระสำคัญ | |---|---| | [`wrangler.jsonc`](../../wrangler.jsonc) | `name: nvx-stack-builder`, `main: .open-next/worker.js`, `compatibility_flags: [nodejs_compat, global_fetch_strictly_public]`, `workers_dev: true`, `preview_urls: false`, **ไม่มี `routes`**, `assets` (binding `ASSETS`), service self-reference `WORKER_SELF_REFERENCE`, `vars` (OPENAI_MODEL, OPENAI_MAX_OUTPUT_TOKENS, OPENAI_TIMEOUT_MS), `ratelimits` (สอง limiter), `observability.enabled`, `upload_source_maps` | | [`open-next.config.ts`](../../open-next.config.ts) | `incrementalCache: staticAssetsIncrementalCache` เพราะทุกหน้า prerender และไม่มี revalidate ตอน runtime จึงไม่ต้องใช้ R2/KV | | [`cloudflare-env.d.ts`](../../cloudflare-env.d.ts) | ชนิดของ binding (สร้างด้วย `npm run cf:typegen`) | | [`.dev.vars.example`](../../.dev.vars.example) | ต้นแบบ `.dev.vars` สำหรับ `npm run preview` (ห้าม commit `.dev.vars`) | | [`public/_headers`](../../public/_headers) | headers ของ static assets ที่เสิร์ฟตรงจาก `.open-next/assets` | | [`scripts/patch-opennext.mjs`](../../scripts/patch-opennext.mjs) | patch ตอน `postinstall` ให้ adapter inline `preview-props.json` ของ Next 16.4 (ไม่งั้นทุกคำขอ error `Unexpected loadManifest(...preview-props.json)`) idempotent และเตือนถ้า pattern เปลี่ยน ลบได้เมื่อ adapter แก้เอง | | [`next.config.ts`](../../next.config.ts) | headers, 404 rewrite, และ `initOpenNextCloudflareForDev()` ใน dev เพื่อให้ `next dev` เห็น binding | ## การทำงานตอนรัน 1. คำขอเข้าที่ Worker → ถ้าเป็นไฟล์ใน `.open-next/assets` (เช่น `/_next/static/*`, HTML ที่ prerender) เสิร์ฟผ่าน `ASSETS` 2. ไม่ใช่ → handler ของ Next (OpenNext) ซึ่งรวมถึง `/api/plan` 3. `/api/plan` เรียก `getCloudflareContext().env` เพื่อหา rate-limit binding (dynamic import จึงไม่พังนอก Workers) 4. secret `OPENAI_API_KEY` และ `vars` เข้าถึงผ่าน `process.env` (ด้วย `nodejs_compat`) แล้วตรวจด้วย `serverEnv()` ## ข้อจำกัดและข้อควรรู้ - Wrangler 4 ต้องใช้ **Node 22+** - build ใช้เวลาประมาณ 1 นาที (`npx opennextjs-cloudflare build`) ผลลัพธ์อยู่ใน `.open-next/` (gitignored) - `npm run preview` รันใน workerd จริงที่ `http://localhost:8787` ใช้ตรวจภาพและพฤติกรรมให้ตรงกับ production มากกว่า `next dev` - rate-limit counters เป็นต่อ Cloudflare location - **ห้าม** เพิ่ม `routes` หรือ custom domain จนกว่าจะได้รับอนุมัติตาม [custom-domain.md](../operations/custom-domain.md) ## D1 (ฐานข้อมูล) Worker มี binding `DB` ชี้ D1 `nvx-db` (ประกาศใน `d1_databases` ของ `wrangler.jsonc` พร้อม `migrations_dir`) ตอนนี้ยังไม่มีโค้ดเรียกใช้ D1 ดู schema ที่ [chat-data-model.md](./chat-data-model.md) และวิธีดูแลที่ [d1-database.md](../operations/d1-database.md) ส่วน R2 ยังไม่ได้เปิดใช้ ## สถานะ deploy ล่าสุด Production (v0.6.0) ที่ `https://devstack.bid` และ `www.devstack.bid` (Workers Custom Domains, redirect www → apex) บัญชี `2d92bd5b25768fa9093d6adc0a8887fc` เวอร์ชันปัจจุบัน `7ddd9404-a31f-4e31-8460-82b3b90c7d0e` (ก่อนหน้า `671b6b3b-1074-40cb-84b0-0df2681b69ce` v0.5.1) workers.dev สำรอง `https://nvx-stack-builder.examplessdk.workers.dev` ส่วน deploy เดิม (v0.3.0) ที่ `https://nvx-stack-builder.agen-sdk-work.workers.dev` บัญชี `f70d35188a3c56b9781538c73a86e04e` ยังเปิดอยู่และไม่ถูกแก้ไข --- # ระบบเอกสารในแอป · Docs site (`/docs`) หน้านี้เล่าว่าหน้า [devstack.bid/docs](https://devstack.bid/docs) ทำงานอย่างไร เราตั้งโจทย์ไว้สามข้อ: (1) เนื้อหามีแหล่งเดียวคือไฟล์ Markdown ใน `docs/` ไม่มีการเขียนซ้ำ (2) ทำงานบน Cloudflare Workers ซึ่ง **ไม่มี filesystem ตอน runtime** และ (3) ทั้งคนและ AI agent หยิบ Markdown ต้นฉบับไปใช้ได้จริง ไม่ใช่แค่อ่านบนเว็บ ## ภาพรวม pipeline ```mermaid flowchart LR MD["docs/**/*.md
(แหล่งความจริงเดียว)"] --> B["scripts/build-docs.mjs
(prebuild / predev / pretest)"] B --> J["src/generated/docs-bundle.json
raw + HTML + TOC"] B --> N["src/generated/docs-nav.json
หมวดและชื่อหน้า"] B --> S["public/docs-search-index.json
ข้อความรายหัวข้อ"] D["npm run docs:diagrams
(Playwright + mermaid)"] --> SVG["public/docs-assets/diagrams/*.svg"] SVG --> B J --> P["/docs, /docs/[...slug]
prerender ตอน build"] J --> R["/docs/raw/[...slug]
/docs/<slug>.md"] J --> L["/llms.txt, /llms-full.txt"] N --> P N --> C["command palette"] S --> Q["ช่องค้นหา (client)"] ``` ## ขั้นตอน build `scripts/build-docs.mjs` รันอัตโนมัติก่อน `npm run build` (ซึ่ง `npm run cf:build` เรียก), `npm run dev`, `npm test` และ `npm run typecheck` ไฟล์ที่สร้างทั้งหมดอยู่ใน `.gitignore` จึงไม่มีวันเก่ากว่า `docs/` 1. **Parse** ด้วย remark-parse + remark-gfm (ตาราง, task list, strikethrough) 2. **แปลงเป็น HTML** ด้วย remark-rehype โดย **ทิ้ง raw HTML** ในไฟล์ Markdown 3. **Sanitize** ด้วย rehype-sanitize (schema แบบ GitHub) ลิงก์ `javascript:` และแท็กอันตรายถูกตัดทิ้ง 4. หลัง sanitize จึงแปลงต่อเฉพาะสิ่งที่เราควบคุมเอง: ใส่ `id` ให้หัวข้อ (rehype-slug ใช้กฎเดียวกับ GitHub จึงรองรับหัวข้อภาษาไทย), ปุ่ม `#` ข้างหัวข้อ, highlight โค้ดด้วย lowlight, แถบบนบล็อกโค้ดพร้อมปุ่มคัดลอก, ห่อตารางให้เลื่อนแนวนอนได้บนมือถือ 5. **ลิงก์** ระหว่างไฟล์ เช่น `../reference/api-plan.md` ถูกแปลงเป็น `/docs/reference/api-plan` ส่วนลิงก์ไปไฟล์โค้ด (เช่น `src/lib/composer.ts`) จะเป็นลิงก์ GitHub เมื่อตั้ง `NEXT_PUBLIC_REPO_URL` ไว้ ถ้าไม่ตั้งจะแสดงเป็นข้อความธรรมดา 6. **Mermaid** ถูกแทนด้วยไฟล์ SVG ที่เรนเดอร์ไว้ล่วงหน้า (ชื่อไฟล์คือ hash ของ source) หน้าเว็บจึงไม่ต้องโหลด JavaScript ของ Mermaid และไม่ต้องแก้ CSP ถ้าแก้ diagram แล้วยังไม่รัน `npm run docs:diagrams` บล็อกนั้นจะแสดงเป็นโค้ดแทน build ไม่พัง ## Route ที่ได้ | URL | ได้อะไร | |---|---| | `/docs` | หน้าแรกเอกสาร (จาก `docs/README.md`) | | `/docs/` | หน้าเอกสาร เช่น `/docs/user-guide/getting-started` (slug = path ตัวพิมพ์เล็ก ไม่มี `.md`) | | `/docs/.md`, `/docs/index.md` | Markdown ต้นฉบับแบบไบต์ต่อไบต์ `Content-Type: text/markdown; charset=utf-8` | | `/docs/raw/` | ปลายทางจริงของไฟล์ดิบ (rewrite จาก `.md` ใน `next.config.ts`) | | `/docs/platform` | สารบัญฟังก์ชันของแพลตฟอร์ม (จาก `docs/platform.md`) | | `/docs/agents/cursor-caretakers` | บอท NVX DEVELOPER BOTH ที่สร้างและดูแลทั้งแพลตฟอร์ม | | `/docs/agents/platform-manifest.json` | แผนที่แพลตฟอร์มแบบ JSON | | `/docs/agents/cursor-agents.json` | roster ของ caretaker แบบ JSON | | `/llms.txt` | สารบัญลิงก์ไฟล์ดิบทุกหน้าตามรูปแบบ llmstxt.org | | `/llms-full.txt` | ทุกหน้ารวมเป็นไฟล์เดียว | slug ที่ไม่มีอยู่ได้ **HTTP 404 จริง** ผ่าน rewrite แบบเดียวกับ `/templates/` (เพราะ `notFound()` ใน Cache Components จะ stream หลัง shell ที่เป็น 200) ไฟล์ดิบและ `llms*.txt` ส่ง `Access-Control-Allow-Origin: *` เพื่อให้เครื่องมือของคนอื่นดึงไปใช้ได้ ## UI (Material 3 บนธีมดำของเรา) - **Navigation drawer** ด้านซ้ายแบ่งตามหมวด (platform, user-guide, concepts, architecture, history, operations, contributing, reference, agents) รายการที่เลือกอยู่ใช้สีแบบ "secondary container" และทุกปุ่มมี state layer ตอน hover/focus/กด - บนมือถือมี **top app bar** เล็ก ๆ ใต้ header กดปุ่มเมนูเพื่อเปิด drawer แบบ modal จากซ้าย - **ในหน้านี้ (TOC)** ด้านขวาบนเดสก์ท็อป ไฮไลต์หัวข้อที่กำลังอ่าน บนจอแคบเป็นกล่องพับได้เหนือเนื้อหา - breadcrumb, ปุ่มก่อนหน้า/ถัดไป, ปุ่ม **คัดลอก Markdown**, **ดูไฟล์ดิบ** และ **แก้ไขบน GitHub** (ซ่อนไว้จนกว่าจะตั้ง `NEXT_PUBLIC_REPO_URL`) - ฟอนต์ **Space Grotesk** (`next/font/google`, self-host) ใช้กับหัวข้อและ UI ของหน้าเอกสารเท่านั้น โหลดใน `src/app/docs/layout.tsx` หน้าอื่นจึงไม่ต้องดาวน์โหลด ตัวอักษรไทยใช้ IBM Plex Sans Thai โค้ดใช้ IBM Plex Mono เหมือนเดิม ## การค้นหา ดัชนี `public/docs-search-index.json` มีหนึ่งรายการต่อหนึ่งหัวข้อ (ประมาณ 300 รายการ) โหลดครั้งแรกที่เริ่มพิมพ์เท่านั้น ตัวค้นหาใน `src/lib/docs-search.ts` จับคู่แบบ substring บนข้อความที่ normalize เป็น NFC และตัวพิมพ์เล็ก ภาษาไทยซึ่งไม่มีช่องว่างระหว่างคำจึงค้นได้โดยไม่ต้องตัดคำ คำที่คั่นด้วยช่องว่างต้องพบครบทุกคำ ผลจากชื่อหน้าได้คะแนนสูงกว่าหัวข้อ และหัวข้อสูงกว่าเนื้อหา ## ไฟล์ที่เกี่ยวข้อง `scripts/build-docs.mjs`, `scripts/render-diagrams.mjs`, `src/lib/docs.ts`, `src/lib/docs-search.ts`, `src/app/docs/layout.tsx`, `src/app/docs/page.tsx`, `src/app/docs/[...slug]/page.tsx`, `src/app/docs/raw/[...slug]/route.ts`, `src/app/llms.txt/route.ts`, `src/components/docs/docs-shell.tsx`, `src/components/docs/doc-view.tsx`, `src/components/docs/doc-article.tsx`, `src/components/docs/doc-toc.tsx` และ test ใน `tests/docs-site.test.ts` --- # โมเดลข้อมูลแชต · Chat data model (D1 + R2) หน้านี้อธิบายว่าเราเก็บประวัติแชตระหว่างผู้ใช้กับ AI ไว้อย่างไร ตารางหลักอยู่ใน D1 (`nvx-db`) และหน้า `/chat` อ่านเขียนตารางเหล่านี้อยู่แล้ว รายละเอียด route อยู่ที่ [reference/api-chat.md](../reference/api-chat.md) การเข้าสู่ระบบมีสี่วิธีบน `/login`: Cursor (ใบเสร็จติดตั้ง Origin ที่ตรวจลายเซ็นแล้ว ไม่เก็บใบเสร็จ), Google (ไม่เก็บ access token), GitHub และรหัสผ่านของ DevStack migration `0006_github_auth` เพิ่ม `users.login`, `users.avatar_url` และตาราง `github_credentials` ที่เก็บเฉพาะ ciphertext ของ access token (AES-GCM จาก `CHAT_COOKIE_SECRET`) migration `0007_password_auth` เก็บแฮช PBKDF2 ใน `password_credentials` cookie `nvx_auth` เป็นรหัสผู้ใช้ที่ลงลายเซ็น ไม่ใช่ token หรือรหัสผ่าน อีเมลเดียวกันบนคนละผู้ให้บริการเป็นคนละบัญชี แผนเดิมที่ยังไม่ทำ (API key สำหรับบริการภายนอก, R2) อยู่ท้ายหน้า ## เป้าหมาย 1. เก็บบทสนทนาของผู้ใช้กับ AI ได้ครบ (ข้อความทุก role, โมเดลที่ใช้, จำนวน token) เพื่อเปิดต่อ ค้นย้อนหลัง และคิดต้นทุนได้ 2. ไฟล์แนบอยู่ใน R2 ส่วน D1 เก็บแค่ key และ metadata เพื่อให้ฐานข้อมูลเล็กและเร็ว 3. ให้ AI หรือบริการอื่นเชื่อมต่อผ่าน API key ที่ **เราออกและเพิกถอนได้เอง** โดยเก็บแค่ hash ของ key 4. ไม่ผูกกับผู้ให้บริการ auth รายใด คอลัมน์ auth จึงเป็น nullable ไว้ก่อน ## แผนภาพ ```mermaid erDiagram users ||--o{ conversations : owns conversations ||--o{ messages : contains messages ||--o{ attachments : has users { TEXT id PK INTEGER created_at TEXT display_name TEXT auth_provider TEXT auth_subject TEXT email } conversations { TEXT id PK TEXT user_id FK TEXT title TEXT model INTEGER created_at INTEGER updated_at INTEGER archived } messages { TEXT id PK TEXT conversation_id FK TEXT role TEXT content INTEGER tokens_in INTEGER tokens_out TEXT model INTEGER created_at } attachments { TEXT id PK TEXT message_id FK TEXT r2_key TEXT mime INTEGER size INTEGER created_at } api_clients { TEXT id PK TEXT name TEXT key_hash TEXT scopes INTEGER rate_limit INTEGER created_at INTEGER revoked_at } ``` `api_clients` ไม่ผูก foreign key กับตารางอื่น เพราะเป็นตัวตนของ "เครื่อง" ที่เข้ามาใช้ API ไม่ใช่ผู้ใช้คน ## การตัดสินใจหลัก | เรื่อง | ทางที่เลือก | เหตุผล | |---|---|---| | ชนิดของ id | `TEXT` (UUIDv7 หรือ ULID ที่แอปสร้าง) | เรียงตามเวลาได้ ไม่เดาง่ายแบบเลขรัน และสร้างได้ก่อน insert (ใช้ทำ R2 key ได้ทันที) | | เวลา | `INTEGER` เป็น Unix epoch มิลลิวินาที (UTC) | เทียบและเรียงได้เร็ว ไม่มีปัญหา timezone; ค่า default มาจาก `unixepoch('subsec') * 1000` | | boolean | `INTEGER` 0/1 พร้อม `CHECK` | SQLite ไม่มีชนิด boolean | | ตาราง | `STRICT` | SQLite บังคับชนิดคอลัมน์จริง ไม่ยอมให้ใส่ข้อความลงคอลัมน์ตัวเลข | | role | `CHECK (role IN ('user','assistant','system','tool'))` | กันค่าแปลกตั้งแต่ระดับฐานข้อมูล | | การลบ | `ON DELETE CASCADE` จาก users → conversations → messages → attachments | ลบผู้ใช้ทีเดียวได้ข้อมูลหายครบ (ตรงกับคำขอลบข้อมูลส่วนบุคคล) แต่ **วัตถุใน R2 ต้องลบเองจากโค้ด** เพราะ D1 สั่ง R2 ไม่ได้ | | ไฟล์แนบ | bytes อยู่ R2, D1 เก็บ `r2_key` (unique), `mime`, `size` | D1 จำกัดขนาดแถวและฐานข้อมูล ไฟล์ใหญ่จึงไม่ควรอยู่ใน D1 | | API key | เก็บ `key_hash` = SHA-256 hex ของ key เต็ม (unique) | key จริงแสดงครั้งเดียวตอนสร้าง ถ้าฐานข้อมูลรั่วก็ใช้ key ไม่ได้; key สุ่มยาวพอจึงไม่ต้องใช้ hash แบบช้า | | scopes | JSON array ในคอลัมน์ `TEXT` พร้อม `CHECK (json_valid(...))` | ยืดหยุ่น เพิ่ม scope ใหม่ได้โดยไม่ต้อง migrate | | `updated_at` ของบทสนทนา | แอปอัปเดตเองทุกครั้งที่เพิ่มข้อความ (ไม่ใช้ trigger) | ควบคุมได้ชัดและอยู่ใน batch เดียวกับ insert ข้อความ | ## Index และ query ที่รองรับ | Query | Index | |---|---| | รายการบทสนทนาของผู้ใช้ เรียงล่าสุดก่อน ซ่อนที่เก็บถาวร | `conversations_user_updated_idx (user_id, archived, updated_at DESC)` | | โหลดข้อความในบทสนทนาตามลำดับ และแบ่งหน้าด้วย `created_at` | `messages_conversation_created_idx (conversation_id, created_at, id)` | | ไฟล์แนบของข้อความ | `attachments_message_idx (message_id)` | | หา API client จาก key | unique index บน `key_hash` | | รายการ API client ที่ยังใช้งานได้ | `api_clients_active_created_idx (created_at DESC) WHERE revoked_at IS NULL` | | หาผู้ใช้จากตัวตนของผู้ให้บริการ auth | `users_auth_identity_uq (auth_provider, auth_subject)` (unique) | ## Migration ไฟล์อยู่ในโฟลเดอร์ `migrations/` และรันตามลำดับชื่อ: - `0001_schema_meta.sql`: ตาราง `schema_meta` (key/value) พร้อม `schema_version = 1` - `0002_chat_history.sql`: ทั้ง 5 ตารางและ index แล้วตั้ง `schema_version = 2` wrangler บันทึกว่ารันไฟล์ไหนไปแล้วในตาราง `d1_migrations` ห้ามแก้ไฟล์ที่ apply ไปแล้ว ถ้าจะเปลี่ยน schema ให้สร้างไฟล์ใหม่ (`0003_...sql`) ขั้นตอนดูที่ [operations/d1-database.md](../operations/d1-database.md) รายละเอียดคอลัมน์ดูที่ [reference/d1-schema.md](../reference/d1-schema.md) ## สถานะ R2 bucket `nvx-assets` (binding `ASSETS_BUCKET`) **ยังไม่ได้สร้าง** เพราะบัญชียังไม่เปิดใช้ R2 (API ตอบ error 10042 "Please enable R2 through the Cloudflare Dashboard") การเปิด R2 ต้องยอมรับเงื่อนไขการใช้งานและการคิดเงินใน dashboard ซึ่งเจ้าของบัญชีต้องตัดสินใจเอง ตาราง `attachments` ออกแบบรอไว้แล้ว และ binding จะเพิ่มใน `wrangler.jsonc` หลัง bucket มีจริงเท่านั้น (ถ้าใส่ก่อน deploy จะล้ม) ## แผนขั้นต่อไป (ยังไม่ได้สร้าง) **API routes** (ทั้งหมดอยู่ใต้ `/api`, ใช้ zod ตรวจ input และ rate limit แบบเดียวกับ `/api/plan`): | Route | ทำอะไร | |---|---| | `GET /api/chat/conversations` | รายการบทสนทนาของผู้ใช้ (cursor ด้วย `updated_at`) | | `POST /api/chat/conversations` | สร้างบทสนทนา | | `GET/PATCH/DELETE /api/chat/conversations/:id` | อ่าน, เปลี่ยนชื่อ/เก็บถาวร, ลบ (ลบวัตถุ R2 ด้วย) | | `GET /api/chat/conversations/:id/messages` | ข้อความ แบ่งหน้าด้วย `created_at` | | `POST /api/chat/conversations/:id/messages` | ส่งข้อความผู้ใช้แล้ว stream คำตอบ AI (SSE) จากนั้นบันทึกข้อความ assistant พร้อม `tokens_in/out` ใน batch เดียว | | `POST /api/chat/attachments` | อัปโหลดผ่าน Worker เข้า R2 (จำกัดชนิดและขนาด) หรือออก presigned URL | | `/api/v1/...` | API เดียวกันสำหรับ AI ภายนอก ใช้ `Authorization: Bearer ` | **Auth:** - ผู้ใช้คน: เริ่มด้วย OAuth (GitHub/Google) หรือ passkey ผ่าน session cookie แบบ `HttpOnly; Secure; SameSite=Lax`; ทางเลือกที่เร็วกว่าสำหรับใช้ภายในคือ Cloudflare Access - AI ภายนอก: key รูปแบบ `nvx_live_` แสดงครั้งเดียว เก็บ SHA-256 ใน `api_clients.key_hash` ตรวจ `revoked_at IS NULL` และ scope (เช่น `chat:read`, `chat:write`) ทุกคำขอ - Rate limit ต่อ client ตาม `api_clients.rate_limit` ด้วย Workers Rate Limiting (ต้องเพิ่ม binding และ namespace ใหม่) **UI:** - หน้า `/chat` ใช้ shell และ component ของ NVX ที่มีอยู่ (ไม่เปลี่ยน layout หน้าเดิม): sidebar รายการบทสนทนา, หน้าต่างข้อความที่ render Markdown ด้วย pipeline ที่ sanitize แล้ว, ช่องพิมพ์พร้อมแนบไฟล์, เลือกโมเดล - สองภาษา (ไทย/อังกฤษ) และผ่าน WCAG AA ทั้งสองธีม ตามมาตรฐานเดิมของโปรเจกต์ ## ดูเพิ่ม [reference/d1-schema.md](../reference/d1-schema.md), [operations/d1-database.md](../operations/d1-database.md), [reference/config-env.md](../reference/config-env.md), [cloudflare-runtime.md](./cloudflare-runtime.md) --- # Chat: User Chat, Dev Chat, Sandbox Factory สามหน้าใช้คอมโพเนนต์ [`ChatApp`](../../src/components/chat/chat-app.tsx) ตัวเดียว และ handler ฝั่งเซิร์ฟเวอร์ร่วม [`turn-route.ts`](../../src/lib/chat/turn-route.ts) ```mermaid flowchart LR U[/chat User Chat/] -->|POST /api/chat/turn| H[handleTurn user] D[/chat/dev Dev Chat/] -->|POST /api/chat/dev/turn| HD[handleTurn dev] F[/factory BYOK/] -->|POST /api/factory/*| FR[factory routes] H --> WAI[Workers AI binding AI] H --> HF[HF router DeepSeek V3] HD --> AG[OpenAI Agents API + openai_hosted sandbox] FR --> AG2[Agents API with user's key] H & HD & FR --> DB[(D1 nvx-db)] ``` ## User Chat (`channel = 'user'`) - โหมด `general | thai | code | strong | image` ([`plain-turn.ts`](../../src/lib/chat/plain-turn.ts)) - Workers AI ผ่าน binding `AI` ([`workers-ai.ts`](../../src/lib/ai/workers-ai.ts)) สตรีม delta เป็น SSE; `strong` ใช้ HF router ([`hf.ts`](../../src/lib/ai/hf.ts)); ล้มแล้วถอยไปอีกค่าย - ภาพ: FLUX-1 schnell → bytes เก็บใน `attachment_blobs` (≤1.5 MB) เสิร์ฟที่ `/api/chat/images/` เฉพาะเจ้าของ cookie - โควตา: `messageRules` / `imageRules(…,"user")` ใน [`limits.ts`](../../src/lib/chat/limits.ts) day bucket ตามเวลาไทย ## Dev Chat (`channel = 'dev'`) - [`turn.ts`](../../src/lib/chat/turn.ts) สร้าง/ต่อ session ของ Agents API ด้วย config inline จาก [`agents/orchestrator/agent.json`](../../agents/orchestrator/agent.json) แปลง event เป็น `UiEvent` ([`events.ts`](../../src/lib/chat/events.ts)) สำหรับแผง Sandbox - function tool (`search_docs`, `generate_image`, `plan_site`) รันใน Worker - guard: `agentRules` (5/คน/วัน, 30/ทั้งเว็บ) + งบ `CHAT_DAILY_BUDGET_USD` (micro-USD ใน `usage_counters` scope `cost:global`) ## Sandbox Factory - key ของผู้ใช้มากับ header `x-openai-key` ทุกคำขอ ([`factory.ts`](../../src/lib/chat/factory.ts)) ไม่เก็บ ไม่ log - `POST /api/factory/session` สร้าง agent + session (`openai_hosted` หรือ `self_hosted`) บันทึก conversation `source='factory'` - `GET /api/factory/watch` สตรีมสถานะ environment (pending/connected/failed) สูงสุด 10 นาที - guard: per-IP ต่อชั่วโมง + `NVX_FACTORY_LIMITER` ## Self-hosted executor ของเรา (devbox) กล่องพัฒนาของเรารัน `codex exec-server` ภายใต้ supervisor (`/opt/nvx-agent/devbox`, คำสั่ง `devboxctl start|stop|restart|status|ensure|logs`) เป็นผู้ใช้ `nvxagent` โดยมีแค่ `CODEX_API_KEY`, `HOME`, `PATH` ใน env ใช้สำหรับทดสอบ self-hosted เท่านั้น ไม่ได้ให้บริการแชตสาธารณะ ## ค่าใช้จ่ายโดยประมาณ | ส่วน | ราคา | |---|---| | Workers AI | ฟรี 10,000 neurons/วัน (รีเซ็ต 00:00 UTC) แล้ว $0.011/1k neurons; Gemma 4 ≈ $0.10/$0.30 ต่อ M token; ภาพ FLUX ≈ 58 neurons (~$0.0006) | | HF DeepSeek V3 | ตัดจากเครดิต HF Pro | | Dev Chat (gpt-6.1-sol) | $2 / $0.10 cached / $10 ต่อ M token; turn ง่าย ≈ $0.01–0.06, turn หนัก ≈ $0.25 | --- # NVX UI — UI/UX Framework > 🇹🇭 ภาษาไทยอยู่ด้านล่าง · Thai version below — [ไปที่ภาษาไทย](#ภาษาไทย) NVX UI is the design system behind NVX Stack Builder: **design tokens** (CSS variables → Tailwind theme), a small **component library** in `src/components/ui`, and a **living style guide** at [`/design`](http://localhost:3000/design) that renders every token and component variant in Thai and English, in both themes. **v0.5 Apple-inspired developer theme.** The look references [developer.apple.com](https://developer.apple.com/) and is black-first: pure-black canvas, grey surfaces separated by tone and hairline borders, big bold tightly tracked sans headlines, generous spacing, 12–22px radii, pill buttons, a restrained blue accent (filled `primary` CTA plus `link` text), and real tool logos as app-icon tiles. It's the default for everyone, whatever the OS colour preference. A light variant (Apple's `#f5f5f7` canvas) stays available from the theme toggle. The terminal flavour is now limited to code windows: `$` prompts, macOS traffic lights, a blinking cursor in the hero. ``` src/app/tokens.css ← token values (.dark = default black theme, :root = light variant) src/app/globals.css ← maps tokens into Tailwind (@theme inline), motion utilities, keyframes src/lib/design-tokens.ts ← token manifest (names, descriptions) used by /design and tests src/components/ui/ ← components (import from "@/components/ui") src/components/design-system.tsx + src/app/design/page.tsx ← the /design style guide tests/ui-components.test.tsx, tests/tokens.test.ts ← component + token/contrast tests ``` ## 1. Design tokens All values live in `src/app/tokens.css` as `--nvx-*` variables. `.dark` overrides only what changes; everything else cascades from `:root`. `globals.css` maps them into the Tailwind theme so you write **semantic utilities**, never raw palette colours: | Group | Tokens (`--nvx-…`) | Tailwind utilities | |---|---|---| | Surfaces | `bg` (#000), `surface`, `surface-2`, `surface-3`, `raised` (selected segment/tab), `border`, `border-strong` | `bg-bg`, `bg-surface`, `bg-surface-2`, `border-border`, `border-border-strong` | | Text | `fg`, `muted`, `subtle` | `text-fg`, `text-muted`, `text-subtle` | | Brand | `primary` `#0071e3`, `primary-hover`, `primary-fg`, `primary-soft`, `primary-soft-fg`, `link` (`#2997ff` dark / `#0066cc` light), `accent`, `ring`, `brand-pink` `#fb0fab` + `brand-glow` (mascot only, decorative) | `bg-primary text-primary-fg` for filled CTAs, **`text-link` for blue text** (never `text-primary`: `#0071e3` is not AA on black), `ring-ring` | | Status | `success`/`warning`/`danger`/`info` + `-soft` + `-fg` (`danger-hover`) | `bg-success-soft text-success-fg`, `bg-danger` … | | Code | `code-bg`, `code-fg`, `code-prompt` (terminal green), `code-muted`, `code-border`, `chrome-close/min/zoom` (traffic lights) — always dark | `bg-code-bg text-code-fg text-code-prompt` | | Decoration | `glow-1`, `glow-2`, `tile-top`, `tile-bottom`, `tile-edge`, `tile-sheen` | `.hero-glow` (soft blue/purple radial glows), `.tool-tile` (app-icon tile), `.nav-glass` (translucent header) | | Overlay | `overlay` | `bg-overlay` | | Radius | xs 4px · sm 6px · md 10px · lg 12px · xl 18px · 2xl 22px · full | `rounded-md` (inputs, badges), `rounded-xl` (cards, code windows), `rounded-full` (buttons, chips, switches) | | Spacing | 4px base unit (`--spacing: 0.25rem`) | `p-1` = 4px, `gap-4` = 16px — prefer 2/3/4/6/8 | | Shadow | dark: surfaces separate by tone + hairline, `shadow-md` soft drop on hover, `shadow-lg` dialogs/toasts; light: soft Apple-style shadows | `shadow-sm` … | | Motion | `duration-fast` 120ms, `duration-base` 200ms, `duration-slow` 320ms, `ease-standard`, `ease-emphasized` | `motion-fast`, `motion-base`, `animate-fade-in`, `animate-pop-in`, `animate-slide-in`, `animate-sheet-in` | | Focus | — | `focus-ring` utility (2px `ring` outline on `:focus-visible`) | **Typography.** `font-sans` = **Inter** (SF-like) → **IBM Plex Sans Thai** → system UI, for headlines, body, nav and buttons. `font-mono` = **IBM Plex Mono** → Plex Sans Thai, **only** for `code`/`pre`/`kbd`/`samp`, commands and small tags (badge `sm`). Headlines are bold with tight tracking (`tracking-tighter` −0.03em); Thai headings automatically get `letter-spacing: 0` and line-height 1.3. Scale: `text-display` 64px (hero, 44px on mobile), `text-4xl`/`sm:text-5xl` (page h1), `text-3xl`/`sm:text-[40px]` (section h2), `text-xl` (stage titles), `text-base`/17px (body), `text-sm` (UI), `text-xs` (captions/badges). Thai needs line-height ≥ 1.5 for body — don't use `leading-none`/`leading-tight` on Thai body text. Links use the `link` utility (`text-link`, underline on hover). **Theme.** The black theme is the `.dark` class on ``. It's rendered by the server and is the **default**. The inline pre-paint script only removes it when `localStorage["nvx-theme"] === "light"` (the toggle), and the OS `prefers-color-scheme` is deliberately ignored. Because components only use semantic tokens, nothing in a component should ever need a `dark:` variant — if you reach for one, add/adjust a token instead. **Reduced motion.** Under `prefers-reduced-motion: reduce` all durations become 0.01ms and animations are disabled. The hero cursor stays visible but stops blinking. **Icons.** No emoji anywhere in the UI. - **Tool logos** (Node.js, npm, pnpm, Yarn, Python, PyPI, uv, Next.js, React, Vite, Express, FastAPI, Streamlit, TypeScript, Tailwind, Docker, GitHub Actions, Vitest, ESLint, pytest, Ruff, …) come from [simple-icons](https://simpleicons.org) (CC0 data). `scripts/gen-brand-icons.mjs` copies only the icons we use into `src/components/brand/brand-icons.data.ts` (run `node scripts/gen-brand-icons.mjs` after adding an id) so the bundle stays small. Logos are trademarks of their owners, used only to identify the tools (see `NOTICE`). OpenAI isn't in simple-icons, so AI features use a generic `ai` tile (lucide Sparkles). - `src/components/brand/tool-icon.tsx`: - `` is an app-icon tile (28/40/56px, radius 8/11/15px) with the logo in its official colour. Use it on template cards, template detail, builder pick-lists and step cards. - `` is an inline 1em mark, `currentColor` by default (tags, badges, segmented options). - Helpers: `templateTool(t)`, `tagTool(tag)`, `commandTool(commands)` (first known CLI → logo, else `terminal`), `addonTool(id, runtimes)`. - **UI icons** are [lucide-react](https://lucide.dev) line icons (`h-4 w-4`, `aria-hidden`; icon-only buttons keep an `aria-label`). **Code windows (the remaining terminal touch).** `CodeBlock` and the hero card use `code-bg`, macOS traffic lights (`chrome-*` tokens), a `$` prompt in `code-prompt` green, and `term-cursor` (blinking block cursor, `aria-hidden`, static under reduced motion). The v0.4 `term-prompt`/`term-section`/`term-dos`/`term-bracket`/scanline classes were removed. ## 2. Components Import everything from the barrel: `import { Button, Card, useToast } from "@/components/ui"`. Interactive pieces are built on **Radix UI** primitives (headless, accessible); styling is Tailwind + tokens. `card`, `badge`, `empty-state`, `kbd`, `skeleton`, `button-classes` are server-safe; the rest are client components. | Component | File | Variants / props | Notes | |---|---|---|---| | `Button` | `button.tsx` | `variant`: primary · secondary · outline · soft · ghost · danger; `size`: sm · md · lg · icon; `loading`; `asChild` | Defaults to `type="button"`. `loading` ⇒ disabled + `aria-busy` + spinner. Icon-only buttons need `aria-label`. `buttonClasses()` (server-safe) styles a ``. | | `Input`, `Textarea`, `Select` | `field.tsx` | `size` sm·md·lg, `invalid` | Native controls (best mobile + a11y support). | | `Field`, `Label` | `field.tsx` | `label`, `hint`, `error`, render-prop child | Wires `id`, `aria-describedby`, `aria-invalid`; error has `role="alert"`. | | `Checkbox`, `Switch` | `checkbox.tsx` | `label`, `description`, `checked`, `onCheckedChange`, `disabled` | Radix. Always labelled. | | `Segmented` | `segmented.tsx` | `legend`, `options`, `value`, `onChange`, `size`, `hideLegend` | Native radio group in a `
` — arrow keys work. | | `Card` + `CardHeader/Title/Description/Content/Footer` | `card.tsx` | `interactive`, `as` (div/article/section/aside/li); `CardTitle as` h2/h3/h4 | Pick the heading level that fits the page outline. | | `Badge` | `badge.tsx` | neutral · primary · success · warning · danger · info · outline; `size` sm·md | Text carries the meaning, not colour. | | `Tabs` | `tabs.tsx` | Radix Tabs (`TabsList` needs `aria-label`) | ←/→ roving focus. | | `Dialog`, `DialogTrigger`, `DialogContent`, `DialogClose` | `dialog.tsx` | `title` (required), `description`, `side` center·right, `hideTitle`, `closeLabel` | Focus trap, Esc, scroll lock, focus return. `side="right"` = sheet (mobile nav). | | `ToastProvider`, `useToast` | `toast.tsx` | `toast({ title, description?, variant: default·success·error·info, duration? })` | Radix Toast; polite (errors assertive); swipe/✕ to dismiss; pauses on hover/focus. `useToastOptional` for library code. | | `Tooltip`, `TooltipProvider` | `tooltip.tsx` | `content`, `side` | Opens on hover **and** focus. Supplements, never replaces, an accessible name. | | `CodeBlock` | `code-block.tsx` | `code`, `title`, `prompt`, `copyLabel`, `copiedLabel`, `toastTitle`, `maxHeight` | Copy button with live label; `$` prompt glyphs aren't copied; toast when inside provider. App wrapper with i18n: `src/components/copy-button.tsx`. | | `Stepper` | `stepper.tsx` | `items`, `current`, `onStepChange?`, `label` | `