x
This commit is contained in:
1 parent
683f54fd95
commit
44a613f8ec
12 files changed
+1222
-4
No files matched your search
@@ -18,6 +18,7 @@ import ProtectedRoute from './components/ProtectedRoute'
|
|||||||
import OrdersPage from './pages/OrdersPage'
|
import OrdersPage from './pages/OrdersPage'
|
||||||
import OrderDetailPage from './pages/OrderDetailPage'
|
import OrderDetailPage from './pages/OrderDetailPage'
|
||||||
import StoreSettingsPage from './pages/StoreSettingsPage'
|
import StoreSettingsPage from './pages/StoreSettingsPage'
|
||||||
|
import BrandIntelligencePage from './pages/BrandIntelligencePage'
|
||||||
import PaymentSettingsPage from './pages/PaymentSettingsPage'
|
import PaymentSettingsPage from './pages/PaymentSettingsPage'
|
||||||
import ThemeSettingsPage from './pages/ThemeSettingsPage'
|
import ThemeSettingsPage from './pages/ThemeSettingsPage'
|
||||||
import ContentSettingsPage from './pages/ContentSettingsPage'
|
import ContentSettingsPage from './pages/ContentSettingsPage'
|
||||||
@@ -113,6 +114,8 @@ function App() {
|
|||||||
<Route path="/coupons" element={<CouponsPage />} />
|
<Route path="/coupons" element={<CouponsPage />} />
|
||||||
<Route path="/raw-materials" element={<RawMaterialsPage />} />
|
<Route path="/raw-materials" element={<RawMaterialsPage />} />
|
||||||
<Route path="/settings" element={<StoreSettingsPage />} />
|
<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/payments" element={<PaymentSettingsPage />} />
|
||||||
<Route path="/settings/theme" element={<ThemeSettingsPage />} />
|
<Route path="/settings/theme" element={<ThemeSettingsPage />} />
|
||||||
<Route path="/settings/content" element={<ContentSettingsPage />} />
|
<Route path="/settings/content" element={<ContentSettingsPage />} />
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
FolderArchive,
|
FolderArchive,
|
||||||
Map,
|
Map,
|
||||||
Target,
|
Target,
|
||||||
|
BrainCircuit,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useStoreSettings } from '../contexts/StoreSettingsContext'
|
import { useStoreSettings } from '../contexts/StoreSettingsContext'
|
||||||
import { clearAdminAuth } from '../utils/auth'
|
import { clearAdminAuth } from '../utils/auth'
|
||||||
@@ -140,6 +141,7 @@ export default function Sidebar({ isCollapsed, onToggle }: SidebarProps) {
|
|||||||
|
|
||||||
const CMS_MENU = [
|
const CMS_MENU = [
|
||||||
{ icon: Layers, label: t('nav.pageBuilder', 'Page Builder (CMS)'), to: '/page-builder' },
|
{ 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: Sparkles, label: t('nav.cmsBlockShowcase', 'CMS Block Showcase'), to: '/cms-mockup' },
|
||||||
{ icon: ImageIcon, label: t('nav.mediaLibrary', 'Media Library'), to: '/media' },
|
{ icon: ImageIcon, label: t('nav.mediaLibrary', 'Media Library'), to: '/media' },
|
||||||
{ icon: BookOpen, label: t('nav.scentGuide', 'Scent Guide'), to: '/articles' },
|
{ icon: BookOpen, label: t('nav.scentGuide', 'Scent Guide'), to: '/articles' },
|
||||||
@@ -170,6 +172,7 @@ export default function Sidebar({ isCollapsed, onToggle }: SidebarProps) {
|
|||||||
|
|
||||||
const SETTINGS_MENU = [
|
const SETTINGS_MENU = [
|
||||||
{ icon: Store, label: t('nav.storeSettings', 'Store Settings'), to: '/settings' },
|
{ 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: CreditCard, label: t('nav.paymentSettings', 'Payment & Shipping'), to: '/settings/payments' },
|
||||||
{ icon: Palette, label: t('nav.themeSettings', 'Theme & Design'), to: '/settings/theme' },
|
{ icon: Palette, label: t('nav.themeSettings', 'Theme & Design'), to: '/settings/theme' },
|
||||||
{ icon: FileText, label: t('nav.contentSettings', 'Content'), to: '/settings/content' },
|
{ icon: FileText, label: t('nav.contentSettings', 'Content'), to: '/settings/content' },
|
||||||
|
|||||||
@@ -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 ปี • ไลฟ์สไตล์: ชอบความมินิมอล การพักผ่อน และสกินแคร์/อโรมากลิ่นละมุน..."
|
||||||
|
/>
|
||||||
|
</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="• บุคลิก: อบอุ่น สุภาพ เป็นมิตร พรีเมียม อ่อนโยน • คำที่ชอบ: Serenity, Luminous glow, Mindful calm • คำต้องห้าม: ถูกที่สุด, การันตี 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 • จุดเด่น: สารสกัดธรรมชาติ 100%, ปราศจากแอลกอฮอล์และสารเคมีอันตราย, Cruelty-free • อารมณ์ที่ได้รับ: ผ่อนคลาย จิตใจสงบ รู้สึกสดชื่นและมีพลังบวก..."
|
||||||
|
/>
|
||||||
|
</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 สวยงาม • Instagram: คุมโทนสีพาสเทล นุ่มนวล คลีน • CTAs: สัมผัสความผ่อนคลายวันนี้, เริ่มต้นการดูแลตัวเองของคุณ • 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 ตลอดเวลา 2. ห้ามใช้สีฉูดฉาดแบบนีออนเด็ดขาด 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,11 +8,115 @@ import {
|
|||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
UploadCloud,
|
UploadCloud,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
CheckCircle2
|
CheckCircle2,
|
||||||
|
Palette,
|
||||||
|
Wand2,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
FileText
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useStoreSettings } from '../contexts/StoreSettingsContext';
|
import { useStoreSettings } from '../contexts/StoreSettingsContext';
|
||||||
import MediaLibraryModal from '../components/MediaLibraryModal';
|
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 = [
|
const SUPPORTED_LOCALES = [
|
||||||
{ code: 'th', label: 'TH', full: 'ภาษาไทย (Default)' },
|
{ code: 'th', label: 'TH', full: 'ภาษาไทย (Default)' },
|
||||||
{ code: 'en', label: 'EN', full: 'English' },
|
{ code: 'en', label: 'EN', full: 'English' },
|
||||||
@@ -29,11 +133,13 @@ export default function StoreSettingsPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
|
const [promptCopied, setPromptCopied] = useState(false);
|
||||||
|
|
||||||
// Store Brand & Identity
|
// Store Brand & Identity
|
||||||
const [brandName, setBrandName] = useState('');
|
const [brandName, setBrandName] = useState('');
|
||||||
const [logoUrl, setLogoUrl] = useState('');
|
const [logoUrl, setLogoUrl] = useState('');
|
||||||
const [themePreset, setThemePreset] = useState('default');
|
const [themePreset, setThemePreset] = useState('default');
|
||||||
|
const [brandPrompt, setBrandPrompt] = useState('');
|
||||||
const [facebookUrl, setFacebookUrl] = useState('');
|
const [facebookUrl, setFacebookUrl] = useState('');
|
||||||
const [instagramUrl, setInstagramUrl] = useState('');
|
const [instagramUrl, setInstagramUrl] = useState('');
|
||||||
const [tiktokUrl, setTiktokUrl] = useState('');
|
const [tiktokUrl, setTiktokUrl] = useState('');
|
||||||
@@ -56,6 +162,7 @@ export default function StoreSettingsPage() {
|
|||||||
setBrandName(data.brandName || '');
|
setBrandName(data.brandName || '');
|
||||||
setLogoUrl(data.logoUrl || '');
|
setLogoUrl(data.logoUrl || '');
|
||||||
setThemePreset(data.themePreset || 'default');
|
setThemePreset(data.themePreset || 'default');
|
||||||
|
setBrandPrompt(data.brandPrompt || DEFAULT_BRAND_PROMPT);
|
||||||
setFacebookUrl(data.facebookUrl || '');
|
setFacebookUrl(data.facebookUrl || '');
|
||||||
setInstagramUrl(data.instagramUrl || '');
|
setInstagramUrl(data.instagramUrl || '');
|
||||||
setTiktokUrl(data.tiktokUrl || '');
|
setTiktokUrl(data.tiktokUrl || '');
|
||||||
@@ -68,6 +175,17 @@ export default function StoreSettingsPage() {
|
|||||||
.finally(() => setLoading(false));
|
.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 => {
|
const getLocalizedSeoValue = (field: 'defaultSeoTitle' | 'defaultSeoDescription'): string => {
|
||||||
if (activeSeoLang === 'th') {
|
if (activeSeoLang === 'th') {
|
||||||
const thTrans = translations.find((t) => t.langCode === 'th');
|
const thTrans = translations.find((t) => t.langCode === 'th');
|
||||||
@@ -122,6 +240,7 @@ export default function StoreSettingsPage() {
|
|||||||
brandName,
|
brandName,
|
||||||
logoUrl: logoUrl || null,
|
logoUrl: logoUrl || null,
|
||||||
themePreset,
|
themePreset,
|
||||||
|
brandPrompt: brandPrompt || null,
|
||||||
facebookUrl: facebookUrl || null,
|
facebookUrl: facebookUrl || null,
|
||||||
instagramUrl: instagramUrl || null,
|
instagramUrl: instagramUrl || null,
|
||||||
tiktokUrl: tiktokUrl || null,
|
tiktokUrl: tiktokUrl || null,
|
||||||
@@ -278,7 +397,101 @@ export default function StoreSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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="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 justify-between border-b border-white/10 pb-4">
|
||||||
<div className="flex items-center gap-3">
|
<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")
|
||||||
|
);
|
||||||
@@ -288,6 +288,7 @@ model StoreSettings {
|
|||||||
defaultSeoTitle String?
|
defaultSeoTitle String?
|
||||||
defaultSeoDescription String?
|
defaultSeoDescription String?
|
||||||
defaultOgImage String?
|
defaultOgImage String?
|
||||||
|
brandPrompt String?
|
||||||
translations StoreSettingsTranslation[]
|
translations StoreSettingsTranslation[]
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
@@ -304,6 +305,7 @@ model StoreSettingsTranslation {
|
|||||||
newsletterButtonText String?
|
newsletterButtonText String?
|
||||||
defaultSeoTitle String?
|
defaultSeoTitle String?
|
||||||
defaultSeoDescription String?
|
defaultSeoDescription String?
|
||||||
|
brandPrompt String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -317,6 +319,22 @@ model NewsletterSubscriber {
|
|||||||
createdAt DateTime @default(now())
|
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 {
|
model Order {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
channel String @default("ONLINE") // "ONLINE" | "POS"
|
channel String @default("ONLINE") // "ONLINE" | "POS"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { backupRoute } from "./routes/backup";
|
|||||||
import { businessAnalyticsRoute } from "./routes/businessAnalytics";
|
import { businessAnalyticsRoute } from "./routes/businessAnalytics";
|
||||||
import { toolsRoute } from "./routes/tools";
|
import { toolsRoute } from "./routes/tools";
|
||||||
import { crmRoute } from "./routes/crm";
|
import { crmRoute } from "./routes/crm";
|
||||||
|
import { brandKnowledgeRoute } from "./routes/brandKnowledge";
|
||||||
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051;
|
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051;
|
||||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
|
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_brand";
|
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_brand";
|
||||||
@@ -139,7 +140,7 @@ const app = new Elysia()
|
|||||||
return file;
|
return file;
|
||||||
})
|
})
|
||||||
.group("/api", (app) =>
|
.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" });
|
.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.newsletterButtonText !== undefined) updateData.newsletterButtonText = item.newsletterButtonText;
|
||||||
if (item.defaultSeoTitle !== undefined) updateData.defaultSeoTitle = item.defaultSeoTitle;
|
if (item.defaultSeoTitle !== undefined) updateData.defaultSeoTitle = item.defaultSeoTitle;
|
||||||
if (item.defaultSeoDescription !== undefined) updateData.defaultSeoDescription = item.defaultSeoDescription;
|
if (item.defaultSeoDescription !== undefined) updateData.defaultSeoDescription = item.defaultSeoDescription;
|
||||||
|
if (item.brandPrompt !== undefined) updateData.brandPrompt = item.brandPrompt;
|
||||||
|
|
||||||
const existing = await prisma.storeSettingsTranslation.findUnique({
|
const existing = await prisma.storeSettingsTranslation.findUnique({
|
||||||
where: { storeSettingsId_langCode: { storeSettingsId: settingsId, langCode: lang } }
|
where: { storeSettingsId_langCode: { storeSettingsId: settingsId, langCode: lang } }
|
||||||
@@ -82,6 +83,12 @@ export async function saveStoreSettingsTranslations(settingsId: number, payload:
|
|||||||
? (lang === 'th' ? payload.defaultSeoDescription : undefined)
|
? (lang === 'th' ? payload.defaultSeoDescription : undefined)
|
||||||
: payload.defaultSeoDescription?.[lang];
|
: 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 (
|
if (
|
||||||
footerDescription === undefined &&
|
footerDescription === undefined &&
|
||||||
newsletterTitle === undefined &&
|
newsletterTitle === undefined &&
|
||||||
@@ -89,7 +96,8 @@ export async function saveStoreSettingsTranslations(settingsId: number, payload:
|
|||||||
newsletterPlaceholder === undefined &&
|
newsletterPlaceholder === undefined &&
|
||||||
newsletterButtonText === undefined &&
|
newsletterButtonText === undefined &&
|
||||||
defaultSeoTitle === undefined &&
|
defaultSeoTitle === undefined &&
|
||||||
defaultSeoDescription === undefined
|
defaultSeoDescription === undefined &&
|
||||||
|
brandPrompt === undefined
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -102,6 +110,7 @@ export async function saveStoreSettingsTranslations(settingsId: number, payload:
|
|||||||
if (newsletterButtonText !== undefined) updateData.newsletterButtonText = newsletterButtonText;
|
if (newsletterButtonText !== undefined) updateData.newsletterButtonText = newsletterButtonText;
|
||||||
if (defaultSeoTitle !== undefined) updateData.defaultSeoTitle = defaultSeoTitle;
|
if (defaultSeoTitle !== undefined) updateData.defaultSeoTitle = defaultSeoTitle;
|
||||||
if (defaultSeoDescription !== undefined) updateData.defaultSeoDescription = defaultSeoDescription;
|
if (defaultSeoDescription !== undefined) updateData.defaultSeoDescription = defaultSeoDescription;
|
||||||
|
if (brandPrompt !== undefined) updateData.brandPrompt = brandPrompt;
|
||||||
|
|
||||||
const existing = await prisma.storeSettingsTranslation.findUnique({
|
const existing = await prisma.storeSettingsTranslation.findUnique({
|
||||||
where: { storeSettingsId_langCode: { storeSettingsId: settingsId, langCode: lang } }
|
where: { storeSettingsId_langCode: { storeSettingsId: settingsId, langCode: lang } }
|
||||||
@@ -155,6 +164,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
|||||||
const localizedDescription = translation?.footerDescription;
|
const localizedDescription = translation?.footerDescription;
|
||||||
const localizedSeoTitle = translation?.defaultSeoTitle;
|
const localizedSeoTitle = translation?.defaultSeoTitle;
|
||||||
const localizedSeoDescription = translation?.defaultSeoDescription;
|
const localizedSeoDescription = translation?.defaultSeoDescription;
|
||||||
|
const localizedBrandPrompt = translation?.brandPrompt;
|
||||||
|
|
||||||
const headerBehavior = (settings.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll';
|
const headerBehavior = (settings.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll';
|
||||||
const mobileHeaderBehavior = (settings.themeTokens as any)?.mobileHeaderBehavior ?? headerBehavior;
|
const mobileHeaderBehavior = (settings.themeTokens as any)?.mobileHeaderBehavior ?? headerBehavior;
|
||||||
@@ -169,6 +179,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
|||||||
defaultSeoTitle: localizedSeoTitle ?? settingsData.defaultSeoTitle,
|
defaultSeoTitle: localizedSeoTitle ?? settingsData.defaultSeoTitle,
|
||||||
defaultSeoDescription: localizedSeoDescription ?? settingsData.defaultSeoDescription,
|
defaultSeoDescription: localizedSeoDescription ?? settingsData.defaultSeoDescription,
|
||||||
defaultOgImage: settingsData.defaultOgImage,
|
defaultOgImage: settingsData.defaultOgImage,
|
||||||
|
brandPrompt: localizedBrandPrompt ?? settingsData.brandPrompt,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Settings] Error:", err);
|
console.error("[Settings] Error:", err);
|
||||||
@@ -342,6 +353,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
|||||||
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
|
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
|
||||||
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
|
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
|
||||||
defaultOgImage: 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()),
|
translations: t.Optional(t.Any()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -426,6 +438,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
|||||||
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
|
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
|
||||||
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
|
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
|
||||||
defaultOgImage: 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()),
|
translations: t.Optional(t.Any()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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) }] };
|
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:
|
default:
|
||||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ export interface StoreSettingsTranslation {
|
|||||||
newsletterButtonText?: string | null
|
newsletterButtonText?: string | null
|
||||||
defaultSeoTitle?: string | null
|
defaultSeoTitle?: string | null
|
||||||
defaultSeoDescription?: string | null
|
defaultSeoDescription?: string | null
|
||||||
|
brandPrompt?: string | null
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
updatedAt?: string
|
updatedAt?: string
|
||||||
}
|
}
|
||||||
@@ -217,9 +218,26 @@ export interface StoreSettings {
|
|||||||
defaultSeoTitle?: string | null
|
defaultSeoTitle?: string | null
|
||||||
defaultSeoDescription?: string | null
|
defaultSeoDescription?: string | null
|
||||||
defaultOgImage?: string | null
|
defaultOgImage?: string | null
|
||||||
|
brandPrompt?: string | null
|
||||||
translations?: StoreSettingsTranslation[]
|
translations?: StoreSettingsTranslation[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BrandKnowledge {
|
||||||
|
id: number
|
||||||
|
brandName: string
|
||||||
|
tagline?: string | null
|
||||||
|
story?: string | null
|
||||||
|
targetAudience?: string | null
|
||||||
|
toneOfVoice?: string | null
|
||||||
|
visualArtPrompt?: string | null
|
||||||
|
productUsps?: string | null
|
||||||
|
marketingGuidelines?: string | null
|
||||||
|
customRules?: string | null
|
||||||
|
masterPromptOverride?: string | null
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface FooterBrand {
|
export interface FooterBrand {
|
||||||
brandName: string
|
brandName: string
|
||||||
logoUrl?: string | null
|
logoUrl?: string | null
|
||||||
|
|||||||
Reference in new issue
Block a user