feat: centralizar ADN y estilo de marca en Zona de Marca y simplificar panel de creatividades a operativa ads
This commit is contained in:
885
package-lock.json
generated
885
package-lock.json
generated
@@ -74,6 +74,84 @@
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||
@@ -91,6 +169,363 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
@@ -113,6 +548,28 @@
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||
@@ -129,6 +586,307 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -190,6 +948,118 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.7.tgz",
|
||||
"integrity": "sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.7.tgz",
|
||||
"integrity": "sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.7.tgz",
|
||||
"integrity": "sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.7.tgz",
|
||||
"integrity": "sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.7.tgz",
|
||||
"integrity": "sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.7.tgz",
|
||||
"integrity": "sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.7.tgz",
|
||||
"integrity": "sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1094,6 +1964,21 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
||||
@@ -8,6 +8,12 @@ import {
|
||||
compileClientSkillMarkdown,
|
||||
normalizeCreativeStyle,
|
||||
} from '@/lib/creative-style';
|
||||
import {
|
||||
compileClientSkillFromBrandbook,
|
||||
parseBrandbook,
|
||||
seedBrandbook,
|
||||
type Brandbook,
|
||||
} from '@/lib/brandbook';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -62,8 +68,39 @@ export async function GET(req: NextRequest) {
|
||||
: (tenant.settings as any)?.creative_style;
|
||||
|
||||
const style = normalizeCreativeStyle(rawStyle, tenant.name || 'Cliente', tenant.slug || '');
|
||||
const compiledMarkdown = compileClientSkillMarkdown(
|
||||
style,
|
||||
|
||||
// Cargar brandbook de documents (fuente central de verdad)
|
||||
const { data: brandDoc } = await admin
|
||||
.from('documents')
|
||||
.select('content')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('type', 'brandbook')
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
let brandbook: Brandbook | null = null;
|
||||
if (brandDoc?.content) {
|
||||
try {
|
||||
const parsedContent =
|
||||
typeof brandDoc.content === 'string'
|
||||
? JSON.parse(brandDoc.content)
|
||||
: brandDoc.content;
|
||||
brandbook = parseBrandbook(parsedContent);
|
||||
} catch {
|
||||
brandbook = null;
|
||||
}
|
||||
}
|
||||
if (!brandbook) {
|
||||
brandbook = seedBrandbook(tenant.slug || 'cliente', tenant.name || 'Cliente');
|
||||
}
|
||||
|
||||
const compiledMarkdown = compileClientSkillFromBrandbook(
|
||||
brandbook,
|
||||
{
|
||||
image_ai: style.image_ai,
|
||||
campaign_rules: style.campaign_rules,
|
||||
},
|
||||
tenant.name || 'Cliente',
|
||||
tenant.slug || 'cliente'
|
||||
);
|
||||
@@ -204,8 +241,38 @@ export async function PUT(req: NextRequest) {
|
||||
return NextResponse.json({ error: updErr.message }, { status: 500 });
|
||||
}
|
||||
|
||||
const compiledMarkdown = compileClientSkillMarkdown(
|
||||
normalizedStyle,
|
||||
// Cargar brandbook de documents para compilar skill actualizado
|
||||
const { data: brandDoc } = await admin
|
||||
.from('documents')
|
||||
.select('content')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('type', 'brandbook')
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
let brandbook: Brandbook | null = null;
|
||||
if (brandDoc?.content) {
|
||||
try {
|
||||
const parsedContent =
|
||||
typeof brandDoc.content === 'string'
|
||||
? JSON.parse(brandDoc.content)
|
||||
: brandDoc.content;
|
||||
brandbook = parseBrandbook(parsedContent);
|
||||
} catch {
|
||||
brandbook = null;
|
||||
}
|
||||
}
|
||||
if (!brandbook) {
|
||||
brandbook = seedBrandbook(existing.slug || 'cliente', existing.name || 'Cliente');
|
||||
}
|
||||
|
||||
const compiledMarkdown = compileClientSkillFromBrandbook(
|
||||
brandbook,
|
||||
{
|
||||
image_ai: normalizedStyle.image_ai,
|
||||
campaign_rules: normalizedStyle.campaign_rules,
|
||||
},
|
||||
existing.name || 'Cliente',
|
||||
existing.slug || 'cliente'
|
||||
);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { authenticateIntegrationRequest } from '@/lib/integrations-auth';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { compileClientSkillMarkdown, normalizeCreativeStyle } from '@/lib/creative-style';
|
||||
import { normalizeCreativeStyle } from '@/lib/creative-style';
|
||||
import {
|
||||
compileClientSkillFromBrandbook,
|
||||
parseBrandbook,
|
||||
seedBrandbook,
|
||||
type Brandbook,
|
||||
} from '@/lib/brandbook';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
@@ -32,36 +38,54 @@ export async function GET(req: NextRequest) {
|
||||
const slug = (tenant.slug || 'bps').toLowerCase().trim();
|
||||
const formatParam = (req.nextUrl.searchParams.get('format') || 'estatico').toLowerCase().trim();
|
||||
|
||||
// 2. Resolver skill del cliente: primero desde BD (creative_style editable en la app), fallback a archivo en disco
|
||||
// 2. Obtener brandbook de documents (fuente central de verdad: Zona de Marca)
|
||||
const { data: brandDoc } = await adminClient
|
||||
.from('documents')
|
||||
.select('content')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('type', 'brandbook')
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
let brandbook: Brandbook | null = null;
|
||||
if (brandDoc?.content) {
|
||||
try {
|
||||
const parsedContent =
|
||||
typeof brandDoc.content === 'string'
|
||||
? JSON.parse(brandDoc.content)
|
||||
: brandDoc.content;
|
||||
brandbook = parseBrandbook(parsedContent);
|
||||
} catch {
|
||||
brandbook = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!brandbook) {
|
||||
brandbook = seedBrandbook(slug, tenant.name || 'Cliente');
|
||||
}
|
||||
|
||||
// 3. Obtener configuración operativa de Ads (Freepik Mystic, Canva y Reglas de campaña)
|
||||
const rawDbStyle =
|
||||
tenant.creative_style && Object.keys(tenant.creative_style).length > 0
|
||||
? tenant.creative_style
|
||||
: (tenant.settings as any)?.creative_style;
|
||||
|
||||
let clientSkill = '';
|
||||
const operativeStyle = normalizeCreativeStyle(rawDbStyle, tenant.name || 'Cliente', slug);
|
||||
|
||||
// 4. Compilar skill de cliente a partir de Zona de Marca + Operativa Ads
|
||||
let clientSkill = compileClientSkillFromBrandbook(
|
||||
brandbook,
|
||||
{
|
||||
image_ai: operativeStyle.image_ai,
|
||||
campaign_rules: operativeStyle.campaign_rules,
|
||||
},
|
||||
tenant.name || 'Cliente',
|
||||
slug
|
||||
);
|
||||
|
||||
// 5. Resolver skill del formato (skills/formats/<format>-4-5.md o skills/formats/<format>.md, fallback a estatico)
|
||||
const skillsDir = path.join(process.cwd(), 'skills');
|
||||
|
||||
if (rawDbStyle && Object.keys(rawDbStyle).length > 0) {
|
||||
const normalized = normalizeCreativeStyle(rawDbStyle, tenant.name || 'Cliente', slug);
|
||||
clientSkill = compileClientSkillMarkdown(normalized, tenant.name || 'Cliente', slug);
|
||||
} else {
|
||||
const clientCandidates = [
|
||||
path.join(skillsDir, 'clients', `${slug}.md`),
|
||||
path.join(skillsDir, 'clients', slug === 'bps' ? 'blue-parrot-school.md' : `${slug}.md`),
|
||||
];
|
||||
for (const p of clientCandidates) {
|
||||
if (fs.existsSync(p)) {
|
||||
clientSkill = fs.readFileSync(p, 'utf-8');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!clientSkill) {
|
||||
const normalized = normalizeCreativeStyle(null, tenant.name || 'Cliente', slug);
|
||||
clientSkill = compileClientSkillMarkdown(normalized, tenant.name || 'Cliente', slug);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Resolver skill del formato (skills/formats/<format>-4-5.md o skills/formats/<format>.md, fallback a estatico)
|
||||
const formatCandidates = [
|
||||
path.join(skillsDir, 'formats', `${formatParam}-4-5.md`),
|
||||
path.join(skillsDir, 'formats', `${formatParam}.md`),
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
type BrandbookSectionId,
|
||||
type ColorToken,
|
||||
type FontToken,
|
||||
type CreativeB2BPersona,
|
||||
type CreativeB2CPersona,
|
||||
type CreativeObjection,
|
||||
} from '@/lib/brandbook';
|
||||
import GoogleFontsGallery from '@/components/GoogleFontsGallery';
|
||||
import BrandOverview from '@/components/BrandOverview';
|
||||
@@ -632,6 +635,58 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
||||
onChange={(e) => patch({ origin_story: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Sitio Web">
|
||||
<input
|
||||
className={inputCls}
|
||||
value={book.website || ''}
|
||||
placeholder="ej. blueparrotschool.com"
|
||||
onChange={(e) => patch({ website: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Oferta y programas B2B (empresas)"
|
||||
hint="Programas in-company, formación equipos, acreditaciones oficiales"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.b2b_offering || ''}
|
||||
onChange={(e) => patch({ b2b_offering: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Oferta y cursos B2C (particulares)"
|
||||
hint="Cursos adultos, intensivos, certificaciones Cambridge/Aptis, 1to1"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.b2c_offering || ''}
|
||||
onChange={(e) => patch({ b2c_offering: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Diferenciación clave (USPs frente a competencia)"
|
||||
hint="Profesor fijo, sesiones completas, acreditación directa FUNDAE..."
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.usp_differentiators || ''}
|
||||
onChange={(e) => patch({ usp_differentiators: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Prioridades actuales y foco comercial"
|
||||
hint="Foco de captación prioritario para este periodo"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.current_priorities || ''}
|
||||
onChange={(e) => patch({ current_priorities: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -725,6 +780,48 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Estructura y reglas de redacción (Copy Ads & Web)"
|
||||
hint="Longitud, número de frases, estructura hook -> beneficio -> CTA"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.copy_structure_rules || ''}
|
||||
placeholder="ej. Frases cortas. Un dato concreto vale más que un adjetivo. En primary_text: 2-4 frases, máx. 300 caracteres..."
|
||||
onChange={(e) => patch({ copy_structure_rules: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Política de términos en inglés y anglicismos"
|
||||
hint="Términos admitidos vs anglicismos prohibidos tipo 'boost' o 'level up'"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.english_terms_policy || ''}
|
||||
placeholder="ej. Inglés en el copy: solo nombres propios de exámenes o términos sin traducción natural..."
|
||||
onChange={(e) => patch({ english_terms_policy: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Conceptos fuerza (Power Words)" hint="Uno por línea">
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={listToLines(book.power_words || [])}
|
||||
onChange={(e) =>
|
||||
patch({ power_words: linesToList(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="CTAs permitidos para Ads / Web" hint="Uno por línea">
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={listToLines(book.allowed_ctas || [])}
|
||||
onChange={(e) =>
|
||||
patch({ allowed_ctas: linesToList(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1031,6 +1128,38 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Uso de superficies y combinaciones" className="sm:col-span-2">
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.surface_rules || ''}
|
||||
placeholder="ej. Azul (#2A4D9C) y blanco (#FFFFFF) como base. Amarillo (#FBC02D) solo para resaltar una palabra o CTA..."
|
||||
onChange={(e) => patch({ surface_rules: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Tipografía titulares (Hook)">
|
||||
<input
|
||||
className={inputCls}
|
||||
value={book.typography_headline || ''}
|
||||
placeholder="ej. Poppins Extrabold"
|
||||
onChange={(e) => patch({ typography_headline: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Tipografía cuerpo / apoyo (Support)">
|
||||
<input
|
||||
className={inputCls}
|
||||
value={book.typography_body || ''}
|
||||
placeholder="ej. Poppins Regular"
|
||||
onChange={(e) => patch({ typography_body: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Reglas de texto en imagen" className="sm:col-span-2">
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.image_text_rules || ''}
|
||||
placeholder="ej. Máximo 2 niveles (titular ≤ 8 palabras + apoyo ≤ 12 palabras). Margen mínimo del 8% en todos los lados..."
|
||||
onChange={(e) => patch({ image_text_rules: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -1181,6 +1310,274 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
||||
onChange={(e) => patch({ customer_journey: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Frustración principal previa de la audiencia"
|
||||
hint="Por qué han fracasado con otras alternativas o en qué punto están atascados"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.customer_frustrations || ''}
|
||||
placeholder="ej. Estancamiento en el nivel intermedio eterno por apps de 15 minutos sin profesor fijo..."
|
||||
onChange={(e) => patch({ customer_frustrations: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Transformación deseada (Antes vs Después)"
|
||||
hint="De qué estado evitado pasan a qué resultado concreto"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.desired_transformation || ''}
|
||||
placeholder="ej. Pasar de evitar hablar en inglés o depender de traductores a negociar con seguridad..."
|
||||
onChange={(e) => patch({ desired_transformation: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* B2B Personas Card Editor */}
|
||||
<div className="sm:col-span-2 space-y-3 pt-3 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-gray-800">
|
||||
Buyer Personas B2B (Empresas / Directivos)
|
||||
</h4>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Cargos, dolores de gestión y disparadores de compra corporativos.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const list = [...(book.b2b_personas || [])];
|
||||
list.push({
|
||||
name: `Persona B2B #${list.length + 1}`,
|
||||
role: '',
|
||||
goal: '',
|
||||
pain_point: '',
|
||||
buying_trigger: '',
|
||||
});
|
||||
patch({ b2b_personas: list });
|
||||
}}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-2.5 py-1 text-xs font-semibold text-gray-700 hover:bg-gray-50 shadow-sm"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 text-gray-500" />
|
||||
Añadir B2B
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(!book.b2b_personas || book.b2b_personas.length === 0) ? (
|
||||
<p className="text-xs text-gray-400 italic">No hay buyer personas B2B definidas.</p>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{book.b2b_personas.map((p, idx) => (
|
||||
<div key={idx} className="rounded-xl border border-gray-200 bg-gray-50/50 p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<input
|
||||
type="text"
|
||||
value={p.name}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2b_personas!];
|
||||
list[idx] = { ...list[idx], name: e.target.value };
|
||||
patch({ b2b_personas: list });
|
||||
}}
|
||||
placeholder="Nombre / Identificador"
|
||||
className="font-bold text-xs text-gray-900 bg-transparent border-b border-dashed border-gray-300 focus:outline-none focus:border-brand-500 px-1 py-0.5"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const list = book.b2b_personas!.filter((_, i) => i !== idx);
|
||||
patch({ b2b_personas: list });
|
||||
}}
|
||||
className="text-gray-400 hover:text-red-500 p-1"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-500 block">Cargo / Rol</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.role}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2b_personas!];
|
||||
list[idx] = { ...list[idx], role: e.target.value };
|
||||
patch({ b2b_personas: list });
|
||||
}}
|
||||
placeholder="ej. Responsable de RRHH o Talento"
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-500 block">Objetivo</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.goal}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2b_personas!];
|
||||
list[idx] = { ...list[idx], goal: e.target.value };
|
||||
patch({ b2b_personas: list });
|
||||
}}
|
||||
placeholder="ej. Proveedor llave en mano sin carga administrativa"
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-500 block">Dolor / Frustración</label>
|
||||
<textarea
|
||||
value={p.pain_point}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2b_personas!];
|
||||
list[idx] = { ...list[idx], pain_point: e.target.value };
|
||||
patch({ b2b_personas: list });
|
||||
}}
|
||||
placeholder="ej. Papeleo FUNDAE, quejas por rotación de profes..."
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1 min-h-[50px]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-500 block">Disparador de Compra</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.buying_trigger}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2b_personas!];
|
||||
list[idx] = { ...list[idx], buying_trigger: e.target.value };
|
||||
patch({ b2b_personas: list });
|
||||
}}
|
||||
placeholder="ej. Gestión FUNDAE directa 100% resuelta"
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* B2C Personas Card Editor */}
|
||||
<div className="sm:col-span-2 space-y-3 pt-3 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-gray-800">
|
||||
Buyer Personas B2C (Particulares / Alumnos)
|
||||
</h4>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Segmentos de particulares, edad, situación vital, dolor y meta.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const list = [...(book.b2c_personas || [])];
|
||||
list.push({
|
||||
name: `Persona B2C #${list.length + 1}`,
|
||||
age_range: '',
|
||||
situation: '',
|
||||
pain_point: '',
|
||||
goal: '',
|
||||
});
|
||||
patch({ b2c_personas: list });
|
||||
}}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-2.5 py-1 text-xs font-semibold text-gray-700 hover:bg-gray-50 shadow-sm"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 text-gray-500" />
|
||||
Añadir B2C
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(!book.b2c_personas || book.b2c_personas.length === 0) ? (
|
||||
<p className="text-xs text-gray-400 italic">No hay buyer personas B2C definidas.</p>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{book.b2c_personas.map((p, idx) => (
|
||||
<div key={idx} className="rounded-xl border border-gray-200 bg-gray-50/50 p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<input
|
||||
type="text"
|
||||
value={p.name}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2c_personas!];
|
||||
list[idx] = { ...list[idx], name: e.target.value };
|
||||
patch({ b2c_personas: list });
|
||||
}}
|
||||
placeholder="Perfil / Segmento"
|
||||
className="font-bold text-xs text-gray-900 bg-transparent border-b border-dashed border-gray-300 focus:outline-none focus:border-brand-500 px-1 py-0.5"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const list = book.b2c_personas!.filter((_, i) => i !== idx);
|
||||
patch({ b2c_personas: list });
|
||||
}}
|
||||
className="text-gray-400 hover:text-red-500 p-1"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-500 block">Rango de Edad</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.age_range}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2c_personas!];
|
||||
list[idx] = { ...list[idx], age_range: e.target.value };
|
||||
patch({ b2c_personas: list });
|
||||
}}
|
||||
placeholder="ej. 25-45 años"
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-500 block">Situación</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.situation}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2c_personas!];
|
||||
list[idx] = { ...list[idx], situation: e.target.value };
|
||||
patch({ b2c_personas: list });
|
||||
}}
|
||||
placeholder="ej. Opositor / Empleado"
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-500 block">Dolor / Frustración</label>
|
||||
<textarea
|
||||
value={p.pain_point}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2c_personas!];
|
||||
list[idx] = { ...list[idx], pain_point: e.target.value };
|
||||
patch({ b2c_personas: list });
|
||||
}}
|
||||
placeholder="ej. Ha fracasado con apps sin feedback ni estructura..."
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1 min-h-[50px]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-500 block">Meta / Objetivo</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.goal}
|
||||
onChange={(e) => {
|
||||
const list = [...book.b2c_personas!];
|
||||
list[idx] = { ...list[idx], goal: e.target.value };
|
||||
patch({ b2c_personas: list });
|
||||
}}
|
||||
placeholder="ej. Certificar B2/C1 Cambridge en 6 meses"
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1256,6 +1653,18 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
||||
onChange={(e) => patch({ pricing_notes: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Enfoque de precio / oferta para Ads y Funnel"
|
||||
hint="Coste neto, bonificación FUNDAE, evitar descuentos agresivos..."
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.price_and_offer_framing || ''}
|
||||
placeholder="ej. En B2B destacar el coste bonificado vía FUNDAE... En B2C destacar el valor del programa sin descuentos agresivos..."
|
||||
onChange={(e) => patch({ price_and_offer_framing: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1473,6 +1882,116 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
||||
onChange={(e) => patch({ landing_notes: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Hooks ganadores de referencia"
|
||||
hint="Ejemplos reales que han tenido buen CTR / conversión (uno por línea, para referencia de estilo)"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={listToLines(book.winning_hooks || [])}
|
||||
placeholder="ej. Tu equipo no necesita más inglés. Necesita el inglés de su trabajo..."
|
||||
onChange={(e) =>
|
||||
patch({ winning_hooks: linesToList(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Ángulos recomendados para rotar en campañas"
|
||||
hint="Ángulos prioritarios para Claude (uno por línea)"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={listToLines(book.preferred_angles || [])}
|
||||
placeholder="ej. objeción tiempo, coste FUNDAE, contraste app vs programa real, especialización sectorial..."
|
||||
onChange={(e) =>
|
||||
patch({ preferred_angles: linesToList(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Objeciones a desactivar con Rebuttal */}
|
||||
<div className="sm:col-span-2 space-y-3 pt-3 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-gray-800">
|
||||
Objeciones a Desactivar (Objeción → Argumento de Refutación)
|
||||
</h4>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Utilizadas por Claude para redactar copys de consideración y remarketing que resuelvan dudas del lead.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const list = [...(book.objections || [])];
|
||||
list.push({ objection: '', rebuttal: '' });
|
||||
patch({ objections: list });
|
||||
}}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-2.5 py-1 text-xs font-semibold text-gray-700 hover:bg-gray-50 shadow-sm"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 text-gray-500" />
|
||||
Añadir Objeción
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(!book.objections || book.objections.length === 0) ? (
|
||||
<p className="text-xs text-gray-400 italic">No hay objeciones estructuradas definidas.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{book.objections.map((o, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col sm:flex-row items-start sm:items-center gap-2 rounded-xl border border-gray-200 bg-gray-50/60 p-2.5"
|
||||
>
|
||||
<div className="flex-1 w-full sm:w-auto">
|
||||
<label className="text-[10px] font-semibold text-gray-500 block mb-0.5">
|
||||
Objeción del cliente
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={o.objection}
|
||||
onChange={(e) => {
|
||||
const list = [...book.objections!];
|
||||
list[idx] = { ...list[idx], objection: e.target.value };
|
||||
patch({ objections: list });
|
||||
}}
|
||||
placeholder='ej. "Ya usamos una app"'
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1.5"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[2] w-full sm:w-auto">
|
||||
<label className="text-[10px] font-semibold text-gray-500 block mb-0.5">
|
||||
Respuesta / Refutación
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={o.rebuttal}
|
||||
onChange={(e) => {
|
||||
const list = [...book.objections!];
|
||||
list[idx] = { ...list[idx], rebuttal: e.target.value };
|
||||
patch({ objections: list });
|
||||
}}
|
||||
placeholder='ej. "La app mantiene, no avanza. Sin profesor fijo ni sesiones de 60 min no se sale del nivel intermedio."'
|
||||
className="w-full text-xs rounded border border-gray-200 bg-white px-2 py-1.5"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const list = book.objections!.filter((_, i) => i !== idx);
|
||||
patch({ objections: list });
|
||||
}}
|
||||
className="self-end sm:self-center mt-2 sm:mt-4 text-gray-400 hover:text-red-500 p-1"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1634,6 +2153,42 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
||||
onChange={(e) => patch({ image_rights: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Competidores que NUNCA se deben nombrar"
|
||||
hint="Marcas de la competencia totalmente vetadas en copy y claims (uno por línea)"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={listToLines(book.competitors_never_mention || [])}
|
||||
placeholder="ej. Twenix, Preply, Italki, Open English, What's Up!..."
|
||||
onChange={(e) =>
|
||||
patch({ competitors_never_mention: linesToList(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Política de Emojis"
|
||||
hint="Normas sobre el uso de emojis en creatividades y copies"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.emoji_policy || ''}
|
||||
placeholder="ej. Prohibidos los emojis en creatividades de imagen. En copy, como mucho uno al inicio solo en B2C..."
|
||||
onChange={(e) => patch({ emoji_policy: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Política de Testimonios y Casos de Estudio"
|
||||
hint="Normas para evitar testimonios falsos o no autorizados"
|
||||
>
|
||||
<textarea
|
||||
className={areaCls}
|
||||
value={book.testimonials_policy || ''}
|
||||
placeholder="ej. Prohibidos testimonios inventados. Si no hay un caso real verificado, no se usa..."
|
||||
onChange={(e) => patch({ testimonials_policy: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,27 @@ export type BrandPersona = {
|
||||
channels?: string;
|
||||
};
|
||||
|
||||
export type CreativeB2BPersona = {
|
||||
name: string;
|
||||
role: string;
|
||||
goal: string;
|
||||
pain_point: string;
|
||||
buying_trigger: string;
|
||||
};
|
||||
|
||||
export type CreativeB2CPersona = {
|
||||
name: string;
|
||||
age_range: string;
|
||||
situation: string;
|
||||
pain_point: string;
|
||||
goal: string;
|
||||
};
|
||||
|
||||
export type CreativeObjection = {
|
||||
objection: string;
|
||||
rebuttal: string;
|
||||
};
|
||||
|
||||
export type Brandbook = {
|
||||
version: 2;
|
||||
updated_at?: string;
|
||||
@@ -85,6 +106,11 @@ export type Brandbook = {
|
||||
vision: string;
|
||||
values: string[];
|
||||
vertical: string;
|
||||
b2b_offering?: string;
|
||||
b2c_offering?: string;
|
||||
usp_differentiators?: string;
|
||||
current_priorities?: string;
|
||||
website?: string;
|
||||
/** Voz */
|
||||
tone: string;
|
||||
personality: string[];
|
||||
@@ -96,6 +122,10 @@ export type Brandbook = {
|
||||
example_good: string[];
|
||||
example_bad: string[];
|
||||
slogan_variants: string[];
|
||||
copy_structure_rules?: string;
|
||||
english_terms_policy?: string;
|
||||
power_words?: string[];
|
||||
allowed_ctas?: string[];
|
||||
/** Visual kit */
|
||||
colors: ColorToken[];
|
||||
fonts_kit: FontToken[];
|
||||
@@ -112,10 +142,18 @@ export type Brandbook = {
|
||||
icon_style: string;
|
||||
imagery_do: string[];
|
||||
imagery_dont: string[];
|
||||
surface_rules?: string;
|
||||
image_text_rules?: string;
|
||||
typography_headline?: string;
|
||||
typography_body?: string;
|
||||
kit_assets: BrandAsset[];
|
||||
/** Audiencia */
|
||||
icp: string;
|
||||
personas: BrandPersona[];
|
||||
b2b_personas?: CreativeB2BPersona[];
|
||||
b2c_personas?: CreativeB2CPersona[];
|
||||
customer_frustrations?: string;
|
||||
desired_transformation?: string;
|
||||
geo: string;
|
||||
languages: string[];
|
||||
jobs_to_be_done: string[];
|
||||
@@ -131,6 +169,7 @@ export type Brandbook = {
|
||||
competitive_alternatives: string[];
|
||||
guarantee: string;
|
||||
pricing_notes: string;
|
||||
price_and_offer_framing?: string;
|
||||
/** Pilares */
|
||||
pillars_rrss: string[];
|
||||
series_recurrentes: string[];
|
||||
@@ -169,6 +208,9 @@ export type Brandbook = {
|
||||
ads_formats: string[];
|
||||
landing_notes: string;
|
||||
retargeting_angles: string[];
|
||||
objections?: CreativeObjection[];
|
||||
winning_hooks?: string[];
|
||||
preferred_angles?: string[];
|
||||
/** B2B */
|
||||
linkedin_themes: string[];
|
||||
linkedin_formats: string[];
|
||||
@@ -189,6 +231,9 @@ export type Brandbook = {
|
||||
legal_notes: string;
|
||||
sector_rules: string;
|
||||
image_rights: string;
|
||||
emoji_policy?: string;
|
||||
testimonials_policy?: string;
|
||||
competitors_never_mention?: string[];
|
||||
/** Fuentes */
|
||||
source_excerpts: Array<{
|
||||
id: string;
|
||||
@@ -404,6 +449,30 @@ export function emptyBrandbook(label = ''): Brandbook {
|
||||
image_rights: '',
|
||||
source_excerpts: [],
|
||||
freeform_notes: '',
|
||||
b2b_offering: '',
|
||||
b2c_offering: '',
|
||||
usp_differentiators: '',
|
||||
current_priorities: '',
|
||||
website: '',
|
||||
copy_structure_rules: '',
|
||||
english_terms_policy: '',
|
||||
power_words: [],
|
||||
allowed_ctas: [],
|
||||
surface_rules: '',
|
||||
image_text_rules: '',
|
||||
typography_headline: '',
|
||||
typography_body: '',
|
||||
b2b_personas: [],
|
||||
b2c_personas: [],
|
||||
customer_frustrations: '',
|
||||
desired_transformation: '',
|
||||
price_and_offer_framing: '',
|
||||
objections: [],
|
||||
winning_hooks: [],
|
||||
preferred_angles: [],
|
||||
emoji_policy: '',
|
||||
testimonials_policy: '',
|
||||
competitors_never_mention: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -582,6 +651,80 @@ export function seedBrandbook(slug: string, label: string): Brandbook {
|
||||
],
|
||||
value_proposition:
|
||||
'Aprendes inglés usable desde la primera semana, con camino claro a certificado.',
|
||||
b2b_offering:
|
||||
'Programas de inglés profesional para equipos. Diferencia frente a apps conversacionales tipo Twenix: profesor fijo, sesiones de 60 minutos, programas por sector (Inglés Legal, IT, Financiero, Comercial Internacional), modalidad híbrida. Es Entidad Organizadora de FUNDAE (gestiona la bonificación directamente, no a través de un partner).',
|
||||
b2c_offering:
|
||||
'Cursos de inglés para adultos, preparación de exámenes oficiales (Cambridge, Aptis, IELTS), intensivos, clases 1to1 y plataforma online.',
|
||||
usp_differentiators:
|
||||
'Profesor fijo asignado + sesiones completas de 60 min (progreso estructural frente a conversación suelta de 15-20 min en apps). Gestión integral FUNDAE como Entidad Organizadora oficial. Especialización real por vertical profesional.',
|
||||
current_priorities:
|
||||
'B2C: captación de matrículas de adultos y preparación de certificados oficiales (Cambridge / Aptis). B2B: captación de empresas que quieren aprovechar su crédito anual FUNDAE con programas in-company u online estructurados.',
|
||||
price_and_offer_framing:
|
||||
'En B2B destacar el coste bonificado vía FUNDAE (coste cero o casi cero para la empresa, expresado en coste neto o gestión bonificada). En B2C destacar el valor del programa estructurado con profesor fijo, sin recurrir nunca a descuentos agresivos ni cuentas atrás.',
|
||||
website: 'blueparrotschool.com',
|
||||
copy_structure_rules:
|
||||
'Frases cortas. Un dato concreto vale más que un adjetivo. En primary_text: 2-4 frases, máx. 300 caracteres; la frase 1 amplía el hook, frases 2-3 dan un beneficio o dato concreto, frase final cierra con el CTA sin enlaces ni hashtags.',
|
||||
english_terms_policy:
|
||||
'Inglés en el copy: solo nombres propios de exámenes (Cambridge, Aptis, IELTS) o términos sin traducción natural (in-company). Nada de "boost", "unlock", "level up".',
|
||||
power_words: [
|
||||
'Profesor fijo',
|
||||
'60 minutos',
|
||||
'Entidad Organizadora FUNDAE',
|
||||
'Inglés de tu sector',
|
||||
'Programa estructurado',
|
||||
'Madrid y Málaga + Online',
|
||||
],
|
||||
allowed_ctas: [
|
||||
'Más información',
|
||||
'Solicitar presupuesto',
|
||||
'Reservar',
|
||||
'Contactar',
|
||||
'Registrarte',
|
||||
],
|
||||
b2b_personas: [
|
||||
{
|
||||
name: 'Marta (RRHH / Formación)',
|
||||
role: 'Responsable de RRHH o Desarrollo de Talento',
|
||||
goal: 'Quiere un proveedor llave en mano que no le dé trabajo administrativo.',
|
||||
pain_point:
|
||||
'Proveedores que obligan a RRHH a gestionar papeleo FUNDAE, perseguir asistencias o lidiar con quejas por cambios constantes de profesor.',
|
||||
buying_trigger:
|
||||
'Gestión FUNDAE 100% resuelta por Blue Parrot, informes periódicos de asistencia y progreso, y un único interlocutor.',
|
||||
},
|
||||
{
|
||||
name: 'Carlos (Socio / Director de Despacho o Consultora)',
|
||||
role: 'Director General o Socio en firma legal, financiera o consultoría',
|
||||
goal: 'Que su equipo hable con clientes internacionales con solvencia y sin hacer el ridículo.',
|
||||
pain_point:
|
||||
'Su equipo sabe gramática pero se bloquea al negociar, redactar contratos o defender propuestas en inglés técnico.',
|
||||
buying_trigger:
|
||||
'Compra rigor y especialización sectorial (Inglés Legal, Financiero, IT), no "clases de inglés general".',
|
||||
},
|
||||
{
|
||||
name: 'Lucía (Directora Comercial / Internacional)',
|
||||
role: 'Directora Comercial o de Expansión Internacional',
|
||||
goal: 'Resultados medibles en poco tiempo para presentaciones, negociación y llamadas internacionales.',
|
||||
pain_point:
|
||||
'Las apps conversacionales entretienen pero el equipo sigue sin cerrar reuniones complejas en inglés.',
|
||||
buying_trigger:
|
||||
'Sesiones de 60 minutos enfocadas en casos reales de su puesto de trabajo.',
|
||||
},
|
||||
],
|
||||
b2c_personas: [
|
||||
{
|
||||
name: 'Adulto Profesional / Opositor (25-45 años)',
|
||||
age_range: '25-45 años',
|
||||
situation:
|
||||
'Trabaja o busca salto laboral / necesita certificar nivel oficial (Cambridge B2/C1, Aptis, IELTS).',
|
||||
pain_point:
|
||||
'Ha fracasado o se ha estancado con apps y cursos grabados; siente que pierde el tiempo sin un profesor que le corrija y le marque ritmo.',
|
||||
goal: 'Avanzar de nivel de forma real o aprobar su examen oficial con horario compatible con su jornada.',
|
||||
},
|
||||
],
|
||||
customer_frustrations:
|
||||
'Estancamiento en el "nivel intermedio eterno" por usar apps de 15 minutos con profesores distintos cada día que solo dan charla superficial sin estructura ni seguimiento.',
|
||||
desired_transformation:
|
||||
'Pasar de evitar hablar en inglés o depender de traductores a desenvolverse con seguridad en reuniones de trabajo o aprobar su certificación oficial a la primera.',
|
||||
ads_usps: [
|
||||
'Metodología práctica',
|
||||
'Profes preparados',
|
||||
@@ -593,6 +736,58 @@ export function seedBrandbook(slug: string, label: string): Brandbook {
|
||||
'Ya probé y no aprendí',
|
||||
'Es caro',
|
||||
],
|
||||
objections: [
|
||||
{
|
||||
objection: 'Ya usamos una app',
|
||||
rebuttal:
|
||||
'La app mantiene, no avanza. Sin profesor fijo ni sesiones de 60 min no se sale del nivel intermedio.',
|
||||
},
|
||||
{
|
||||
objection: 'Es caro',
|
||||
rebuttal:
|
||||
'Bonificación FUNDAE gestionada directamente por nosotros; coste mínimo por empleado y mes.',
|
||||
},
|
||||
{
|
||||
objection: 'No tenemos tiempo',
|
||||
rebuttal:
|
||||
'Sesiones in-company u online integradas dentro del horario laboral.',
|
||||
},
|
||||
],
|
||||
winning_hooks: [
|
||||
'Tu equipo no necesita más inglés. Necesita el inglés de su trabajo.',
|
||||
'FUNDAE lo paga. Nosotros lo gestionamos.',
|
||||
'Un profesor. Sesenta minutos. Cada semana. Así se avanza.',
|
||||
'El inglés legal no se aprende en una app.',
|
||||
],
|
||||
preferred_angles: [
|
||||
'objeción tiempo',
|
||||
'coste FUNDAE',
|
||||
'contraste app vs programa real',
|
||||
'especialización por sector (Legal / IT / Finanzas)',
|
||||
'cero carga para RRHH',
|
||||
'certificación oficial (Cambridge / Aptis)',
|
||||
],
|
||||
claims_forbidden: [
|
||||
'Prometer niveles o plazos cerrados ("B2 en 3 meses").',
|
||||
'Comparar nombrando explícitamente a la competencia (ni Twenix ni ninguna).',
|
||||
'Descuentos agresivos, cuentas atrás, "última oportunidad".',
|
||||
'Fotografía de stock genérica de "gente feliz con portátil" con aspecto de banco de imágenes.',
|
||||
'Deformar el logo, recolorearlo o recrearlo (se coloca siempre el archivo oficial en plantilla).',
|
||||
'Empezar un hook por "¿Sabías que", "Descubre" o "Imagina".',
|
||||
],
|
||||
competitors_never_mention: ['Twenix', 'Preply', 'Italki', 'Open English', "What's Up!"],
|
||||
emoji_policy:
|
||||
'Prohibidos los emojis en creatividades de imagen. En copy (primary_text), como mucho uno al inicio y solo en B2C (nunca en B2B).',
|
||||
testimonials_policy:
|
||||
'Prohibidos testimonios inventados o con nombre y cargo de personas que no existen. Si no hay un caso real aprobado, no se usa testimonio.',
|
||||
surface_rules:
|
||||
'Azul (#2A4D9C) y blanco (#FFFFFF) como base. Amarillo (#FBC02D) solo para resaltar una palabra, un dato o el CTA. Negro (#000000) reservado a mensajes de formalidad o urgencia (FUNDAE que caduca, manifiesto). Ningún otro color.',
|
||||
typography_headline: 'Poppins Extrabold (siempre en titulares / hook). Nunca otra fuente.',
|
||||
typography_body: 'Poppins Regular (en cuerpo y línea de apoyo / support).',
|
||||
image_text_rules:
|
||||
'Máximo 2 niveles en imagen (titular hook + línea de apoyo support). El titular no supera 8 palabras. Margen seguro mínimo del 8% en todos los lados; nunca texto en el 15% superior ni inferior.',
|
||||
photography_style:
|
||||
'Fotografía real de aula, oficina o reunión con luz natural; personas adultas de 25-50 años en contexto de trabajo; sin poses de catálogo. Si es ilustración o composición gráfica, geometría limpia, mucho aire y una sola idea.',
|
||||
ads_funnel: ['Awareness Reel', 'Consideración carrusel', 'Conversión test'],
|
||||
linkedin_themes: [
|
||||
'Inglés profesional',
|
||||
@@ -878,6 +1073,51 @@ export function parseBrandbook(raw: unknown): Brandbook | null {
|
||||
source_excerpts: Array.isArray(partial.source_excerpts)
|
||||
? partial.source_excerpts
|
||||
: [],
|
||||
b2b_offering: String(partial.b2b_offering || ''),
|
||||
b2c_offering: String(partial.b2c_offering || ''),
|
||||
usp_differentiators: String(partial.usp_differentiators || ''),
|
||||
current_priorities: String(partial.current_priorities || ''),
|
||||
website: String(partial.website || ''),
|
||||
copy_structure_rules: String(partial.copy_structure_rules || ''),
|
||||
english_terms_policy: String(partial.english_terms_policy || ''),
|
||||
power_words: arr(partial.power_words),
|
||||
allowed_ctas: arr(partial.allowed_ctas),
|
||||
surface_rules: String(partial.surface_rules || ''),
|
||||
image_text_rules: String(partial.image_text_rules || ''),
|
||||
typography_headline: String(partial.typography_headline || ''),
|
||||
typography_body: String(partial.typography_body || ''),
|
||||
b2b_personas: Array.isArray(partial.b2b_personas)
|
||||
? partial.b2b_personas.map((p: any) => ({
|
||||
name: String(p.name || ''),
|
||||
role: String(p.role || ''),
|
||||
goal: String(p.goal || ''),
|
||||
pain_point: String(p.pain_point || ''),
|
||||
buying_trigger: String(p.buying_trigger || ''),
|
||||
}))
|
||||
: [],
|
||||
b2c_personas: Array.isArray(partial.b2c_personas)
|
||||
? partial.b2c_personas.map((p: any) => ({
|
||||
name: String(p.name || ''),
|
||||
age_range: String(p.age_range || ''),
|
||||
situation: String(p.situation || ''),
|
||||
pain_point: String(p.pain_point || ''),
|
||||
goal: String(p.goal || ''),
|
||||
}))
|
||||
: [],
|
||||
customer_frustrations: String(partial.customer_frustrations || ''),
|
||||
desired_transformation: String(partial.desired_transformation || ''),
|
||||
price_and_offer_framing: String(partial.price_and_offer_framing || ''),
|
||||
objections: Array.isArray(partial.objections)
|
||||
? partial.objections.map((o: any) => ({
|
||||
objection: String(o.objection || ''),
|
||||
rebuttal: String(o.rebuttal || ''),
|
||||
}))
|
||||
: [],
|
||||
winning_hooks: arr(partial.winning_hooks),
|
||||
preferred_angles: arr(partial.preferred_angles),
|
||||
emoji_policy: String(partial.emoji_policy || ''),
|
||||
testimonials_policy: String(partial.testimonials_policy || ''),
|
||||
competitors_never_mention: arr(partial.competitors_never_mention),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1046,3 +1286,356 @@ export function primaryLogo(book: Brandbook): BrandAsset | undefined {
|
||||
) || book.kit_assets.find((a) => a.kind === 'logo')
|
||||
);
|
||||
}
|
||||
|
||||
export type OperativeAdsConfig = {
|
||||
image_ai?: {
|
||||
photo_style?: string;
|
||||
rule_1_5_seconds?: string;
|
||||
forbidden_scenes?: string;
|
||||
people_count_rules?: string;
|
||||
negative_space_prompt?: string;
|
||||
};
|
||||
campaign_rules?: {
|
||||
b2c_detection_rules?: string;
|
||||
b2b_detection_rules?: string;
|
||||
fallback_audience_rule?: string;
|
||||
extra_claude_instructions?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function compileClientSkillFromBrandbook(
|
||||
book: Brandbook,
|
||||
technicalOps?: OperativeAdsConfig,
|
||||
clientName?: string,
|
||||
slug?: string
|
||||
): string {
|
||||
const name = clientName || book.brand_name || 'Cliente';
|
||||
const cSlug = slug || name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`# Skill de cliente · ${name} (slug: ${cSlug})`);
|
||||
lines.push('');
|
||||
lines.push(
|
||||
`> Esta skill define cómo habla y cómo se ve ${name} en cualquier creatividad. Se inyecta como contexto fijo a Claude en cada petición de relevo. Si algo aquí contradice un brief puntual, manda esta skill.`
|
||||
);
|
||||
lines.push('');
|
||||
|
||||
// 1. Quién es
|
||||
lines.push('## Quién es');
|
||||
lines.push('');
|
||||
const summary =
|
||||
book.elevator_pitch ||
|
||||
book.mission ||
|
||||
book.positioning ||
|
||||
`${name} — actividad y propuesta de valor.`;
|
||||
lines.push(summary);
|
||||
lines.push('');
|
||||
if (book.b2b_offering) {
|
||||
lines.push(`- **B2B (empresas):** ${book.b2b_offering}`);
|
||||
}
|
||||
if (book.b2c_offering) {
|
||||
lines.push(`- **B2C (particulares):** ${book.b2c_offering}`);
|
||||
}
|
||||
const usps =
|
||||
book.usp_differentiators || book.differentiators?.join(' · ') || '';
|
||||
if (usps) {
|
||||
lines.push(`- **Diferenciación clave:** ${usps}`);
|
||||
}
|
||||
if (book.current_priorities) {
|
||||
lines.push(
|
||||
`- **Prioridades actuales y foco comercial:** ${book.current_priorities}`
|
||||
);
|
||||
}
|
||||
const priceFraming =
|
||||
book.price_and_offer_framing || book.pricing_notes || '';
|
||||
if (priceFraming) {
|
||||
lines.push(`- **Enfoque de precio / oferta:** ${priceFraming}`);
|
||||
}
|
||||
if (book.website) {
|
||||
lines.push('');
|
||||
lines.push(`Web: ${book.website}`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// 2. A quién le hablamos
|
||||
lines.push('## A quién le hablamos');
|
||||
lines.push('');
|
||||
if (book.b2b_personas && book.b2b_personas.length > 0) {
|
||||
lines.push('**B2B**');
|
||||
for (const p of book.b2b_personas) {
|
||||
const details = [p.role, p.goal, p.pain_point, p.buying_trigger]
|
||||
.filter(Boolean)
|
||||
.join('. ');
|
||||
lines.push(`- *${p.name}* — ${details}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
if (book.b2c_personas && book.b2c_personas.length > 0) {
|
||||
lines.push('**B2C**');
|
||||
for (const p of book.b2c_personas) {
|
||||
const details = [p.age_range, p.situation, p.pain_point, p.goal]
|
||||
.filter(Boolean)
|
||||
.join('. ');
|
||||
lines.push(`- *${p.name}* — ${details}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
if (
|
||||
(!book.b2b_personas || book.b2b_personas.length === 0) &&
|
||||
(!book.b2c_personas || book.b2c_personas.length === 0) &&
|
||||
book.personas &&
|
||||
book.personas.length > 0
|
||||
) {
|
||||
for (const p of book.personas) {
|
||||
const details = [p.description, p.pains, p.goals, p.channels]
|
||||
.filter(Boolean)
|
||||
.join('. ');
|
||||
lines.push(`- *${p.name}* — ${details}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
const frust =
|
||||
book.customer_frustrations ||
|
||||
(book.barriers?.length ? book.barriers.join('; ') : '');
|
||||
if (frust) {
|
||||
lines.push(`- **Frustración principal previa:** ${frust}`);
|
||||
}
|
||||
const transf = book.desired_transformation || book.brand_promise || '';
|
||||
if (transf) {
|
||||
lines.push(`- **Transformación que buscan:** ${transf}`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// 3. Cómo habla
|
||||
lines.push('## Cómo habla');
|
||||
lines.push('');
|
||||
if (book.tone) lines.push(`- **Tono:** ${book.tone}`);
|
||||
const posture =
|
||||
book.positioning ||
|
||||
(book.personality?.length ? book.personality.join(', ') : '');
|
||||
if (posture) lines.push(`- **Postura:** ${posture}`);
|
||||
const langPolicy =
|
||||
book.languages && book.languages.length > 0
|
||||
? book.languages.includes('es')
|
||||
? 'Español de España, tratamiento de tú.'
|
||||
: book.languages.join(', ')
|
||||
: 'Español de España, tratamiento de tú.';
|
||||
lines.push(`- **Idioma y tratamiento:** ${langPolicy}`);
|
||||
const copyRules =
|
||||
book.copy_structure_rules ||
|
||||
(book.voice_do?.length
|
||||
? book.voice_do.join('; ')
|
||||
: 'Frases cortas. Un dato concreto vale más que un adjetivo.');
|
||||
lines.push(`- **Estructura y estilo:** ${copyRules}`);
|
||||
const englishPolicy =
|
||||
book.english_terms_policy ||
|
||||
(book.vocabulary_avoid?.length
|
||||
? `Evitar términos y jerga en inglés sin traducción: ${book.vocabulary_avoid.join(', ')}`
|
||||
: '');
|
||||
if (englishPolicy) {
|
||||
lines.push(`- **Idiomas / anglicismos en el copy:** ${englishPolicy}`);
|
||||
}
|
||||
const powers =
|
||||
book.power_words && book.power_words.length > 0
|
||||
? book.power_words
|
||||
: book.vocabulary_prefer;
|
||||
if (powers && powers.length > 0) {
|
||||
lines.push(`- **Conceptos fuerza:** ${powers.join(' · ')}`);
|
||||
}
|
||||
const ctas =
|
||||
book.allowed_ctas && book.allowed_ctas.length > 0
|
||||
? book.allowed_ctas
|
||||
: book.ads_ctas;
|
||||
if (ctas && ctas.length > 0) {
|
||||
lines.push(`- **CTAs permitidos:** ${ctas.join(', ')}`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// 4. Argumentos y objeciones
|
||||
const winArgs = book.ads_usps?.length
|
||||
? book.ads_usps
|
||||
: book.proof_points?.length
|
||||
? book.proof_points
|
||||
: book.differentiators;
|
||||
if (winArgs && winArgs.length > 0) {
|
||||
lines.push('### Argumentos que funcionan');
|
||||
winArgs.forEach((arg, idx) => {
|
||||
lines.push(`${idx + 1}. ${arg}`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (book.objections && book.objections.length > 0) {
|
||||
lines.push('### Objeciones a desactivar');
|
||||
for (const obj of book.objections) {
|
||||
lines.push(`- "${obj.objection}" → ${obj.rebuttal}`);
|
||||
}
|
||||
lines.push('');
|
||||
} else if (book.ads_objections && book.ads_objections.length > 0) {
|
||||
lines.push('### Objeciones a desactivar');
|
||||
for (const obj of book.ads_objections) {
|
||||
lines.push(`- "${obj}"`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (book.proof_points && book.proof_points.length > 0) {
|
||||
lines.push('### Pruebas y datos reales verificados');
|
||||
for (const pt of book.proof_points) {
|
||||
lines.push(`- ${pt}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const angles =
|
||||
book.preferred_angles && book.preferred_angles.length > 0
|
||||
? book.preferred_angles
|
||||
: book.ads_angles;
|
||||
if (angles && angles.length > 0) {
|
||||
lines.push(
|
||||
`### Ángulos recomendados para rotar\n- ${angles.join(' · ')}\n`
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Prohibido
|
||||
lines.push('## Prohibido');
|
||||
const forbidden =
|
||||
book.claims_forbidden && book.claims_forbidden.length > 0
|
||||
? book.claims_forbidden
|
||||
: [
|
||||
'Promesas irreales o plazos garantizados sin base.',
|
||||
'Fotografía genérica con aspecto artificial.',
|
||||
'Deformar o recrear el logo mediante IA.',
|
||||
];
|
||||
for (const item of forbidden) {
|
||||
lines.push(`- ${item}`);
|
||||
}
|
||||
if (
|
||||
book.competitors_never_mention &&
|
||||
book.competitors_never_mention.length > 0
|
||||
) {
|
||||
lines.push(
|
||||
`- Prohibido mencionar a competidores (${book.competitors_never_mention.join(', ')}).`
|
||||
);
|
||||
}
|
||||
if (book.emoji_policy) {
|
||||
lines.push(`- **Emojis:** ${book.emoji_policy}`);
|
||||
}
|
||||
if (book.testimonials_policy) {
|
||||
lines.push(`- **Testimonios:** ${book.testimonials_policy}`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// 6. Identidad visual
|
||||
lines.push('## Identidad visual');
|
||||
lines.push('');
|
||||
if (book.colors && book.colors.length > 0) {
|
||||
const paletteStr = book.colors
|
||||
.map((c) => `${c.name} \`${c.hex}\` (${c.usage || c.role})`)
|
||||
.join(' · ');
|
||||
lines.push(`- **Paleta exacta y única:** ${paletteStr}`);
|
||||
}
|
||||
if (book.surface_rules) {
|
||||
lines.push(`- **Uso de superficies:** ${book.surface_rules}`);
|
||||
}
|
||||
const headFont =
|
||||
book.typography_headline ||
|
||||
book.fonts_kit?.find(
|
||||
(f) => f.role === 'headline' || f.role === 'display'
|
||||
)?.family ||
|
||||
'N/A';
|
||||
const bodyFont =
|
||||
book.typography_body ||
|
||||
book.fonts_kit?.find((f) => f.role === 'body')?.family ||
|
||||
'N/A';
|
||||
if (headFont !== 'N/A' || bodyFont !== 'N/A') {
|
||||
lines.push(
|
||||
`- **Tipografía:** Titulares: ${headFont}. Cuerpo/Apoyo: ${bodyFont}.`
|
||||
);
|
||||
}
|
||||
const logoText =
|
||||
book.logo_usage ||
|
||||
'Logo oficial de marca con área de protección respetada en esquina; nunca centrado ni dominando.';
|
||||
lines.push(`- **Logo:** ${logoText}`);
|
||||
|
||||
const photoStyle =
|
||||
technicalOps?.image_ai?.photo_style ||
|
||||
book.photography_style ||
|
||||
'Fotografía realista con luz natural, personas reales en contexto del servicio, sin poses forzadas.';
|
||||
lines.push(`- **Estilo de imagen:** ${photoStyle}`);
|
||||
|
||||
const rule15 =
|
||||
technicalOps?.image_ai?.rule_1_5_seconds ||
|
||||
'La escena debe mostrar claramente el producto o servicio en menos de 1,5 segundos sin necesidad de leer el texto.';
|
||||
lines.push(`- **Regla de lectura en 1,5 s:** ${rule15}`);
|
||||
|
||||
const forbiddenScenes =
|
||||
technicalOps?.image_ai?.forbidden_scenes ||
|
||||
(book.imagery_dont?.length
|
||||
? book.imagery_dont.join('; ')
|
||||
: 'Escenas ambiguas que puedan confundirse con otro sector o stock genérico.');
|
||||
lines.push(`- **Escenas a evitar en imagen:** ${forbiddenScenes}`);
|
||||
|
||||
const peopleCount =
|
||||
technicalOps?.image_ai?.people_count_rules ||
|
||||
'1 persona si el mensaje es individual; 2 personas si muestra interacción profesional.';
|
||||
lines.push(`- **Personas en escena:** ${peopleCount}`);
|
||||
|
||||
const negSpace =
|
||||
technicalOps?.image_ai?.negative_space_prompt ||
|
||||
'the upper third of the frame is empty wall or sky. no text, no logos, no watermarks, photorealistic, vertical 4:5 composition with clear negative space for a headline.';
|
||||
lines.push(`- **Espacio negativo (image_prompt):** ${negSpace}`);
|
||||
|
||||
const imgText =
|
||||
book.image_text_rules ||
|
||||
'Máximo 2 niveles (titular ≤ 8 palabras + apoyo ≤ 12 palabras). Margen mínimo del 8% en todos los lados.';
|
||||
lines.push(`- **Texto en imagen:** ${imgText}`);
|
||||
lines.push('');
|
||||
|
||||
// 7. Audiencia por campaña
|
||||
lines.push('## Audiencia por campaña');
|
||||
lines.push('');
|
||||
lines.push(
|
||||
'El brief trae `audience`. Si no viene, se deduce del nombre de campaña:'
|
||||
);
|
||||
const b2cDet =
|
||||
technicalOps?.campaign_rules?.b2c_detection_rules ||
|
||||
'Campañas B2C: dolor personal, cursos individuales, examen oficial. Habla siempre de "tu".';
|
||||
lines.push(`- **B2C:** ${b2cDet}`);
|
||||
const b2bDet =
|
||||
technicalOps?.campaign_rules?.b2b_detection_rules ||
|
||||
'Campañas B2B: foco en equipos, formación para empresas, bonificación. Habla siempre de "tu equipo".';
|
||||
lines.push(`- **B2B:** ${b2bDet}`);
|
||||
const fallbackAud =
|
||||
technicalOps?.campaign_rules?.fallback_audience_rule ||
|
||||
'Si el brief no trae audience y no se puede deducir del nombre de campaña, se pregunta en notes y se genera en B2C.';
|
||||
lines.push(fallbackAud);
|
||||
lines.push('');
|
||||
|
||||
// 8. Ejemplos de hooks
|
||||
const hooks =
|
||||
book.winning_hooks && book.winning_hooks.length > 0
|
||||
? book.winning_hooks
|
||||
: book.slogan_variants;
|
||||
if (hooks && hooks.length > 0) {
|
||||
lines.push(
|
||||
'## Ejemplos de hooks que han funcionado (referencia de registro, no para copiar)'
|
||||
);
|
||||
for (const h of hooks) {
|
||||
lines.push(`- "${h}"`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const extra =
|
||||
technicalOps?.campaign_rules?.extra_claude_instructions?.trim() ||
|
||||
book.freeform_notes?.trim();
|
||||
if (extra) {
|
||||
lines.push('## Instrucciones adicionales específicas del cliente');
|
||||
lines.push(extra);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user