x
This commit is contained in:
1 parent
683f54fd95
commit
44a613f8ec
12 files changed
+1227
-9
No files matched your search
@@ -32,6 +32,7 @@ import { backupRoute } from "./routes/backup";
|
||||
import { businessAnalyticsRoute } from "./routes/businessAnalytics";
|
||||
import { toolsRoute } from "./routes/tools";
|
||||
import { crmRoute } from "./routes/crm";
|
||||
import { brandKnowledgeRoute } from "./routes/brandKnowledge";
|
||||
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";
|
||||
@@ -139,7 +140,7 @@ const app = new Elysia()
|
||||
return file;
|
||||
})
|
||||
.group("/api", (app) =>
|
||||
app.use(authRoute).use(categoriesRoute).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)
|
||||
app.use(authRoute).use(categoriesRoute).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)
|
||||
)
|
||||
.listen({ port: PORT, hostname: "0.0.0.0" });
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import prisma from "../lib/prisma";
|
||||
import { adminOnly } from "../middleware/auth";
|
||||
|
||||
export const DEFAULT_AKERIA_KNOWLEDGE = {
|
||||
brandName: "AKÉRIA",
|
||||
tagline: "Soft Asian Elegance & Mindful Beauty Sanctuary",
|
||||
story: `AKÉRIA is a modern beauty e-commerce and lifestyle brand born from a reverence for soft Asian/Japanese-inspired serenity.
|
||||
|
||||
We believe that beauty and well-being are intimate daily rituals of self-restoration. Every product is thoughtfully curated to transform ordinary spaces and skincare routines into luminous, tranquil sanctuaries. Our design and philosophy balance modern minimalism with gentle, time-honored botanical sensibilities.`,
|
||||
targetAudience: `• Primary Persona: Modern women & young professionals (ages 20–38) seeking refined, gentle, and aesthetic lifestyle products.
|
||||
• Demographics: Urban, design-conscious, value self-care rituals, clean beauty, and sensory living.
|
||||
• Psychographics: Drawn to Japanese/Korean soft minimalism, pastel elegance, gentle glow, and mindful calm.
|
||||
• Pain Points: Tired of overly aggressive, dark, or cluttered beauty branding; desires products that feel like a breath of fresh air.`,
|
||||
toneOfVoice: `• Core Personality: Bright, feminine, clean, fresh, premium-friendly, elegant, youthful, soft, luminous.
|
||||
• Language Style: Warm, welcoming, sensory-rich, gentle, and polished. Uses evocative yet accessible phrasing.
|
||||
• Words & Phrases We Love: Serenity, luminous glow, gentle ritual, mindful calm, airy softness, blossom, delicate harmony.
|
||||
• Words & Phrases We Avoid: Hyperbolic sales hype ("cheapest", "best ever"), harsh slang, aggressive discount urgency, dark/cynical humor.`,
|
||||
visualArtPrompt: `You are the Visual Art Director for AKÉRIA.
|
||||
|
||||
AKÉRIA is a modern beauty e-commerce and lifestyle brand with a soft Asian/Japanese-inspired sensibility.
|
||||
|
||||
BRAND MOOD
|
||||
Bright, feminine, clean, fresh, premium-friendly, elegant, youthful, soft, luminous.
|
||||
|
||||
COLOR PALETTE
|
||||
Primary:
|
||||
- Coral Rose #F28F96
|
||||
- Sakura Blush #F5C2C9
|
||||
- Warm Ivory #F7EFE6
|
||||
|
||||
Secondary:
|
||||
- Soft Lavender #DCCDE3
|
||||
- Soft Peach #F4D4C2
|
||||
- Pure White #FFFFFF
|
||||
|
||||
Contrast:
|
||||
- Charcoal #2D2D2D
|
||||
|
||||
COLOR DISTRIBUTION
|
||||
- 55–65% Warm Ivory / White
|
||||
- 15–20% Coral Rose / Sakura Blush
|
||||
- 10–15% Soft Peach / Lavender
|
||||
- 5–10% Charcoal for contrast
|
||||
|
||||
LIGHTING
|
||||
Soft diffused beauty lighting.
|
||||
Bright but not overexposed.
|
||||
Warm-neutral white balance.
|
||||
Luminous skin.
|
||||
Soft highlights.
|
||||
Low-to-medium contrast.
|
||||
Fresh commercial beauty campaign feel.
|
||||
|
||||
PHOTOGRAPHY STYLE
|
||||
Modern Asian beauty editorial.
|
||||
Clean skin texture.
|
||||
Natural glossy makeup.
|
||||
Soft floral details.
|
||||
Elegant product styling.
|
||||
Light airy composition.
|
||||
Premium e-commerce campaign photography.
|
||||
|
||||
MATERIALS & TEXTURES
|
||||
Frosted glass.
|
||||
Clear acrylic.
|
||||
Soft satin.
|
||||
Glossy cosmetic packaging.
|
||||
Light marble.
|
||||
Soft petals.
|
||||
Minimal reflective surfaces.
|
||||
|
||||
BACKGROUND
|
||||
Prefer:
|
||||
Warm ivory.
|
||||
Soft blush gradient.
|
||||
White.
|
||||
Very pale peach.
|
||||
Subtle lavender.
|
||||
|
||||
WARDROBE
|
||||
Prefer:
|
||||
White.
|
||||
Ivory.
|
||||
Blush.
|
||||
Soft beige.
|
||||
Dusty rose.
|
||||
Pale lavender.
|
||||
|
||||
ACCENT ELEMENTS
|
||||
Use cherry blossom, soft petals, subtle glow, fine sparkles, or clean beauty props.
|
||||
Keep these subtle and refined.
|
||||
|
||||
AVOID
|
||||
Hot pink.
|
||||
Neon colors.
|
||||
Strong red.
|
||||
Electric blue.
|
||||
Bright green.
|
||||
Strong yellow.
|
||||
Dark dramatic backgrounds.
|
||||
Heavy black.
|
||||
Cyberpunk lighting.
|
||||
Overly luxurious gold.
|
||||
Overly mature fashion styling.
|
||||
|
||||
IMAGE COMPOSITION
|
||||
Keep generous negative space for typography or website UI.
|
||||
Use clean commercial framing.
|
||||
Hero subject should feel approachable, polished, and aspirational.
|
||||
|
||||
IMPORTANT
|
||||
All backgrounds, props, flowers, clothing, packaging accents, and decorative elements must remain within the AKÉRIA color palette.
|
||||
|
||||
The final image must immediately feel like AKÉRIA:
|
||||
bright beauty commerce, soft Asian elegance, modern, friendly, and premium.`,
|
||||
productUsps: `• Hero Product Lines: Pure Aroma & Botanical Oils, Reed Diffusers, Ambient Room Sprays, Gentle Skincare Rituals.
|
||||
• Core Differentiators: 100% gentle botanical extractions, alcohol-free and safe for sensitive spaces.
|
||||
• Formulations: Cruelty-free, clean ingredients, sustainable frosted glass packaging.
|
||||
• Emotional Outcome: Instant sense of relaxation, soft luxurious ambiance, mindful mental clarity.`,
|
||||
marketingGuidelines: `• TikTok: Short ASMR unboxing, soft aesthetic morning routines, relaxing evening wind-downs, visual calm reels.
|
||||
• Instagram: Clean feed aesthetic, generous white space, soft sakura/ivory color theme, product macro shots with soft natural light.
|
||||
• Website Copy: Storytelling focus, serene headlines, sensory product descriptions, clear value propositions.
|
||||
• Calls-to-Action (CTAs): "Discover Your Ritual", "Explore the Collection", "Embrace Serenity", "Begin Your Glow".
|
||||
• Core Hashtags: #AKERIA #SoftBeauty #AromaRitual #MindfulLiving #AsianElegance #CleanAesthetic`,
|
||||
customRules: `1. Always keep visual and verbal assets strictly within the soft Asian/Japanese beauty aesthetic.
|
||||
2. Never introduce harsh contrast, neon highlights, or aggressive dark palettes.
|
||||
3. Every response or content generated should feel polite, premium, approachable, and calming.`
|
||||
};
|
||||
|
||||
export function compileBrandPrompt(knowledge: any): string {
|
||||
if (knowledge.masterPromptOverride && knowledge.masterPromptOverride.trim()) {
|
||||
return knowledge.masterPromptOverride.trim();
|
||||
}
|
||||
|
||||
const sections: string[] = [];
|
||||
|
||||
sections.push(`# BRAND KNOWLEDGE DIRECTIVE: ${knowledge.brandName || "AKÉRIA"}`);
|
||||
if (knowledge.tagline) {
|
||||
sections.push(`**Tagline:** ${knowledge.tagline}`);
|
||||
}
|
||||
|
||||
if (knowledge.story) {
|
||||
sections.push(`## 1. BRAND STORY & CORE PHILOSOPHY\n${knowledge.story.trim()}`);
|
||||
}
|
||||
|
||||
if (knowledge.targetAudience) {
|
||||
sections.push(`## 2. TARGET AUDIENCE & PERSONA\n${knowledge.targetAudience.trim()}`);
|
||||
}
|
||||
|
||||
if (knowledge.toneOfVoice) {
|
||||
sections.push(`## 3. TONE OF VOICE & MESSAGING STYLE\n${knowledge.toneOfVoice.trim()}`);
|
||||
}
|
||||
|
||||
if (knowledge.visualArtPrompt) {
|
||||
sections.push(`## 4. VISUAL ART DIRECTION & PHOTOGRAPHY DIRECTIVE\n${knowledge.visualArtPrompt.trim()}`);
|
||||
}
|
||||
|
||||
if (knowledge.productUsps) {
|
||||
sections.push(`## 5. PRODUCTS, SIGNATURE NOTES & USPs\n${knowledge.productUsps.trim()}`);
|
||||
}
|
||||
|
||||
if (knowledge.marketingGuidelines) {
|
||||
sections.push(`## 6. MARKETING CHANNELS & CONTENT GUIDELINES\n${knowledge.marketingGuidelines.trim()}`);
|
||||
}
|
||||
|
||||
if (knowledge.customRules) {
|
||||
sections.push(`## 7. STRICT BRAND COMPLIANCE & RULES\n${knowledge.customRules.trim()}`);
|
||||
}
|
||||
|
||||
sections.push(`---\n*This document is the single source of truth for all LLM, AI Image, and Copywriting tasks representing ${knowledge.brandName || "AKÉRIA"}.*`);
|
||||
|
||||
return sections.join("\n\n");
|
||||
}
|
||||
|
||||
export const brandKnowledgeRoute = new Elysia({ prefix: "/brand-knowledge" })
|
||||
.get("/", async () => {
|
||||
let knowledge = await prisma.brandKnowledge.findFirst();
|
||||
if (!knowledge) {
|
||||
knowledge = await prisma.brandKnowledge.create({
|
||||
data: DEFAULT_AKERIA_KNOWLEDGE,
|
||||
});
|
||||
}
|
||||
return {
|
||||
...knowledge,
|
||||
compiledPrompt: compileBrandPrompt(knowledge),
|
||||
};
|
||||
})
|
||||
.get("/compiled-prompt", async () => {
|
||||
let knowledge = await prisma.brandKnowledge.findFirst();
|
||||
if (!knowledge) {
|
||||
knowledge = await prisma.brandKnowledge.create({
|
||||
data: DEFAULT_AKERIA_KNOWLEDGE,
|
||||
});
|
||||
}
|
||||
return {
|
||||
brandName: knowledge.brandName,
|
||||
compiledPrompt: compileBrandPrompt(knowledge),
|
||||
updatedAt: knowledge.updatedAt,
|
||||
};
|
||||
})
|
||||
.use(adminOnly)
|
||||
.put(
|
||||
"/",
|
||||
async ({ body, set }) => {
|
||||
try {
|
||||
const data = body as any;
|
||||
let knowledge = await prisma.brandKnowledge.findFirst();
|
||||
|
||||
if (!knowledge) {
|
||||
knowledge = await prisma.brandKnowledge.create({
|
||||
data: {
|
||||
...DEFAULT_AKERIA_KNOWLEDGE,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
knowledge = await prisma.brandKnowledge.update({
|
||||
where: { id: knowledge.id },
|
||||
data: {
|
||||
brandName: data.brandName !== undefined ? data.brandName : knowledge.brandName,
|
||||
tagline: data.tagline !== undefined ? data.tagline : knowledge.tagline,
|
||||
story: data.story !== undefined ? data.story : knowledge.story,
|
||||
targetAudience: data.targetAudience !== undefined ? data.targetAudience : knowledge.targetAudience,
|
||||
toneOfVoice: data.toneOfVoice !== undefined ? data.toneOfVoice : knowledge.toneOfVoice,
|
||||
visualArtPrompt: data.visualArtPrompt !== undefined ? data.visualArtPrompt : knowledge.visualArtPrompt,
|
||||
productUsps: data.productUsps !== undefined ? data.productUsps : knowledge.productUsps,
|
||||
marketingGuidelines: data.marketingGuidelines !== undefined ? data.marketingGuidelines : knowledge.marketingGuidelines,
|
||||
customRules: data.customRules !== undefined ? data.customRules : knowledge.customRules,
|
||||
masterPromptOverride: data.masterPromptOverride !== undefined ? data.masterPromptOverride : knowledge.masterPromptOverride,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...knowledge,
|
||||
compiledPrompt: compileBrandPrompt(knowledge),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("[BrandKnowledge] Update error:", err);
|
||||
set.status = 500;
|
||||
return { success: false, error: "Failed to update brand knowledge" };
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
brandName: t.Optional(t.String()),
|
||||
tagline: t.Optional(t.Nullable(t.String())),
|
||||
story: t.Optional(t.Nullable(t.String())),
|
||||
targetAudience: t.Optional(t.Nullable(t.String())),
|
||||
toneOfVoice: t.Optional(t.Nullable(t.String())),
|
||||
visualArtPrompt: t.Optional(t.Nullable(t.String())),
|
||||
productUsps: t.Optional(t.Nullable(t.String())),
|
||||
marketingGuidelines: t.Optional(t.Nullable(t.String())),
|
||||
customRules: t.Optional(t.Nullable(t.String())),
|
||||
masterPromptOverride: t.Optional(t.Nullable(t.String())),
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -18,6 +18,7 @@ export async function saveStoreSettingsTranslations(settingsId: number, payload:
|
||||
if (item.newsletterButtonText !== undefined) updateData.newsletterButtonText = item.newsletterButtonText;
|
||||
if (item.defaultSeoTitle !== undefined) updateData.defaultSeoTitle = item.defaultSeoTitle;
|
||||
if (item.defaultSeoDescription !== undefined) updateData.defaultSeoDescription = item.defaultSeoDescription;
|
||||
if (item.brandPrompt !== undefined) updateData.brandPrompt = item.brandPrompt;
|
||||
|
||||
const existing = await prisma.storeSettingsTranslation.findUnique({
|
||||
where: { storeSettingsId_langCode: { storeSettingsId: settingsId, langCode: lang } }
|
||||
@@ -82,6 +83,12 @@ export async function saveStoreSettingsTranslations(settingsId: number, payload:
|
||||
? (lang === 'th' ? payload.defaultSeoDescription : undefined)
|
||||
: payload.defaultSeoDescription?.[lang];
|
||||
|
||||
const brandPrompt = typeof langObj?.brandPrompt === 'string' || langObj?.brandPrompt === null
|
||||
? langObj.brandPrompt
|
||||
: typeof payload.brandPrompt === 'string' || payload.brandPrompt === null
|
||||
? (lang === 'th' ? payload.brandPrompt : undefined)
|
||||
: payload.brandPrompt?.[lang];
|
||||
|
||||
if (
|
||||
footerDescription === undefined &&
|
||||
newsletterTitle === undefined &&
|
||||
@@ -89,7 +96,8 @@ export async function saveStoreSettingsTranslations(settingsId: number, payload:
|
||||
newsletterPlaceholder === undefined &&
|
||||
newsletterButtonText === undefined &&
|
||||
defaultSeoTitle === undefined &&
|
||||
defaultSeoDescription === undefined
|
||||
defaultSeoDescription === undefined &&
|
||||
brandPrompt === undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -102,6 +110,7 @@ export async function saveStoreSettingsTranslations(settingsId: number, payload:
|
||||
if (newsletterButtonText !== undefined) updateData.newsletterButtonText = newsletterButtonText;
|
||||
if (defaultSeoTitle !== undefined) updateData.defaultSeoTitle = defaultSeoTitle;
|
||||
if (defaultSeoDescription !== undefined) updateData.defaultSeoDescription = defaultSeoDescription;
|
||||
if (brandPrompt !== undefined) updateData.brandPrompt = brandPrompt;
|
||||
|
||||
const existing = await prisma.storeSettingsTranslation.findUnique({
|
||||
where: { storeSettingsId_langCode: { storeSettingsId: settingsId, langCode: lang } }
|
||||
@@ -155,6 +164,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
||||
const localizedDescription = translation?.footerDescription;
|
||||
const localizedSeoTitle = translation?.defaultSeoTitle;
|
||||
const localizedSeoDescription = translation?.defaultSeoDescription;
|
||||
const localizedBrandPrompt = translation?.brandPrompt;
|
||||
|
||||
const headerBehavior = (settings.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll';
|
||||
const mobileHeaderBehavior = (settings.themeTokens as any)?.mobileHeaderBehavior ?? headerBehavior;
|
||||
@@ -169,6 +179,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
||||
defaultSeoTitle: localizedSeoTitle ?? settingsData.defaultSeoTitle,
|
||||
defaultSeoDescription: localizedSeoDescription ?? settingsData.defaultSeoDescription,
|
||||
defaultOgImage: settingsData.defaultOgImage,
|
||||
brandPrompt: localizedBrandPrompt ?? settingsData.brandPrompt,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("[Settings] Error:", err);
|
||||
@@ -342,6 +353,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
||||
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
|
||||
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
|
||||
defaultOgImage: t.Optional(t.Nullable(t.String())),
|
||||
brandPrompt: t.Optional(t.Nullable(t.String())),
|
||||
translations: t.Optional(t.Any()),
|
||||
})
|
||||
})
|
||||
@@ -426,6 +438,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
||||
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
|
||||
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
|
||||
defaultOgImage: t.Optional(t.Nullable(t.String())),
|
||||
brandPrompt: t.Optional(t.Nullable(t.String())),
|
||||
translations: t.Optional(t.Any()),
|
||||
})
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user