This commit is contained in:
Your Name committed 2026-10-06 15:35:04 +07:00
1 parent 230716933e
commit 3bbe2bb35e
16 files changed
+1815 -109

No files matched your search

+2 -1
View File
@@ -34,6 +34,7 @@ import { toolsRoute } from "./routes/tools";
import { crmRoute } from "./routes/crm";
import { brandKnowledgeRoute } from "./routes/brandKnowledge";
import { categorySectionsRoute } from "./routes/categorySections";
import { llmPromptsRoute } from "./routes/llmPrompts";
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051;
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_brand";
@@ -141,7 +142,7 @@ const app = new Elysia()
return file;
})
.group("/api", (app) =>
app.use(authRoute).use(categoriesRoute).use(categorySectionsRoute).use(productsRoute).use(productAttributesRoute).use(uploadRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute).use(profileRoute).use(lineRoutes).use(menusRoute).use(newsletterRoute).use(mediaRoute).use(contactsRoute).use(backupRoute).use(businessAnalyticsRoute).use(toolsRoute).use(crmRoute).use(brandKnowledgeRoute)
app.use(authRoute).use(categoriesRoute).use(categorySectionsRoute).use(productsRoute).use(productAttributesRoute).use(uploadRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute).use(profileRoute).use(lineRoutes).use(menusRoute).use(newsletterRoute).use(mediaRoute).use(contactsRoute).use(backupRoute).use(businessAnalyticsRoute).use(toolsRoute).use(crmRoute).use(brandKnowledgeRoute).use(llmPromptsRoute)
)
.listen({ port: PORT, hostname: "0.0.0.0" });
+416
View File
@@ -0,0 +1,416 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { adminOnly } from "../middleware/auth";
export const DEFAULT_LLM_PROMPTS = [
{
title: "Mega Menu JSON Tree Generator",
category: "navigation",
description: "สร้างโครงสร้างเมนู Header Navigation แบบ Multi-level (Array of Nodes) พร้อม labels ทุกภาษา, href, icon และ badges",
tags: ["Menu", "JSON", "Navigation", "Multilingual"],
isSystem: true,
content: `คุณคือ Senior Full-Stack Navigation & UX Architect สำหรับแพลตฟอร์ม e-Commerce หรู (DBK Platform / HomAroma)
จงออกแบบโครงสร้างเมนู Mega Menu (Main Navigation) ในรูปแบบ JSON Array ตามสเปกด้านล่างนี้ โดยไม่ต้องใส่ Markdown block ครอบ หรือคำอธิบายเสริมใดๆ ให้ตอบเฉพาะ JSON Array เท่านั้น
### โครงสร้างข้อมูล Node แต่ละตัว (MenuItem):
[
{
"id": "menu-unique-id", // string เช่น "nav-scents", "nav-candles"
"label": "English Display Name",
"href": "/shop/category-slug", // URL ภายในเว็บ หรือ # หากมี dropdown
"description": "คำบรรยายสั้นๆ อธิบายหมวดหมู่",
"target": "_self", // "_self" หรือ "_blank"
"badge": "NEW", // ตัวเลือก: null หรือ "NEW", "HOT", "SALE", "BESTSELLER"
"icon": "Sparkles", // Lucide icon เช่น "Flame", "Droplets", "Wind", "Sparkles", "Package"
"displayMode": "mega", // "mega" (กล่องกว้าง 3-4 คอลัมน์) หรือ "dropdown" (เมนูลงมาธรรมดา)
"columns": 3, // จำนวนคอลัมน์ของเมนูย่อย (2, 3 หรือ 4)
"translations": {
"th": { "label": "ชื่อภาษาไทย", "description": "คำบรรยายภาษาไทย" },
"en": { "label": "English Name", "description": "English description" }
},
"children": [
// โครงสร้างลูกแบบเดียวกัน มี children ซ้อนลงไปได้หลายระดับ
]
}
]
### ข้อกำหนด:
1. รองรับการจัดกลุ่มสินค้าอย่างเป็นระเบียบ แบ่งเป็น Header กลุ่มย่อย และไอเทมลูก
2. ทุกรายการควรมี translations ภาษาไทย (th) และอังกฤษ (en) ครบถ้วน
3. ตอบกลับเฉพาะ Pure JSON Array ที่ถูกต้องตามมาตรฐาน JSON เท่านั้น`
},
{
title: "Product Catalog & Scent Formulations Generator",
category: "catalog",
description: "สร้างแคตตาล็อกสินค้าเครื่องหอม เทียนหอม รูมสเปรย์ พร้อม Note กลิ่น (Top/Heart/Base), SKU, ราคา และ Attribute Options",
tags: ["Products", "Catalog", "Aromatherapy", "SKU"],
isSystem: true,
content: `คุณคือ Senior Perfumer และ E-commerce Catalog Specialist เชี่ยวชาญการสร้างข้อมูลผลิตภัณฑ์เครื่องหอมระดับ Luxury & Niche Fragrance
จงช่วยสร้างข้อมูลรายการสินค้า (Products) ในรูปแบบ JSON Array เพื่อนำเข้าสู่ระบบหลังบ้าน DBK Platform
### ข้อมูลสินค้าที่ต้องการในแต่ละ Object:
{
"name": "ชื่อสินค้าภาษาอังกฤษ",
"nameTh": "ชื่อสินค้าภาษาไทย",
"slug": "product-url-slug",
"sku": "HMA-CAT-001",
"categorySlug": "scented-candles",
"price": 890,
"originalPrice": 1290,
"inStock": true,
"stockCount": 50,
"shortDescription": "คำบรรยายสั้นน่าดึงดูด 1-2 ประโยค",
"scentProfile": {
"intensity": "Moderate", // Subtle, Moderate, Strong, Intense
"family": "Woody & Amber", // Floral, Citrus, Woody, Oriental, Fresh
"topNotes": ["Bergamot", "Pink Pepper"],
"heartNotes": ["Atlas Cedar", "Smoked Oud"],
"baseNotes": ["Ambergris", "Madagascar Vanilla"]
},
"attributes": [
{ "name": "Size / Net Weight", "value": "220g / 7.7 oz" },
{ "name": "Burn Time", "value": "Approx. 50-55 Hours" },
{ "name": "Wax Type", "value": "100% Natural Soy & Coconut Wax Blend" }
],
"tags": ["Bestseller", "Relaxation", "Handmade"]
}
### เงื่อนไข:
- อารมณ์ของกลิ่นต้องมีความประณีต อธิบายโน้ตกลิ่นชัดเจนและเป็นไปได้จริง
- ตอบกลับเฉพาะ JSON Array เท่านั้น`
},
{
title: "Dynamic Theme & Color Tokens Generator",
category: "theme",
description: "สร้าง Color Palette JSON สำหรับระบบ Dynamic Theme Tokens (Light/Dark Mode, Glows, Borders, Surfaces)",
tags: ["Theme", "Colors", "Tokens", "UI/UX"],
isSystem: true,
content: `คุณคือ Senior UI/UX Designer และ Design System Architect ระดับมืออาชีพ เชี่ยวชาญการออกแบบ Color Palette, Dark/Light Theme System, และ Visual Identity ให้กับร้านค้าออนไลน์ระดับ Luxury, Minimalist, Cyberpunk, หรือ Modern Lifestyle
กรุณาสร้าง JSON Configuration สำหรับ Theme Tokens ของระบบ e-Commerce ตามสเปกด้านล่าง:
### สเปก JSON Tokens:
{
"themeName": "ชื่อธีม (เช่น Cyberpunk Amber, Midnight Velvet, Nordic Forest)",
"mode": "dark", // "dark" หรือ "light"
"description": "คำอธิบาย Mood & Tone สั้นๆ",
"tokens": {
"primary": "#06b6d4",
"primaryLight": "#22d3ee",
"primaryDark": "#0891b2",
"secondary": "#a855f7",
"background": "#0b0f19",
"surface": "#111827",
"surfaceAlt": "#1f2937",
"text": "#f9fafb",
"textMuted": "#9ca3af",
"border": "#374151",
"glow": "rgba(6, 182, 212, 0.4)",
"fontFamilyHeading": "'Plus Jakarta Sans', sans-serif",
"fontFamilyBody": "'Inter', sans-serif",
"borderRadius": "0.75rem"
}
}
### กฎสำคัญ:
1. สีต้องมีความกลมกลืน (Harmonious contrast ratio) ผ่านเกณฑ์ WCAG AA สำหรับ Text
2. คืนค่าเฉพาะ Valid JSON Object เท่านั้น ห้ามใส่คำบรรยายอื่นนอก JSON`
},
{
title: "Luxury Scent Storytelling & Copywriting",
category: "copywriting",
description: "เขียนคำบรรยายกลิ่นแบบวรรณกรรม Luxury สร้างภาพจินตนาการ พร้อมเรื่องราว บรรยากาศ (Mood & Atmosphere) และเวลาที่เหมาะสม",
tags: ["Copywriting", "Luxury", "Storytelling", "Marketing"],
isSystem: true,
content: `คุณคือ Creative Director และ Luxury Fragrance Copywriter ระดับสากล (สไตล์ Diptyque, Le Labo, Byredo, Jo Malone)
ช่วยเขียนบทบรรยายสินค้ากลิ่นหอมสำหรับ:
- **ชื่อกลิ่น**: [ระบุชื่อกลิ่น]
- **บุคลิกของกลิ่น**: [ระบุ เช่น อบอุ่น สงบ ลึกลับ สดชื่นยามเช้า]
- **กลุ่มเป้าหมาย**: ลูกค้าที่มีรสนิยม มองหาความผ่อนคลายและสร้างบรรยากาศพิเศษในบ้าน
### โครงสร้างเนื้อหาที่ต้องการ:
1. **Hook Title / Headline**: พาดหัวชวนฝัน 1 ประโยคที่สร้างอารมณ์ในทันที
2. **Scent Narrative (120-150 คำ)**: เล่าเรื่องบรรยากาศ เวลา ความทรงจำ และความรู้สึกเมื่อกลิ่นนี้ลอยฟุ้งในห้อง
3. **Olfactory Pyramid (ปิรามิดกลิ่น)**:
- **Top Notes**: (กลิ่นแรกที่สัมผัส - ชัดเจน สดชื่น หรือดึงดูด)
- **Heart Notes**: (หัวใจของกลิ่น - ลึกซึ้ง มีเอกลักษณ์)
- **Base Notes**: (กลิ่นติดตรึง - อบอุ่น ติดทนนาน)
4. **Ideal Setting & Moment**: เวลาและสถานที่ที่เหมาะที่สุด (เช่น ฝนตกยามค่ำคืน, อ่านหนังสือมุมโปรด, ห้องนอนก่อนนอน)
5. **Key Benefit & Mood**: ความรู้สึกที่ได้รับ (เช่น Calming, Uplifting, Grounding, Sensual)
ให้ใช้ภาษาไทยที่สละสลวย มีเสน่ห์ ชวนสัมผัสความอบอุ่น และแปลเวอร์ชันภาษาอังกฤษ (English) แนบมาด้วย`
},
{
title: "CMS Page Builder Section Payloads",
category: "cms",
description: "สร้าง Payload บล็อกเนื้อหาสำหรับ Page Builder เช่น HeroArticle, EditorialCarousel, ScentBreakdown, QuoteCallout",
tags: ["CMS", "PageBuilder", "Payload", "Blocks"],
isSystem: true,
content: `คุณคือ Headless CMS Architect เชี่ยวชาญการกำหนดโครงสร้าง Dynamic Page Sections สำหรับระบบ HomAroma Page Builder
จงสร้าง JSON Array ของ Block Sections สำหรับหน้ารายละเอียดหรือ Landing Page:
### รูปแบบ Section Type ที่ระบบรองรับ:
1. **HeroArticle**: { "type": "HeroArticle", "data": { "title": "...", "subtitle": "...", "ctaText": "...", "ctaLink": "...", "badge": "..." } }
2. **ScentNotesBreakdown**: { "type": "ScentNotesBreakdown", "data": { "topNotes": ["..."], "heartNotes": ["..."], "baseNotes": ["..."], "story": "..." } }
3. **QuoteCallout**: { "type": "QuoteCallout", "data": { "quote": "...", "author": "...", "role": "..." } }
4. **EditorialTile**: { "type": "EditorialTile", "data": { "heading": "...", "paragraph": "...", "alignment": "left" } }
5. **Accordion**: { "type": "Accordion", "data": { "items": [{ "question": "...", "answer": "..." }] } }
ตอบเฉพาะ JSON Array ของ Sections ที่สมบูรณ์ พร้อมรองรับหลายภาษาในแต่ละ Object`
},
{
title: "Multilingual JSON Translation & Localization",
category: "translation",
description: "แปลภาษา JSON Dictionary หรือ Object จากภาษาไทยเป็นอังกฤษ, จีน, ญี่ปุ่น โดยคงโครงสร้าง Key-Value ครบถ้วน",
tags: ["Translation", "i18n", "Localization", "JSON"],
isSystem: true,
content: `คุณคือ Professional Localization Specialist เชี่ยวชาญงานแปล e-Commerce หรูสำหรับภาษา ไทย (TH), อังกฤษ (EN), จีนตัวย่อ (ZH) และ ญี่ปุ่น (JA)
### คำสั่งงาน:
1. รับ JSON Object ต้นฉบับ
2. แปลเฉพาะส่วนของ Value ข้อความ โดย **ห้ามแก้ไข Key** และ **ห้ามเปลี่ยนโครงสร้าง Hierarchy** โดยเด็ดขาด
3. สำนวนการแปลต้องสุภาพ เป็นทางการ และมีระดับ (Luxury E-commerce Tone)
4. คงตัวแปร placeholder เช่น {{name}}, {{count}}, {price} ไว้ตามเดิม
5. ส่งผลลัพธ์กลับมาเป็น JSON Format เดียวกัน 100% ไม่ใส่คำอธิบายใดๆ เสริม`
}
];
// Helper to auto-seed initial presets if DB is empty
async function ensureDefaultPrompts() {
const count = await prisma.llmPrompt.count();
if (count === 0) {
for (const item of DEFAULT_LLM_PROMPTS) {
await prisma.llmPrompt.create({
data: {
title: item.title,
category: item.category,
description: item.description,
tags: item.tags,
content: item.content,
isSystem: item.isSystem,
},
});
}
}
}
export const llmPromptsRoute = new Elysia({ prefix: "/llm-prompts" })
// GET all prompts
.get("/", async () => {
await ensureDefaultPrompts();
const prompts = await prisma.llmPrompt.findMany({
orderBy: [
{ isSystem: "desc" },
{ id: "desc" },
],
});
return prompts;
})
// GET prompt by id
.get("/:id", async ({ params: { id }, set }) => {
const promptId = parseInt(id, 10);
if (isNaN(promptId)) {
set.status = 400;
return { error: "Invalid prompt ID" };
}
const prompt = await prisma.llmPrompt.findUnique({
where: { id: promptId },
});
if (!prompt) {
set.status = 404;
return { error: "Prompt not found" };
}
return prompt;
})
// POST create new prompt
.use(adminOnly)
.post(
"/",
async ({ body, set }) => {
try {
const { title, category, description, tags, content, isSystem } = body;
const newPrompt = await prisma.llmPrompt.create({
data: {
title,
category: category || "custom",
description: description || null,
tags: Array.isArray(tags) ? tags : [],
content,
isSystem: isSystem || false,
},
});
return { success: true, data: newPrompt };
} catch (err: any) {
console.error("[LlmPrompts] Create error:", err);
set.status = 500;
return { success: false, error: err.message || "Failed to create prompt" };
}
},
{
body: t.Object({
title: t.String({ minLength: 1 }),
category: t.Optional(t.String()),
description: t.Optional(t.Nullable(t.String())),
tags: t.Optional(t.Array(t.String())),
content: t.String({ minLength: 1 }),
isSystem: t.Optional(t.Boolean()),
}),
}
)
// PUT update prompt
.put(
"/:id",
async ({ params: { id }, body, set }) => {
try {
const promptId = parseInt(id, 10);
if (isNaN(promptId)) {
set.status = 400;
return { error: "Invalid prompt ID" };
}
const existing = await prisma.llmPrompt.findUnique({
where: { id: promptId },
});
if (!existing) {
set.status = 404;
return { error: "Prompt not found" };
}
const updated = await prisma.llmPrompt.update({
where: { id: promptId },
data: {
title: body.title !== undefined ? body.title : existing.title,
category: body.category !== undefined ? body.category : existing.category,
description: body.description !== undefined ? body.description : existing.description,
tags: body.tags !== undefined ? body.tags : existing.tags,
content: body.content !== undefined ? body.content : existing.content,
},
});
return { success: true, data: updated };
} catch (err: any) {
console.error("[LlmPrompts] Update error:", err);
set.status = 500;
return { success: false, error: err.message || "Failed to update prompt" };
}
},
{
body: t.Object({
title: t.Optional(t.String()),
category: t.Optional(t.String()),
description: t.Optional(t.Nullable(t.String())),
tags: t.Optional(t.Array(t.String())),
content: t.Optional(t.String()),
}),
}
)
// DELETE prompt
.delete("/:id", async ({ params: { id }, set }) => {
try {
const promptId = parseInt(id, 10);
if (isNaN(promptId)) {
set.status = 400;
return { error: "Invalid prompt ID" };
}
await prisma.llmPrompt.delete({
where: { id: promptId },
});
return { success: true, message: "Prompt deleted successfully" };
} catch (err: any) {
console.error("[LlmPrompts] Delete error:", err);
set.status = 500;
return { success: false, error: err.message || "Failed to delete prompt" };
}
})
// POST reset default presets
.post("/reset-presets", async ({ set }) => {
try {
// Upsert default presets
for (const item of DEFAULT_LLM_PROMPTS) {
const existing = await prisma.llmPrompt.findFirst({
where: { title: item.title, isSystem: true },
});
if (!existing) {
await prisma.llmPrompt.create({
data: {
title: item.title,
category: item.category,
description: item.description,
tags: item.tags,
content: item.content,
isSystem: true,
},
});
}
}
const allPrompts = await prisma.llmPrompt.findMany({
orderBy: [{ isSystem: "desc" }, { id: "desc" }],
});
return { success: true, data: allPrompts };
} catch (err: any) {
console.error("[LlmPrompts] Reset presets error:", err);
set.status = 500;
return { success: false, error: err.message || "Failed to reset presets" };
}
})
// POST bulk import
.post(
"/bulk-import",
async ({ body, set }) => {
try {
const items = body as Array<{
title: string;
category?: string;
description?: string;
tags?: string[];
content: string;
}>;
if (!Array.isArray(items) || items.length === 0) {
set.status = 400;
return { success: false, error: "Empty or invalid items array" };
}
const createdItems = [];
for (const item of items) {
if (!item.title || !item.content) continue;
const created = await prisma.llmPrompt.create({
data: {
title: item.title,
category: item.category || "custom",
description: item.description || null,
tags: Array.isArray(item.tags) ? item.tags : [],
content: item.content,
isSystem: false,
},
});
createdItems.push(created);
}
return { success: true, count: createdItems.length, data: createdItems };
} catch (err: any) {
console.error("[LlmPrompts] Bulk import error:", err);
set.status = 500;
return { success: false, error: err.message || "Failed to import prompts" };
}
},
{
body: t.Array(
t.Object({
title: t.String(),
category: t.Optional(t.String()),
description: t.Optional(t.Nullable(t.String())),
tags: t.Optional(t.Array(t.String())),
content: t.String(),
})
),
}
);
+1 -1
View File
@@ -1,6 +1,6 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
// Trigger reload: resilient position and contentWidth support
// Trigger reload: support landscape and wide aspect ratios for FEATURED_TILES and EDITORIAL_TILE
import {
LocalizedStringSchema,
HeroSectionPayloadSchema,