This commit is contained in:
Your Name committed 2026-09-29 12:48:52 +07:00
1 parent 683f54fd95
commit 44a613f8ec
12 files changed
+1227 -9

No files matched your search

+3
View File
@@ -18,6 +18,7 @@ import ProtectedRoute from './components/ProtectedRoute'
import OrdersPage from './pages/OrdersPage'
import OrderDetailPage from './pages/OrderDetailPage'
import StoreSettingsPage from './pages/StoreSettingsPage'
import BrandIntelligencePage from './pages/BrandIntelligencePage'
import PaymentSettingsPage from './pages/PaymentSettingsPage'
import ThemeSettingsPage from './pages/ThemeSettingsPage'
import ContentSettingsPage from './pages/ContentSettingsPage'
@@ -113,6 +114,8 @@ function App() {
<Route path="/coupons" element={<CouponsPage />} />
<Route path="/raw-materials" element={<RawMaterialsPage />} />
<Route path="/settings" element={<StoreSettingsPage />} />
<Route path="/settings/brand-intelligence" element={<BrandIntelligencePage />} />
<Route path="/brand-intelligence" element={<BrandIntelligencePage />} />
<Route path="/settings/payments" element={<PaymentSettingsPage />} />
<Route path="/settings/theme" element={<ThemeSettingsPage />} />
<Route path="/settings/content" element={<ContentSettingsPage />} />
+8 -5
View File
@@ -37,6 +37,7 @@ import {
FolderArchive,
Map,
Target,
BrainCircuit,
} from 'lucide-react'
import { useStoreSettings } from '../contexts/StoreSettingsContext'
import { clearAdminAuth } from '../utils/auth'
@@ -140,6 +141,7 @@ export default function Sidebar({ isCollapsed, onToggle }: SidebarProps) {
const CMS_MENU = [
{ icon: Layers, label: t('nav.pageBuilder', 'Page Builder (CMS)'), to: '/page-builder' },
{ icon: BrainCircuit, label: t('nav.brandIntelligence', 'Brand AI Intelligence'), to: '/brand-intelligence' },
{ icon: Sparkles, label: t('nav.cmsBlockShowcase', 'CMS Block Showcase'), to: '/cms-mockup' },
{ icon: ImageIcon, label: t('nav.mediaLibrary', 'Media Library'), to: '/media' },
{ icon: BookOpen, label: t('nav.scentGuide', 'Scent Guide'), to: '/articles' },
@@ -169,11 +171,12 @@ export default function Sidebar({ isCollapsed, onToggle }: SidebarProps) {
]
const SETTINGS_MENU = [
{ icon: Store, label: t('nav.storeSettings', 'Store Settings'), to: '/settings' },
{ icon: CreditCard, label: t('nav.paymentSettings', 'Payment & Shipping'), to: '/settings/payments' },
{ icon: Palette, label: t('nav.themeSettings', 'Theme & Design'), to: '/settings/theme' },
{ icon: FileText, label: t('nav.contentSettings', 'Content'), to: '/settings/content' },
{ icon: Wrench, label: t('nav.systemMaintenance', 'System'), to: '/settings/system' },
{ icon: Store, label: t('nav.storeSettings', 'Store Settings'), to: '/settings' },
{ icon: BrainCircuit, label: t('nav.brandIntelligence', 'Brand AI Intelligence'), to: '/settings/brand-intelligence' },
{ icon: CreditCard, label: t('nav.paymentSettings', 'Payment & Shipping'), to: '/settings/payments' },
{ icon: Palette, label: t('nav.themeSettings', 'Theme & Design'), to: '/settings/theme' },
{ icon: FileText, label: t('nav.contentSettings', 'Content'), to: '/settings/content' },
{ icon: Wrench, label: t('nav.systemMaintenance', 'System'), to: '/settings/system' },
]
const handleLogout = () => {
@@ -0,0 +1,616 @@
import { useState, useEffect } from 'react'
import { API_BASE, type BrandKnowledge } from '@homaroma/types'
import {
BrainCircuit,
Save,
Loader2,
Copy,
Check,
Wand2,
Sparkles,
CheckCircle2,
Palette,
Users,
Feather,
PackageCheck,
Send,
Code2,
Download
} from 'lucide-react'
import { getAdminToken } from '../utils/auth'
type ActiveTab = 'identity' | 'visual' | 'products' | 'marketing' | 'compiled'
export default function BrandIntelligencePage() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [saveSuccess, setSaveSuccess] = useState(false)
const [copiedType, setCopiedType] = useState<'md' | 'json' | null>(null)
const [activeTab, setActiveTab] = useState<ActiveTab>('identity')
// Form State
const [brandName, setBrandName] = useState('AKÉRIA')
const [tagline, setTagline] = useState('')
const [story, setStory] = useState('')
const [targetAudience, setTargetAudience] = useState('')
const [toneOfVoice, setToneOfVoice] = useState('')
const [visualArtPrompt, setVisualArtPrompt] = useState('')
const [productUsps, setProductUsps] = useState('')
const [marketingGuidelines, setMarketingGuidelines] = useState('')
const [customRules, setCustomRules] = useState('')
const [masterPromptOverride, setMasterPromptOverride] = useState('')
useEffect(() => {
fetch(`${API_BASE}/brand-knowledge`)
.then((r) => r.json())
.then((data: BrandKnowledge) => {
setBrandName(data.brandName || 'AKÉRIA')
setTagline(data.tagline || '')
setStory(data.story || '')
setTargetAudience(data.targetAudience || '')
setToneOfVoice(data.toneOfVoice || '')
setVisualArtPrompt(data.visualArtPrompt || '')
setProductUsps(data.productUsps || '')
setMarketingGuidelines(data.marketingGuidelines || '')
setCustomRules(data.customRules || '')
setMasterPromptOverride(data.masterPromptOverride || '')
})
.catch(console.error)
.finally(() => setLoading(false))
}, [])
// Dynamically compile preview on client
const liveCompiledPrompt = masterPromptOverride.trim()
? masterPromptOverride.trim()
: [
`# BRAND KNOWLEDGE DIRECTIVE: ${brandName || 'AKÉRIA'}`,
tagline ? `**Tagline:** ${tagline}` : null,
story ? `## 1. BRAND STORY & CORE PHILOSOPHY\n${story.trim()}` : null,
targetAudience ? `## 2. TARGET AUDIENCE & PERSONA\n${targetAudience.trim()}` : null,
toneOfVoice ? `## 3. TONE OF VOICE & MESSAGING STYLE\n${toneOfVoice.trim()}` : null,
visualArtPrompt ? `## 4. VISUAL ART DIRECTION & PHOTOGRAPHY DIRECTIVE\n${visualArtPrompt.trim()}` : null,
productUsps ? `## 5. PRODUCTS, SIGNATURE NOTES & USPs\n${productUsps.trim()}` : null,
marketingGuidelines ? `## 6. MARKETING CHANNELS & CONTENT GUIDELINES\n${marketingGuidelines.trim()}` : null,
customRules ? `## 7. STRICT BRAND COMPLIANCE & RULES\n${customRules.trim()}` : null,
`---\n*This document is the single source of truth for all LLM, AI Image, and Copywriting tasks representing ${brandName || 'AKÉRIA'}.*`,
]
.filter(Boolean)
.join('\n\n')
const handleCopyMarkdown = async () => {
try {
await navigator.clipboard.writeText(liveCompiledPrompt)
setCopiedType('md')
setTimeout(() => setCopiedType(null), 2500)
} catch {
// ignore
}
}
const handleCopyJson = async () => {
try {
const payload = {
brandName,
tagline,
story,
targetAudience,
toneOfVoice,
visualArtPrompt,
productUsps,
marketingGuidelines,
customRules,
compiledPrompt: liveCompiledPrompt,
}
await navigator.clipboard.writeText(JSON.stringify(payload, null, 2))
setCopiedType('json')
setTimeout(() => setCopiedType(null), 2500)
} catch {
// ignore
}
}
const handleDownloadMarkdown = () => {
const blob = new Blob([liveCompiledPrompt], { type: 'text/markdown;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${(brandName || 'brand').toLowerCase().replace(/\s+/g, '_')}_ai_knowledge.md`
a.click()
URL.revokeObjectURL(url)
}
const handleSave = async (e?: React.FormEvent) => {
if (e) e.preventDefault()
setSaving(true)
setSaveSuccess(false)
try {
const payload = {
brandName,
tagline: tagline || null,
story: story || null,
targetAudience: targetAudience || null,
toneOfVoice: toneOfVoice || null,
visualArtPrompt: visualArtPrompt || null,
productUsps: productUsps || null,
marketingGuidelines: marketingGuidelines || null,
customRules: customRules || null,
masterPromptOverride: masterPromptOverride || null,
}
const token = getAdminToken()
const res = await fetch(`${API_BASE}/brand-knowledge`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(payload),
})
const data = await res.json()
if (res.ok && data.success) {
setSaveSuccess(true)
setTimeout(() => setSaveSuccess(false), 3500)
} else {
alert(data.error || 'Failed to save brand knowledge')
}
} catch (err) {
console.error('Save brand knowledge error:', err)
alert('Failed to save brand knowledge')
} finally {
setSaving(false)
}
}
if (loading) {
return (
<div className="flex items-center justify-center p-20">
<Loader2 className="w-8 h-8 text-purple-400 animate-spin" />
</div>
)
}
return (
<div className="max-w-5xl space-y-8 animate-fade-in text-slate-100 pb-20">
{/* Page Header */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div className="flex items-center gap-3.5">
<div className="p-3 bg-purple-500/10 border border-purple-500/20 rounded-2xl shadow-lg shadow-purple-950/30">
<BrainCircuit className="w-7 h-7 text-purple-400" />
</div>
<div>
<div className="flex items-center gap-2">
<h1 className="text-3xl font-extrabold tracking-tight text-white">Brand AI Intelligence</h1>
<span className="px-2.5 py-0.5 rounded-full bg-purple-500/20 text-purple-300 text-[10px] font-extrabold uppercase tracking-wider border border-purple-500/30">
LLM Knowledge Base
</span>
</div>
<p className="text-slate-400 text-xs sm:text-sm mt-0.5">
ศูนย์รวมองค์ความรู้ ตัวตน กลิ่น สินค้า และ Art Direction เพื่อส่งต่อให้ AI & Copywriters ทุกระบบ
</p>
</div>
</div>
{/* Global Action Buttons */}
<div className="flex flex-wrap items-center gap-2.5">
<button
type="button"
onClick={handleCopyMarkdown}
className={`px-3.5 py-2 rounded-xl text-xs font-bold flex items-center gap-1.5 transition-all border cursor-pointer ${
copiedType === 'md'
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-400'
: 'bg-white/5 hover:bg-white/10 border-white/10 text-slate-300 hover:text-white'
}`}
title="คัดลอก Master Prompt ในรูปแบบ Markdown สำหรับป้อน ChatGPT / Claude / Gemini"
>
{copiedType === 'md' ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
<span>{copiedType === 'md' ? 'Copied Markdown!' : 'Copy for LLM'}</span>
</button>
<button
type="button"
onClick={handleCopyJson}
className={`px-3.5 py-2 rounded-xl text-xs font-bold flex items-center gap-1.5 transition-all border cursor-pointer ${
copiedType === 'json'
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-400'
: 'bg-white/5 hover:bg-white/10 border-white/10 text-slate-300 hover:text-white'
}`}
title="คัดลอกในรูปแบบ JSON Schema"
>
{copiedType === 'json' ? <Check className="w-3.5 h-3.5" /> : <Code2 className="w-3.5 h-3.5" />}
<span>JSON</span>
</button>
<button
type="button"
onClick={handleSave}
disabled={saving}
className="px-5 py-2 bg-purple-600 hover:bg-purple-500 active:scale-[0.99] disabled:opacity-50 text-white font-bold rounded-xl flex items-center gap-2 transition-all shadow-lg shadow-purple-950/40 text-xs cursor-pointer"
>
{saving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
<span>บันทึกความรู้แบรนด์</span>
</button>
</div>
</div>
{saveSuccess && (
<div className="flex items-center gap-2 px-4 py-3 bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 rounded-xl text-xs font-bold animate-fade-in shadow-lg">
<CheckCircle2 className="w-4 h-4 shrink-0 text-emerald-400" />
<span>บันทึกข้อมูล Brand AI Intelligence เรียบร้อยแล้ว พร้อมส่งต่อไปยัง AI System Context ทันที!</span>
</div>
)}
{/* Modern Tabs Header */}
<div className="bg-night-900/80 p-1.5 rounded-2xl border border-white/10 flex flex-wrap gap-1.5 shadow-xl backdrop-blur-md">
<button
type="button"
onClick={() => setActiveTab('identity')}
className={`flex-1 min-w-[150px] py-2.5 px-3.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all cursor-pointer ${
activeTab === 'identity'
? 'bg-purple-600 text-white shadow-lg shadow-purple-900/40'
: 'text-slate-400 hover:text-white hover:bg-white/5'
}`}
>
<Feather className="w-4 h-4" />
<span>1. Identity & Voice</span>
</button>
<button
type="button"
onClick={() => setActiveTab('visual')}
className={`flex-1 min-w-[150px] py-2.5 px-3.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all cursor-pointer ${
activeTab === 'visual'
? 'bg-purple-600 text-white shadow-lg shadow-purple-900/40'
: 'text-slate-400 hover:text-white hover:bg-white/5'
}`}
>
<Palette className="w-4 h-4" />
<span>2. Art & Visuals</span>
</button>
<button
type="button"
onClick={() => setActiveTab('products')}
className={`flex-1 min-w-[150px] py-2.5 px-3.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all cursor-pointer ${
activeTab === 'products'
? 'bg-purple-600 text-white shadow-lg shadow-purple-900/40'
: 'text-slate-400 hover:text-white hover:bg-white/5'
}`}
>
<PackageCheck className="w-4 h-4" />
<span>3. Products & USPs</span>
</button>
<button
type="button"
onClick={() => setActiveTab('marketing')}
className={`flex-1 min-w-[150px] py-2.5 px-3.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all cursor-pointer ${
activeTab === 'marketing'
? 'bg-purple-600 text-white shadow-lg shadow-purple-900/40'
: 'text-slate-400 hover:text-white hover:bg-white/5'
}`}
>
<Send className="w-4 h-4" />
<span>4. Marketing & Rules</span>
</button>
<button
type="button"
onClick={() => setActiveTab('compiled')}
className={`flex-1 min-w-[160px] py-2.5 px-3.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all cursor-pointer ${
activeTab === 'compiled'
? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg'
: 'text-purple-300 hover:text-white hover:bg-purple-500/10'
}`}
>
<Sparkles className="w-4 h-4 text-amber-300" />
<span>5. Master LLM Context</span>
</button>
</div>
{/* TAB CONTENT 1: IDENTITY & VOICE */}
{activeTab === 'identity' && (
<div className="space-y-6 animate-fade-in">
<div className="admin-card p-6 sm:p-8 space-y-6 border-white/10 shadow-2xl">
<div className="flex items-center justify-between border-b border-white/10 pb-4">
<div>
<h3 className="text-base font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Feather className="w-4 h-4 text-purple-400" /> Brand Core Identity & Philosophy
</h3>
<p className="text-xs text-slate-400 mt-0.5">
ระบุชื่อแบรนด์ สโลแกน และเรื่องราวที่มา เพื่อให้ LLM สร้างบทความหรือแคปชันที่สะท้อนคุณค่าของแบรนด์
</p>
</div>
</div>
<div className="grid sm:grid-cols-2 gap-5">
<div className="space-y-2">
<label className="text-[11px] uppercase font-bold tracking-widest text-slate-400">
Brand Name (ชื่อแบรนด์)
</label>
<input
type="text"
value={brandName}
onChange={(e) => setBrandName(e.target.value)}
className="admin-input w-full font-bold text-white"
placeholder="AKÉRIA"
/>
</div>
<div className="space-y-2">
<label className="text-[11px] uppercase font-bold tracking-widest text-slate-400">
Brand Tagline (สโลแกนประจำแบรนด์)
</label>
<input
type="text"
value={tagline}
onChange={(e) => setTagline(e.target.value)}
className="admin-input w-full text-slate-200"
placeholder="Soft Asian Elegance & Mindful Beauty Sanctuary"
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-[11px] uppercase font-bold tracking-widest text-slate-400">
Brand Story & Core Mission (เรื่องราวและพันธกิจของแบรนด์)
</label>
<span className="text-[10px] text-slate-500 font-mono">{story.length} chars</span>
</div>
<textarea
value={story}
onChange={(e) => setStory(e.target.value)}
rows={6}
className="admin-input w-full text-xs leading-relaxed p-4 bg-night-950/80 resize-y font-sans"
placeholder="อธิบายที่มาของแบรนด์ แรงบันดาลใจ และสิ่งที่แบรนด์ต้องการมอบคุณค่าให้แก่ลูกค้า..."
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-6">
{/* Target Audience */}
<div className="admin-card p-6 space-y-4 border-white/10 shadow-xl">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<h4 className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Users className="w-4 h-4 text-purple-400" /> Target Persona & Audience
</h4>
<span className="text-[10px] text-slate-500 font-mono">{targetAudience.length} chars</span>
</div>
<p className="text-[11px] text-slate-400">
กลุ่มลูกค้าเป้าหมาย ใครคือคนที่จะหลงรักแบรนด์นี้ (อายุ, พฤติกรรม, ความสนใจ, Pain points)
</p>
<textarea
value={targetAudience}
onChange={(e) => setTargetAudience(e.target.value)}
rows={8}
className="admin-input w-full text-xs leading-relaxed p-3.5 bg-night-950/80 resize-y font-sans"
placeholder="• Persona: หญิงสาววัยทำงาน 22-35 ปี&#10;• ไลฟ์สไตล์: ชอบความมินิมอล การพักผ่อน และสกินแคร์/อโรมากลิ่นละมุน..."
/>
</div>
{/* Tone of Voice */}
<div className="admin-card p-6 space-y-4 border-white/10 shadow-xl">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<h4 className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Feather className="w-4 h-4 text-purple-400" /> Tone of Voice & Messaging Style
</h4>
<span className="text-[10px] text-slate-500 font-mono">{toneOfVoice.length} chars</span>
</div>
<p className="text-[11px] text-slate-400">
น้ำเสียง ภาษาที่ใช้ คำที่ชอบ vs คำต้องห้าม (เช่น ห้ามใช้คำโอ้อวดเกินจริง, สุภาพอบอุ่น)
</p>
<textarea
value={toneOfVoice}
onChange={(e) => setToneOfVoice(e.target.value)}
rows={8}
className="admin-input w-full text-xs leading-relaxed p-3.5 bg-night-950/80 resize-y font-sans"
placeholder="• บุคลิก: อบอุ่น สุภาพ เป็นมิตร พรีเมียม อ่อนโยน&#10;• คำที่ชอบ: Serenity, Luminous glow, Mindful calm&#10;• คำต้องห้าม: ถูกที่สุด, การันตี 100%, ภาษาหยาบคาย..."
/>
</div>
</div>
</div>
)}
{/* TAB CONTENT 2: VISUAL ART DIRECTION */}
{activeTab === 'visual' && (
<div className="space-y-6 animate-fade-in">
<div className="admin-card p-6 sm:p-8 space-y-6 border-white/10 shadow-2xl">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-white/10 pb-4">
<div>
<h3 className="text-base font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Palette className="w-4 h-4 text-purple-400" /> Visual Art Direction & AI Prompt Directive
</h3>
<p className="text-xs text-slate-400 mt-0.5">
คำสั่ง Art Direction แบบสมบูรณ์ สำหรับสร้างภาพ Midjourney, Imagen, DALL-E หรือควบคุม Mood & Tone ภาพถ่าย
</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setVisualArtPrompt(visualArtPrompt)}
className="px-3 py-1.5 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 text-purple-300 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all cursor-pointer"
>
<Wand2 className="w-3.5 h-3.5" />
<span>AKÉRIA Art Directive</span>
</button>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-[11px] uppercase font-bold tracking-widest text-slate-400">
Master Visual Art Prompt (Markdown / Monospace)
</span>
<div className="flex items-center gap-3 text-[10px] font-mono text-slate-400">
<span>Lines: {visualArtPrompt.split('\n').length}</span>
<span>Chars: {visualArtPrompt.length.toLocaleString()}</span>
</div>
</div>
<textarea
value={visualArtPrompt}
onChange={(e) => setVisualArtPrompt(e.target.value)}
rows={18}
className="admin-input w-full font-mono text-xs leading-relaxed p-4 bg-night-950/90 border-white/10 focus:border-purple-500/50 rounded-xl resize-y text-slate-200 placeholder:text-slate-600 outline-none transition-all shadow-inner"
placeholder="You are the Visual Art Director for AKÉRIA... [Color Palette, Lighting, Photography Style, Materials, Avoid List]"
spellCheck={false}
/>
</div>
</div>
</div>
)}
{/* TAB CONTENT 3: PRODUCTS & USPS */}
{activeTab === 'products' && (
<div className="space-y-6 animate-fade-in">
<div className="admin-card p-6 sm:p-8 space-y-6 border-white/10 shadow-2xl">
<div className="border-b border-white/10 pb-4">
<h3 className="text-base font-bold text-white uppercase tracking-wider flex items-center gap-2">
<PackageCheck className="w-4 h-4 text-purple-400" /> Products, Signatures & Unique Selling Points (USPs)
</h3>
<p className="text-xs text-slate-400 mt-0.5">
จุดแข็งของสินค้า สารสกัด กลิ่นเอกลักษณ์ มาตรฐานความปลอดภัย และผลลัพธ์ทางอารมณ์ที่ลูกค้าจะได้รับ
</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-[11px] uppercase font-bold tracking-widest text-slate-400">
Product Lines, Ingredients & Quality Standards
</label>
<span className="text-[10px] text-slate-500 font-mono">{productUsps.length} chars</span>
</div>
<textarea
value={productUsps}
onChange={(e) => setProductUsps(e.target.value)}
rows={14}
className="admin-input w-full text-xs leading-relaxed p-4 bg-night-950/80 resize-y font-sans"
placeholder="• สินค้าหลัก: Pure Essential Oil, Reed Diffuser, Room Spray, Skincare Rituals&#10;• จุดเด่น: สารสกัดธรรมชาติ 100%, ปราศจากแอลกอฮอล์และสารเคมีอันตราย, Cruelty-free&#10;• อารมณ์ที่ได้รับ: ผ่อนคลาย จิตใจสงบ รู้สึกสดชื่นและมีพลังบวก..."
/>
</div>
</div>
</div>
)}
{/* TAB CONTENT 4: MARKETING & RULES */}
{activeTab === 'marketing' && (
<div className="space-y-6 animate-fade-in">
<div className="grid md:grid-cols-2 gap-6">
{/* Marketing Guidelines */}
<div className="admin-card p-6 space-y-4 border-white/10 shadow-xl">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<h4 className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Send className="w-4 h-4 text-purple-400" /> Content & Marketing Channels
</h4>
<span className="text-[10px] text-slate-500 font-mono">{marketingGuidelines.length} chars</span>
</div>
<p className="text-[11px] text-slate-400">
แนวทางเขียนคอนเทนต์เฉพาะแพลตฟอร์ม (TikTok, Instagram, Website, LINE OA, CTAs, Hashtags)
</p>
<textarea
value={marketingGuidelines}
onChange={(e) => setMarketingGuidelines(e.target.value)}
rows={14}
className="admin-input w-full text-xs leading-relaxed p-3.5 bg-night-950/80 resize-y font-sans"
placeholder="• TikTok: วิดีโอสั้น ASMR ปลอบประโลมใจ, unboxing สวยงาม&#10;• Instagram: คุมโทนสีพาสเทล นุ่มนวล คลีน&#10;• CTAs: สัมผัสความผ่อนคลายวันนี้, เริ่มต้นการดูแลตัวเองของคุณ&#10;• Hashtags: #AKERIA #SoftBeauty #AromaRitual"
/>
</div>
{/* Custom Rules & Compliance */}
<div className="admin-card p-6 space-y-4 border-white/10 shadow-xl">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<h4 className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Sparkles className="w-4 h-4 text-amber-400" /> Strict Rules & Compliance
</h4>
<span className="text-[10px] text-slate-500 font-mono">{customRules.length} chars</span>
</div>
<p className="text-[11px] text-slate-400">
กฎเหล็กและข้อบังคับพิเศษที่ AI หรือทีมงานต้องปฏิบัติตามเสมอในการผลิตสื่อทุกชิ้น
</p>
<textarea
value={customRules}
onChange={(e) => setCustomRules(e.target.value)}
rows={14}
className="admin-input w-full text-xs leading-relaxed p-3.5 bg-night-950/80 resize-y font-sans"
placeholder="1. รักษาความรู้สึก Soft Asian / Japanese Elegance ตลอดเวลา&#10;2. ห้ามใช้สีฉูดฉาดแบบนีออนเด็ดขาด&#10;3. ทุกข้อความต้องสร้างความรู้สึกผ่อนคลาย พรีเมียม และเข้าถึงง่าย"
/>
</div>
</div>
</div>
)}
{/* TAB CONTENT 5: COMPILED MASTER LLM CONTEXT */}
{activeTab === 'compiled' && (
<div className="space-y-6 animate-fade-in">
<div className="admin-card p-6 sm:p-8 space-y-6 border-purple-500/20 shadow-2xl bg-night-950/90">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-white/10 pb-4">
<div>
<h3 className="text-base font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Sparkles className="w-5 h-5 text-amber-400" /> Compiled Master System Prompt for LLMs
</h3>
<p className="text-xs text-slate-400 mt-0.5">
นี่คือข้อความรวมที่คอมไพล์จากทุกหมวดหมู่อัตโนมัติ พร้อมนำไปวางเป็น <b>System Instructions</b> ใน ChatGPT, Claude, Gemini หรือ Agent ของคุณ
</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={handleCopyMarkdown}
className={`px-3.5 py-1.5 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all border cursor-pointer ${
copiedType === 'md'
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-400'
: 'bg-purple-600 hover:bg-purple-500 text-white'
}`}
>
{copiedType === 'md' ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
<span>{copiedType === 'md' ? 'Copied Markdown!' : 'Copy Markdown'}</span>
</button>
<button
type="button"
onClick={handleDownloadMarkdown}
className="px-3 py-1.5 bg-white/5 hover:bg-white/10 border border-white/10 text-slate-300 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all cursor-pointer"
>
<Download className="w-3.5 h-3.5" />
<span>.md</span>
</button>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-[11px] text-slate-400">
<span className="uppercase font-bold tracking-wider">Live System Prompt Preview</span>
<span className="font-mono">{liveCompiledPrompt.length.toLocaleString()} Chars • {liveCompiledPrompt.split('\n').length} Lines</span>
</div>
<pre className="p-5 rounded-xl bg-black/60 border border-white/10 text-slate-200 font-mono text-xs leading-relaxed whitespace-pre-wrap max-h-[600px] overflow-y-auto shadow-inner select-all">
{liveCompiledPrompt}
</pre>
</div>
</div>
</div>
)}
{/* Bottom Save Bar */}
<div className="flex items-center justify-between pt-6 border-t border-white/10">
<div className="flex items-center gap-2 text-xs text-slate-400">
<Sparkles className="w-4 h-4 text-purple-400" />
<span>ข้อมูลที่บันทึกจะซิงค์ไปยัง API และพร้อมใช้งานในทุกโมดูล AI ของระบบ HomAroma / AKÉRIA</span>
</div>
<button
type="button"
onClick={handleSave}
disabled={saving}
className="btn-primary py-3 px-8 flex items-center gap-2 text-xs font-bold shadow-lg shadow-purple-900/30 cursor-pointer"
>
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
บันทึกการเปลี่ยนแปลง
</button>
</div>
</div>
)
}
+215 -2
View File
@@ -8,11 +8,115 @@ import {
Image as ImageIcon,
UploadCloud,
Sparkles,
CheckCircle2
CheckCircle2,
Palette,
Wand2,
Copy,
Check,
FileText
} from 'lucide-react';
import { useStoreSettings } from '../contexts/StoreSettingsContext';
import MediaLibraryModal from '../components/MediaLibraryModal';
export const DEFAULT_BRAND_PROMPT = `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.`;
const SUPPORTED_LOCALES = [
{ code: 'th', label: 'TH', full: 'ภาษาไทย (Default)' },
{ code: 'en', label: 'EN', full: 'English' },
@@ -29,11 +133,13 @@ export default function StoreSettingsPage() {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const [promptCopied, setPromptCopied] = useState(false);
// Store Brand & Identity
const [brandName, setBrandName] = useState('');
const [logoUrl, setLogoUrl] = useState('');
const [themePreset, setThemePreset] = useState('default');
const [brandPrompt, setBrandPrompt] = useState('');
const [facebookUrl, setFacebookUrl] = useState('');
const [instagramUrl, setInstagramUrl] = useState('');
const [tiktokUrl, setTiktokUrl] = useState('');
@@ -56,6 +162,7 @@ export default function StoreSettingsPage() {
setBrandName(data.brandName || '');
setLogoUrl(data.logoUrl || '');
setThemePreset(data.themePreset || 'default');
setBrandPrompt(data.brandPrompt || DEFAULT_BRAND_PROMPT);
setFacebookUrl(data.facebookUrl || '');
setInstagramUrl(data.instagramUrl || '');
setTiktokUrl(data.tiktokUrl || '');
@@ -68,6 +175,17 @@ export default function StoreSettingsPage() {
.finally(() => setLoading(false));
}, []);
const handleCopyPrompt = async () => {
if (!brandPrompt) return;
try {
await navigator.clipboard.writeText(brandPrompt);
setPromptCopied(true);
setTimeout(() => setPromptCopied(false), 2000);
} catch {
// ignore
}
};
const getLocalizedSeoValue = (field: 'defaultSeoTitle' | 'defaultSeoDescription'): string => {
if (activeSeoLang === 'th') {
const thTrans = translations.find((t) => t.langCode === 'th');
@@ -122,6 +240,7 @@ export default function StoreSettingsPage() {
brandName,
logoUrl: logoUrl || null,
themePreset,
brandPrompt: brandPrompt || null,
facebookUrl: facebookUrl || null,
instagramUrl: instagramUrl || null,
tiktokUrl: tiktokUrl || null,
@@ -278,7 +397,101 @@ export default function StoreSettingsPage() {
</div>
</div>
{/* 2. Global Default SEO & Social Sharing */}
{/* 2. Brand Visual Art Direction & AI Prompt Directive */}
<div className="admin-card p-8 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-white/10 pb-4">
<div className="flex items-center gap-3">
<div className="p-2.5 bg-purple-500/10 border border-purple-500/20 rounded-xl">
<Palette className="w-5 h-5 text-purple-400" />
</div>
<div>
<h2 className="text-lg font-bold uppercase tracking-widest text-white flex items-center gap-2">
Brand Visual Art Direction & AI Prompt
<span className="px-2 py-0.5 rounded-full bg-purple-500/20 text-purple-300 text-[10px] font-bold tracking-wider uppercase border border-purple-500/30">
Master Directive
</span>
</h2>
<p className="text-xs text-slate-400">
Master prompt and guidelines used for AI generation, photography mood, color palette, lighting, and campaign art direction.
</p>
</div>
</div>
{/* Quick Actions Toolbar */}
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => setBrandPrompt(DEFAULT_BRAND_PROMPT)}
className="px-3 py-1.5 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 text-purple-300 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all cursor-pointer"
title="โหลดแม่แบบ Master Prompt สำหรับ AKÉRIA"
>
<Wand2 className="w-3.5 h-3.5" />
<span>Load AKÉRIA Preset</span>
</button>
<button
type="button"
onClick={handleCopyPrompt}
disabled={!brandPrompt}
className={`px-3 py-1.5 border rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all cursor-pointer ${
promptCopied
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-400'
: 'bg-white/5 hover:bg-white/10 border-white/10 text-slate-300 hover:text-white disabled:opacity-40'
}`}
title="คัดลอก Prompt ไปยัง Clipboard"
>
{promptCopied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
<span>{promptCopied ? 'Copied!' : 'Copy Prompt'}</span>
</button>
<button
type="button"
onClick={() => setBrandPrompt('')}
disabled={!brandPrompt}
className="px-2.5 py-1.5 bg-white/5 hover:bg-rose-500/10 hover:border-rose-500/30 text-slate-400 hover:text-rose-400 border border-white/10 rounded-lg text-xs font-semibold transition-all disabled:opacity-40 cursor-pointer"
title="ล้างข้อความในกล่อง"
>
Clear
</button>
</div>
</div>
<div className="space-y-3">
<div className="relative">
<textarea
value={brandPrompt}
onChange={(e) => setBrandPrompt(e.target.value)}
rows={18}
className="admin-input w-full font-mono text-xs leading-relaxed p-4 bg-night-950/90 border-white/10 focus:border-purple-500/50 rounded-xl resize-y text-slate-200 placeholder:text-slate-600 outline-none transition-all shadow-inner"
placeholder="Enter Brand Visual Art Direction and Master AI Prompt here..."
spellCheck={false}
/>
</div>
{/* Prompt Stats & Guidelines Bar */}
<div className="flex flex-wrap items-center justify-between gap-3 text-xs text-slate-400 pt-1">
<div className="flex items-center gap-3">
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-night-950/60 border border-white/5 font-mono text-[11px] text-slate-300">
<FileText className="w-3 h-3 text-purple-400" />
Lines: {brandPrompt ? brandPrompt.split('\n').length : 0}
</span>
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-night-950/60 border border-white/5 font-mono text-[11px] text-slate-300">
Chars: {brandPrompt ? brandPrompt.length.toLocaleString() : 0}
</span>
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-night-950/60 border border-white/5 font-mono text-[11px] text-slate-300">
Words: {brandPrompt ? brandPrompt.trim().split(/\s+/).filter(Boolean).length : 0}
</span>
</div>
<div className="text-[11px] text-slate-500 flex items-center gap-1.5">
<Sparkles className="w-3.5 h-3.5 text-purple-400 shrink-0" />
<span>คำสั่ง Art Direction นี้จะถูกนำไปใช้เป็น System Instruction สำหรับการ Generate ภาพ และคุม Mood & Tone ของแบรนด์</span>
</div>
</div>
</div>
</div>
{/* 3. Global Default SEO & Social Sharing */}
<div className="admin-card p-8 space-y-6">
<div className="flex items-center justify-between border-b border-white/10 pb-4">
<div className="flex items-center gap-3">
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "StoreSettings" ADD COLUMN "brandPrompt" TEXT;
-- AlterTable
ALTER TABLE "StoreSettingsTranslation" ADD COLUMN "brandPrompt" TEXT;
@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "BrandKnowledge" (
"id" INTEGER NOT NULL DEFAULT 1,
"brandName" TEXT NOT NULL DEFAULT 'AKÉRIA',
"tagline" TEXT,
"story" TEXT,
"targetAudience" TEXT,
"toneOfVoice" TEXT,
"visualArtPrompt" TEXT,
"productUsps" TEXT,
"marketingGuidelines" TEXT,
"customRules" TEXT,
"masterPromptOverride" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BrandKnowledge_pkey" PRIMARY KEY ("id")
);
+18
View File
@@ -288,6 +288,7 @@ model StoreSettings {
defaultSeoTitle String?
defaultSeoDescription String?
defaultOgImage String?
brandPrompt String?
translations StoreSettingsTranslation[]
updatedAt DateTime @updatedAt
}
@@ -304,6 +305,7 @@ model StoreSettingsTranslation {
newsletterButtonText String?
defaultSeoTitle String?
defaultSeoDescription String?
brandPrompt String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -317,6 +319,22 @@ model NewsletterSubscriber {
createdAt DateTime @default(now())
}
model BrandKnowledge {
id Int @id @default(1)
brandName String @default("AKÉRIA")
tagline String?
story String?
targetAudience String?
toneOfVoice String?
visualArtPrompt String?
productUsps String?
marketingGuidelines String?
customRules String?
masterPromptOverride String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Order {
id Int @id @default(autoincrement())
channel String @default("ONLINE") // "ONLINE" | "POS"
+2 -1
View File
@@ -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" });
+262
View File
@@ -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())),
}),
}
);
+14 -1
View File
@@ -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()),
})
})
+48
View File
@@ -1012,6 +1012,38 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
}
}
},
{
name: "get_brand_knowledge",
description: "Fetch structured Brand Knowledge, Core Directives, Visual Art Direction, and optionally the Master Compiled System Prompt for LLMs / AI Agents.",
inputSchema: {
type: "object",
properties: {
includeCompiledPrompt: {
type: "boolean",
description: "Whether to include the compiled markdown prompt in the response"
}
}
}
},
{
name: "update_brand_knowledge",
description: "Update Brand Knowledge and Directives (Identity, Tone of Voice, Visual Art Prompt, USPs, Marketing Guidelines, Custom Rules, Master Prompt Override).",
inputSchema: {
type: "object",
properties: {
brandName: { type: "string" },
tagline: { type: "string" },
story: { type: "string" },
targetAudience: { type: "string" },
toneOfVoice: { type: "string" },
visualArtPrompt: { type: "string" },
productUsps: { type: "string" },
marketingGuidelines: { type: "string" },
customRules: { type: "string" },
masterPromptOverride: { type: "string" }
}
}
},
],
};
});
@@ -1736,6 +1768,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
}
case "get_brand_knowledge": {
const res = await api.get("/brand-knowledge");
let result = res.data;
if (params.includeCompiledPrompt) {
const compiledRes = await api.get("/brand-knowledge/compiled-prompt");
result = {
...result,
compiledPrompt: compiledRes.data?.compiledPrompt ?? "",
};
}
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
case "update_brand_knowledge": {
const res = await api.put("/brand-knowledge", params);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}