diff --git a/downloads/tiktok/tt_1777635599428.mp4 b/downloads/tiktok/tt_1777635599428.mp4 deleted file mode 100644 index 32a006c..0000000 Binary files a/downloads/tiktok/tt_1777635599428.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777646870542.mp4 b/downloads/tiktok/tt_1777646870542.mp4 deleted file mode 100644 index 3561f8b..0000000 Binary files a/downloads/tiktok/tt_1777646870542.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777647710531.mp4 b/downloads/tiktok/tt_1777647710531.mp4 deleted file mode 100644 index 17e9551..0000000 Binary files a/downloads/tiktok/tt_1777647710531.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777729146884.mp4 b/downloads/tiktok/tt_1777729146884.mp4 deleted file mode 100644 index 1d02d16..0000000 Binary files a/downloads/tiktok/tt_1777729146884.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777729871518.mp4 b/downloads/tiktok/tt_1777729871518.mp4 deleted file mode 100644 index d9a99d5..0000000 Binary files a/downloads/tiktok/tt_1777729871518.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777730480815.mp4 b/downloads/tiktok/tt_1777730480815.mp4 deleted file mode 100644 index 59dd040..0000000 Binary files a/downloads/tiktok/tt_1777730480815.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777736432419.mp4 b/downloads/tiktok/tt_1777736432419.mp4 deleted file mode 100644 index a40f74b..0000000 Binary files a/downloads/tiktok/tt_1777736432419.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777736571066.mp4 b/downloads/tiktok/tt_1777736571066.mp4 deleted file mode 100644 index 37d2fa2..0000000 Binary files a/downloads/tiktok/tt_1777736571066.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777815981370.mp4 b/downloads/tiktok/tt_1777815981370.mp4 deleted file mode 100644 index 96f5e20..0000000 Binary files a/downloads/tiktok/tt_1777815981370.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777816021235.mp4 b/downloads/tiktok/tt_1777816021235.mp4 deleted file mode 100644 index f534dc7..0000000 Binary files a/downloads/tiktok/tt_1777816021235.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777816043002.mp4 b/downloads/tiktok/tt_1777816043002.mp4 deleted file mode 100644 index d46b672..0000000 Binary files a/downloads/tiktok/tt_1777816043002.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777817519511.mp4.part b/downloads/tiktok/tt_1777817519511.mp4.part deleted file mode 100644 index 19f19c4..0000000 Binary files a/downloads/tiktok/tt_1777817519511.mp4.part and /dev/null differ diff --git a/downloads/tiktok/tt_1777817519513.mp4 b/downloads/tiktok/tt_1777817519513.mp4 deleted file mode 100644 index 94155b3..0000000 Binary files a/downloads/tiktok/tt_1777817519513.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777817519514.mp4.part b/downloads/tiktok/tt_1777817519514.mp4.part deleted file mode 100644 index 306c19f..0000000 Binary files a/downloads/tiktok/tt_1777817519514.mp4.part and /dev/null differ diff --git a/downloads/tiktok/tt_1777817699114.mp4 b/downloads/tiktok/tt_1777817699114.mp4 deleted file mode 100644 index f86602f..0000000 Binary files a/downloads/tiktok/tt_1777817699114.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777817699118.mp4 b/downloads/tiktok/tt_1777817699118.mp4 deleted file mode 100644 index 8ac1af0..0000000 Binary files a/downloads/tiktok/tt_1777817699118.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777817699119.mp4 b/downloads/tiktok/tt_1777817699119.mp4 deleted file mode 100644 index 32eee37..0000000 Binary files a/downloads/tiktok/tt_1777817699119.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777817699120.mp4 b/downloads/tiktok/tt_1777817699120.mp4 deleted file mode 100644 index 9275871..0000000 Binary files a/downloads/tiktok/tt_1777817699120.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777817853639.mp4 b/downloads/tiktok/tt_1777817853639.mp4 deleted file mode 100644 index f86602f..0000000 Binary files a/downloads/tiktok/tt_1777817853639.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777817853644.mp4 b/downloads/tiktok/tt_1777817853644.mp4 deleted file mode 100644 index 8ac1af0..0000000 Binary files a/downloads/tiktok/tt_1777817853644.mp4 and /dev/null differ diff --git a/downloads/tiktok/tt_1777817853645.mp4 b/downloads/tiktok/tt_1777817853645.mp4 deleted file mode 100644 index e8baea6..0000000 Binary files a/downloads/tiktok/tt_1777817853645.mp4 and /dev/null differ diff --git a/package.json b/package.json index d2e7824..19db4de 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,11 @@ "dotenv": "^17.4.1", "google-trends-api": "^4.9.2", "grammy": "^1.42.0", + "helmet": "^8.1.0", "https-proxy-agent": "^9.0.0", "install": "^0.13.0", "ioredis": "^5.10.1", + "localtunnel": "^2.0.2", "lodash": "^4.18.1", "nestjs-telegraf": "^2.9.1", "openai": "^6.34.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8780594..8529fcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: grammy: specifier: ^1.42.0 version: 1.42.0 + helmet: + specifier: ^8.1.0 + version: 8.1.0 https-proxy-agent: specifier: ^9.0.0 version: 9.0.0 @@ -116,6 +119,9 @@ importers: ioredis: specifier: ^5.10.1 version: 5.10.1 + localtunnel: + specifier: ^2.0.2 + version: 2.0.2 lodash: specifier: ^4.18.1 version: 4.18.1 @@ -2072,6 +2078,9 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + axios@0.21.4: + resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + axios@1.15.0: resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} @@ -2282,6 +2291,9 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2408,6 +2420,15 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@4.3.2: + resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2962,6 +2983,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + helmet@8.1.0: + resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} + engines: {node: '>=18.0.0'} + hono@4.12.12: resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} engines: {node: '>=16.9.0'} @@ -3354,6 +3379,11 @@ packages: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} + localtunnel@2.0.2: + resolution: {integrity: sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==} + engines: {node: '>=8.3.0'} + hasBin: true + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -3505,6 +3535,9 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3633,6 +3666,9 @@ packages: zod: optional: true + openurl@1.1.1: + resolution: {integrity: sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4573,10 +4609,18 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@17.1.1: + resolution: {integrity: sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==} + engines: {node: '>=12'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -6919,9 +6963,15 @@ snapshots: aws-ssl-profiles@1.1.2: {} + axios@0.21.4(debug@4.3.2): + dependencies: + follow-redirects: 1.15.11(debug@4.3.2) + transitivePeerDependencies: + - debug + axios@1.15.0: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.15.11(debug@4.3.2) form-data: 4.0.5 proxy-from-env: 2.1.0 transitivePeerDependencies: @@ -7192,6 +7242,12 @@ snapshots: cli-width@4.1.0: {} + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -7305,6 +7361,10 @@ snapshots: csstype@3.2.3: {} + debug@4.3.2: + dependencies: + ms: 2.1.2 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -7690,7 +7750,9 @@ snapshots: flatted@3.4.2: {} - follow-redirects@1.15.11: {} + follow-redirects@1.15.11(debug@4.3.2): + optionalDependencies: + debug: 4.3.2 for-in@0.1.8: {} @@ -7878,6 +7940,8 @@ snapshots: dependencies: function-bind: 1.1.2 + helmet@8.1.0: {} + hono@4.12.12: {} hookified@1.15.1: {} @@ -8429,6 +8493,15 @@ snapshots: loader-runner@4.3.1: {} + localtunnel@2.0.2: + dependencies: + axios: 0.21.4(debug@4.3.2) + debug: 4.3.2 + openurl: 1.1.1 + yargs: 17.1.1 + transitivePeerDependencies: + - supports-color + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -8550,6 +8623,8 @@ snapshots: mri@1.2.0: {} + ms@2.1.2: {} + ms@2.1.3: {} msgpackr-extract@3.0.3: @@ -8669,6 +8744,8 @@ snapshots: optionalDependencies: zod: 4.3.6 + openurl@1.1.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -9616,8 +9693,20 @@ snapshots: yallist@3.1.1: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs@17.1.1: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.2: dependencies: cliui: 8.0.1 diff --git a/src/main.ts b/src/main.ts index a85c40e..f512231 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,8 @@ -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; +import {NestFactory} from '@nestjs/core'; +import {AppModule} from './app.module'; import {ValidationPipe} from "@nestjs/common"; import {DocumentBuilder, SwaggerModule} from "@nestjs/swagger"; +import helmet from "helmet"; // const { createBullBoard } = require('@bull-board/api'); // const { BullAdapter } = require('@bull-board/api/bullAdapter'); @@ -10,41 +11,43 @@ import {DocumentBuilder, SwaggerModule} from "@nestjs/swagger"; async function bootstrap() { - const app = await NestFactory.create(AppModule, { - snapshot: true, - }); + const app = await NestFactory.create(AppModule, { + snapshot: true, + }); - // Global validation pipe - app.useGlobalPipes( - new ValidationPipe({ - transform: true, - whitelist: true, - forbidNonWhitelisted: true, - }), - ); + // Global validation pipe + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + }), + ); + app.use(helmet()); + // CORS + app.enableCors(); - // CORS - app.enableCors(); + // Swagger + const config = new DocumentBuilder() + .setTitle('🔥 Trend Hunter API') + .setDescription( + 'API tìm kiếm và tổng hợp trends từ nhiều nguồn social media', + ) + .setVersion('1.0') + .addTag('Trends') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('docs', app, document); - // Swagger - const config = new DocumentBuilder() - .setTitle('🔥 Trend Hunter API') - .setDescription( - 'API tìm kiếm và tổng hợp trends từ nhiều nguồn social media', - ) - .setVersion('1.0') - .addTag('Trends') - .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('docs', app, document); + const port = process.env.PORT || 3000; + await app.listen(port); - const port = process.env.PORT || 3000; - await app.listen(port); - - console.log(` + console.log(` 🔥 X-News is running! 📡 API: http://localhost:${port} 📖 Swagger: http://localhost:${port}/docs `); } + bootstrap(); + diff --git a/src/modules/content-writer/dto/generate-comment.dto.ts b/src/modules/content-writer/dto/generate-comment.dto.ts index 5e34e38..26709ae 100644 --- a/src/modules/content-writer/dto/generate-comment.dto.ts +++ b/src/modules/content-writer/dto/generate-comment.dto.ts @@ -2,6 +2,7 @@ import { IsEnum, IsString, IsOptional, MaxLength } from 'class-validator'; import { ContentTone } from '../enum/tone.enum'; import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface"; +import {AngleEnum} from "../enum/angle.enum"; export class GenerateCommentDto { @IsString() @@ -10,7 +11,7 @@ export class GenerateCommentDto { @IsOptional() @IsString() - angle?: string; // góc nhìn muốn comment: "agree", "challenge", "add-info", "funny" + angle?: AngleEnum; // góc nhìn muốn comment: "agree", "challenge", "add-info", "funny" @IsString() language: languagePromptInterface.Language; diff --git a/src/modules/content-writer/dto/generate-quote.dto.ts b/src/modules/content-writer/dto/generate-quote.dto.ts index 264d6f0..b120c75 100644 --- a/src/modules/content-writer/dto/generate-quote.dto.ts +++ b/src/modules/content-writer/dto/generate-quote.dto.ts @@ -4,6 +4,7 @@ import { QuoteType } from '../enum/quote-type.enum'; import { ContentTone } from '../enum/tone.enum'; import { PostLength } from '../enum/post-length.enum'; import { AccountTier } from '../enum/account-tier.enum'; +import {AngleEnum} from "../enum/angle.enum"; export class GenerateQuoteDto { @IsString() @@ -39,7 +40,7 @@ export class GenerateQuoteDto { @IsOptional() @IsString() - yourAngle?: string; // Góc nhìn riêng của bạn muốn express + yourAngle?: AngleEnum; // Góc nhìn riêng của bạn muốn express @IsOptional() @IsBoolean() diff --git a/src/modules/content-writer/enum/angle.enum.ts b/src/modules/content-writer/enum/angle.enum.ts index d910d8a..cb5cafa 100644 --- a/src/modules/content-writer/enum/angle.enum.ts +++ b/src/modules/content-writer/enum/angle.enum.ts @@ -1,3 +1,5 @@ +import {ContentTone} from "./tone.enum"; + export enum AngleEnum { AGREE = 'agree', CHALLENGE = 'challenge', @@ -8,7 +10,26 @@ export enum AngleEnum { DEVIL_ADVOCATE = 'devil_advocate', EXPAND = 'expand', VALIDATE = 'validate', - CTA = 'cta' + CTA = 'cta', + + // === Empathy/Support angles === + WISH_RECOVERY = 'wish_recovery', // Chấn thương, bệnh tật + TRIBUTE = 'tribute', // Người đã mất, di sản + SOLIDARITY = 'solidarity', // Tragedy chung, community + PERSONAL_SUPPORT = 'personal_support', // Hỗ trợ cá nhân 1-1 + SHARED_GRIEF = 'shared_grief', +} + +export const EMPATHYTONE_ANGLE = new Set([ + AngleEnum.WISH_RECOVERY, + AngleEnum.TRIBUTE, + AngleEnum.SOLIDARITY, + AngleEnum.PERSONAL_SUPPORT, + AngleEnum.SHARED_GRIEF, +]); + +export function isEMPATHYToneAngle(angle: AngleEnum): boolean { + return EMPATHYTONE_ANGLE.has(angle); } export const ANGLE_HINTS: Record = { @@ -21,21 +42,44 @@ export const ANGLE_HINTS: Record = { [AngleEnum.DEVIL_ADVOCATE]: `Play devil's advocate. Present the opposite view fairly without being hostile`, [AngleEnum.EXPAND]: 'Take one point from the post and zoom in deeper with more nuance', [AngleEnum.VALIDATE]: `Affirm the post's point with evidence or strong agreement, boost credibility`, - [AngleEnum.CTA]: 'End with a soft call-to-action: ask others to share their view' + [AngleEnum.CTA]: 'End with a soft call-to-action: ask others to share their view', + + [AngleEnum.WISH_RECOVERY]: '', + [AngleEnum.TRIBUTE]: '', + [AngleEnum.SOLIDARITY]: '', + [AngleEnum.PERSONAL_SUPPORT]: '', + [AngleEnum.SHARED_GRIEF]: '', } -export const ANGLE_HINTS_TELEGRAM_BUTTON: Record = { +export const get_ANGLE_HINTS_TELEGRAM_BUTTON=(tone:ContentTone)=>{ + let buttons = DEFAULT_ANGLE_HINTS_TELEGRAM_BUTTON; + if(tone === ContentTone.EMPATHETIC) { + return { + ...buttons, + ...EMPATHETIC_ANGLE_TELEGRAM_BUTTON + } + } + return buttons; +} +export const DEFAULT_ANGLE_HINTS_TELEGRAM_BUTTON: Partial> = { [AngleEnum.AGREE]: {text: 'Đồng ý'}, [AngleEnum.CHALLENGE]: {text: 'Không đồng ý'}, [AngleEnum.ADD_INFO]: {text: 'thêm thông tin liên quan hữu ích'}, [AngleEnum.FUNNY]: {text: 'Hóm hỉnh, hài hước nhẹ nhàng, không gây khó chịu'}, [AngleEnum.QUESTION]: {text: 'Đặt một câu hỏi tiếp theo thông minh'}, - [AngleEnum.RELATE]: {text: 'Chia sẻ một trải nghiệm \n hoặc cảm xúc cá nhân tương tự như bài đăng gốc.'}, + [AngleEnum.RELATE]: {text: 'Chia sẻ một trải nghiệm hoặc cảm xúc cá nhân tương tự như bài đăng gốc.'}, [AngleEnum.DEVIL_ADVOCATE]: { - text: `Hãy đóng vai trò người phản biện. \n Trình bày quan điểm trái chiều một cách công bằng mà không tỏ ra thù địch.` + text: `Hãy đóng vai trò người phản biện.Trình bày quan điểm trái chiều một cách công bằng mà không tỏ ra thù địch.` }, - [AngleEnum.EXPAND]: {text: 'expand-Chọn một điểm từ bài viết và phân tích sâu hơn với nhiều sắc thái khác nhau.'}, + [AngleEnum.EXPAND]: {text: 'expand-Chọn 1điểm phân tích sâuhơn với nhiều sắc thái khác nhau.'}, [AngleEnum.VALIDATE]: { - text: `validate-Khẳng định luận điểm của bài đăng bằng bằng chứng hoặc sự đồng tình mạnh mẽ, tăng cường độ tin cậy.` + text: `validate-Khẳngđịnhluậnđiểm = bằngchứng hoặc sựđồngtìnhmạnhmẽ, tăng cường độ tin cậy.` }, - [AngleEnum.CTA]: {text: 'cta-Kết thúc bằng lời kêu gọi hành động nhẹ nhàng'} + [AngleEnum.CTA]: {text: 'cta-Kết thúc bằng lời kêu gọi hành động nhẹ nhàng'}, +} +export const EMPATHETIC_ANGLE_TELEGRAM_BUTTON: Partial> = { + [AngleEnum.WISH_RECOVERY]: {text: 'Chúc hồi phục'}, + [AngleEnum.TRIBUTE]: {text: 'Tưởng nhớ / RIP'}, + [AngleEnum.SOLIDARITY]: {text: 'Đồng lòng / Đứng cùng'}, + [AngleEnum.PERSONAL_SUPPORT]: {text: 'Hỗ trợ cá nhân'}, + [AngleEnum.SHARED_GRIEF]: {text: 'Cùng nỗi buồn'}, } \ No newline at end of file diff --git a/src/modules/content-writer/prompts/comment.templates.ts b/src/modules/content-writer/prompts/comment.templates.ts index dfd6b1d..803d907 100644 --- a/src/modules/content-writer/prompts/comment.templates.ts +++ b/src/modules/content-writer/prompts/comment.templates.ts @@ -3,12 +3,13 @@ import {Language} from "../../../common/interfaces/language.prompt.interface"; import {calculateLengthBudget} from "../../../common/utils/token-calculator"; import {Platform} from "../enum/platform.enum"; -import {ANGLE_HINTS} from "../enum/angle.enum"; +import {AngleEnum, isEMPATHYToneAngle} from "../enum/angle.enum"; import {ContentTone, isEdgyTone} from "../enum/tone.enum"; import {buildEdgySystemPrompt, mapToneToStarterCategory} from "./quote.templates"; import {getToneInstruction} from "./edgy-tones"; -import {LANGUAGE_LOCK} from "./templates"; import {getJpContextBlock} from "./jp-cultural-context"; +import {getAngleHint} from "./empathy-angles"; +import {buildEmpathySystemProgram} from "./empathy-system"; export const COMMENT_SYSTEM_PROMPTS = { en: 'You write casual, human replies on X. Sound like a real person, NOT AI. No hashtags unless natural.', @@ -31,7 +32,7 @@ export const COMMENT_SYSTEM_PROMPTS = { export function buildCommentPrompt(params: { originalPost: string; - angle?: string; + angle?: AngleEnum; language: Language; persona?: string; tone?: ContentTone; @@ -44,9 +45,18 @@ export function buildCommentPrompt(params: { // question: 'question:Đặt một câu hỏi tiếp theo thông minh', // }; const tone = params.tone ?? ContentTone.CASUAL; - const system = isEdgyTone(tone) - ? buildEdgySystemPrompt(params.language) - : COMMENT_SYSTEM_PROMPTS[params.language]; + + let system = '' + if (isEdgyTone(tone)) { + system = buildEdgySystemPrompt(params.language); + } else if (isEMPATHYToneAngle(params.angle!)) { + system = buildEmpathySystemProgram(params.language); + } else { + system = COMMENT_SYSTEM_PROMPTS[params.language]; + } + // const system = isEdgyTone(tone) + // ? buildEdgySystemPrompt(params.language) + // : COMMENT_SYSTEM_PROMPTS[params.language]; const toneInstruction = getToneInstruction(tone, params.language, '-') @@ -60,13 +70,18 @@ export function buildCommentPrompt(params: { : ''; const budget = calculateLengthBudget(Platform.X, params.language); + const angleInstruction = getAngleHint(params.angle!, params.language, '- ANGLE:'); + + console.debug({toneInstruction, angleInstruction}); + const user = [ `Original X post:\n"""${params.originalPost}"""`, ``, `Write a comment/reply target length: ${budget.minChars}-${budget.maxChars} characters:`, `[Target Language: ${params.language}] Rewrite strictly in ${params.language} only.`, - params.angle ? `- Angle: ${ANGLE_HINTS[params.angle] ?? params.angle}` : '- Angle: natural reaction', + angleInstruction, + // params.angle ? `- Angle: ${ANGLE_HINTS[params.angle] ?? params.angle}` : '- Angle: natural reaction', params.persona ? `- Speak as: ${params.persona}` : '', toneInstruction, `- Sound HUMAN, not AI. No "Great post!" openings.`, @@ -77,5 +92,5 @@ export function buildCommentPrompt(params: { `- Output ONLY the reply text.`, ].filter(Boolean).join('\n'); - return {system:system, user}; + return {system: system, user}; } diff --git a/src/modules/content-writer/prompts/empathy-angles.ts b/src/modules/content-writer/prompts/empathy-angles.ts new file mode 100644 index 0000000..a042d28 --- /dev/null +++ b/src/modules/content-writer/prompts/empathy-angles.ts @@ -0,0 +1,305 @@ +// prompts/empathy-angles.ts + + +import {Language} from "../../../common/interfaces/language.prompt.interface"; +import {ANGLE_HINTS, AngleEnum, isEMPATHYToneAngle} from "../enum/angle.enum"; +import {getToneHint} from "./templates"; + +export interface EmpathyAngleSpec { + name: Partial>; + instruction: Partial>; + examples: Partial>; + avoid: Partial>; + contextNote: Partial>; +} + +export const EMPATHY_ANGLE_SPECS: Partial> = { + [AngleEnum.WISH_RECOVERY]: { + name: { + en: 'Wish for recovery', + vi: 'Chúc hồi phục', + ja: '回復を願う', + ko: '회복 기원', + }, + instruction: { + en: 'Express genuine concern + wish for fast recovery. Acknowledge as a fan/observer. Short, sincere.', + vi: 'Bày tỏ lo lắng chân thành + mong chóng hồi phục. Đứng từ góc fan/người quan sát. Ngắn, chân thành.', + ja: 'ファン視点で心配と回復への願いを表現。短く誠実に。大げさにしない。', + ko: '팬/관찰자 시점에서 진심 어린 걱정과 빠른 회복 기원. 짧고 진실되게.', + }, + examples: { + en: [ + 'Damn, hate to see this. Wishing him a speedy recovery 🙏', + 'Tough break. Hope he comes back stronger.', + 'Praying for a clean recovery. The game won\'t be the same without him.', + ], + vi: [ + 'Buồn quá. Mong anh sớm hồi phục.', + 'Chấn thương đau lòng. Chờ ngày anh trở lại mạnh mẽ hơn.', + 'Cầu mong anh bình phục thật nhanh. Fan vẫn ở đây 🙏', + ], + ja: [ + '心配です…早く回復してほしい', + 'これは見るのが辛い。早期回復を祈ってます', + '無理せず、ゆっくり治してほしい。応援してます', + ], + ko: [ + '진짜 마음 아프네요. 빠른 쾌유 기원합니다', + '안타깝네요… 무리하지 말고 푹 쉬셨으면', + '꼭 건강한 모습으로 돌아오시길 🙏', + ], + }, + avoid: { + en: 'No "RIP" (he\'s alive!). No "deeply saddened" (AI-ish). No medical advice.', + vi: 'Không dùng "RIP" (còn sống!). Không "vô cùng đau xót" (AI-ish). Không cho lời khuyên y tế.', + ja: '「ご冥福」絶対NG(生きてる!)。大げさな表現避ける。医療アドバイス禁止。', + ko: '"명복" 절대 금지(살아있음!). 과장된 표현 피하기. 의료 조언 금지.', + }, + contextNote: { + en: 'Person is ALIVE but injured/sick. Tone is concerned but hopeful.', + vi: 'Người đó CÒN SỐNG nhưng bị thương/bệnh. Tone lo lắng nhưng hy vọng.', + ja: '本人は生きている。心配だが希望的なトーン。', + ko: '본인은 살아있음. 걱정스럽지만 희망적인 톤.', + }, + }, + + [AngleEnum.TRIBUTE]: { + name: { + en: 'Tribute / RIP', + vi: 'Tưởng nhớ / RIP', + ja: '追悼', + ko: '추모', + }, + instruction: { + en: 'Honor the deceased. Acknowledge their impact/legacy. Sincere, not performative. Share a brief specific memory/impact if relevant.', + vi: 'Tôn vinh người đã khuất. Ghi nhận đóng góp/di sản. Chân thành, không phô trương. Có thể kể 1 kỷ niệm/ấn tượng cụ thể.', + ja: '故人を偲ぶ。功績・影響を認める。誠実に、わざとらしくない。具体的な思い出/影響を一つ短く。', + ko: '고인을 기리기. 업적/영향 인정. 진실되게, 과시적이지 않게. 구체적인 추억/영향 하나 짧게.', + }, + examples: { + en: [ + 'A true legend. The way he changed the game will outlive all of us. RIP 🕊️', + 'Gutted. Grew up watching him. Rest in peace.', + 'No words. His [specific work/moment] meant everything. Thank you for everything.', + ], + vi: [ + 'Một huyền thoại thực sự. Cách anh thay đổi cuộc chơi sẽ còn mãi. Yên nghỉ nhé.', + 'Buồn quá. Lớn lên xem anh. Cầu mong anh yên nghỉ.', + 'Không lời nào diễn tả được. [Tác phẩm/khoảnh khắc] của anh là tuổi thơ của tôi. Cảm ơn anh tất cả.', + ], + ja: [ + 'ご冥福をお祈りします。あの[作品/瞬間]は今も心に残ってる', + '言葉が出ない…本当に偉大な方だった。安らかに', + '一つの時代が終わった気がする。ありがとうございました', + ], + ko: [ + '삼가 고인의 명복을 빕니다. [작품/순간]은 평생 잊지 못할 거예요', + '말이 안 나옵니다… 정말 큰 분이셨습니다. 편히 쉬세요', + '한 시대가 저무는 느낌입니다. 감사했습니다', + ], + }, + avoid: { + en: 'NO self-promo. NO "anyway, my project..." NO emoji spam (1-2 max). NO platitudes like "thoughts and prayers" alone.', + vi: 'KHÔNG quảng cáo bản thân. KHÔNG "btw, dự án của tôi...". KHÔNG spam emoji (1-2 là đủ). KHÔNG sáo rỗng kiểu "thành kính phân ưu" trống rỗng.', + ja: '自己宣伝絶対NG。「ところで〜」NG。絵文字スパムNG(1-2個まで)。空虚な定型句のみは避ける。', + ko: '자기 홍보 절대 금지. "그런데 제 프로젝트는~" 금지. 이모지 스팸 금지(1-2개). 공허한 상투어만은 피하기.', + }, + contextNote: { + en: 'Person has DIED. Be respectful. Specificity > generality. Brevity OK — silence is OK too.', + vi: 'Người đó ĐÃ MẤT. Tôn trọng. Cụ thể > chung chung. Ngắn cũng được — đôi khi im lặng là tôn trọng.', + ja: '故人。敬意を持って。具体性 > 一般論。短くてOK — 沈黙も尊重。', + ko: '고인. 존중하며. 구체성 > 일반론. 짧아도 OK — 침묵도 존중.', + }, + }, + + [AngleEnum.SOLIDARITY]: { + name: { + en: 'Solidarity / Stand with', + vi: 'Đồng lòng / Đứng cùng', + ja: '連帯', + ko: '연대', + }, + instruction: { + en: 'Express solidarity with affected group. "We are with you." Acknowledge difficulty without minimizing. Action-oriented if appropriate (donate, support, etc.).', + vi: 'Bày tỏ đồng lòng với nhóm bị ảnh hưởng. "Chúng ta cùng bạn." Ghi nhận khó khăn không giảm nhẹ. Hành động nếu phù hợp (quyên góp, hỗ trợ).', + ja: '被害を受けた人々への連帯を表明。「共にいる」気持ち。困難を矮小化せず認める。可能なら行動的に(寄付・支援)。', + ko: '피해자들과의 연대 표현. "함께 있다"는 마음. 어려움을 축소하지 않고 인정. 가능하면 행동 지향(기부, 지원).', + }, + examples: { + en: [ + 'Heart breaks for everyone affected. If anyone needs help with [X], DM me.', + 'Standing with [community]. This is devastating. What\'s the best way to support right now?', + 'No one should go through this alone. Sending real support, not just words.', + ], + vi: [ + 'Lòng đau cho tất cả mọi người bị ảnh hưởng. Ai cần hỗ trợ [X] có thể DM mình.', + 'Đứng cùng [cộng đồng]. Quá đau lòng. Cách tốt nhất để hỗ trợ lúc này là gì?', + 'Không ai nên trải qua điều này một mình. Gửi hỗ trợ thật, không chỉ lời nói.', + ], + ja: [ + '被災された皆様に心を寄せています。[具体的支援]できることがあれば言ってください', + '本当に胸が痛みます。今、一番有効な支援方法を共有してほしい', + '言葉だけじゃなく、実際にできることをやります', + ], + ko: [ + '피해 입으신 모든 분들께 마음을 보냅니다. [구체적 지원] 도울 일 있으면 말씀해 주세요', + '정말 가슴이 아픕니다. 지금 가장 효과적인 지원 방법을 알려주세요', + '말뿐 아니라 실제로 할 수 있는 걸 하겠습니다', + ], + }, + avoid: { + en: 'No empty "thoughts and prayers" if you can offer concrete help. No making it about yourself. No political opportunism on tragedy.', + vi: 'Không "cầu nguyện" sáo rỗng nếu có thể giúp cụ thể. Không biến chuyện thành về mình. Không lợi dụng chính trị trên tragedy.', + ja: '具体的支援できるなら「祈ってます」だけは避ける。自分の話にしない。悲劇を政治利用しない。', + ko: '구체적 도움 가능하면 "기도합니다"만으로 끝내지 말기. 자기 얘기로 만들지 말기. 비극을 정치적으로 이용하지 말기.', + }, + contextNote: { + en: 'Group/community affected. Focus on THEM, not yourself.', + vi: 'Nhóm/cộng đồng bị ảnh hưởng. Tập trung vào HỌ, không phải bản thân.', + ja: '集団・コミュニティが影響を受けた。彼らに焦点、自分の話ではなく。', + ko: '집단/커뮤니티가 영향받음. 그들에 집중, 자기 얘기 아님.', + }, + }, + + [AngleEnum.PERSONAL_SUPPORT]: { + name: { + en: 'Personal support', + vi: 'Hỗ trợ cá nhân', + ja: '個人的サポート', + ko: '개인적 지원', + }, + instruction: { + en: 'Direct 1-on-1 support to someone sharing personal grief. Acknowledge feeling first, validate, then optionally offer ear/help. Don\'t fix unless asked.', + vi: 'Hỗ trợ trực tiếp 1-1 cho người chia sẻ nỗi buồn cá nhân. Ghi nhận cảm xúc TRƯỚC, validate, sau đó có thể đề nghị lắng nghe/giúp đỡ. Đừng "fix" trừ khi được hỏi.', + ja: '個人的な悲しみを共有した人への1対1サポート。まず感情を認める、validateする、必要なら聞き役を申し出る。求められない限り「解決」しない。', + ko: '개인적 슬픔을 공유한 사람에게 1대1 지원. 먼저 감정 인정, validate, 필요시 들어주기 제안. 요청 없으면 "해결" 시도 말기.', + }, + examples: { + en: [ + 'Hey, that\'s a lot to carry. Take your time. Here if you need to talk.', + 'I\'m so sorry. There\'s no right way to feel this — whatever you\'re feeling is valid.', + 'Sending love. No need to be okay right now.', + ], + vi: [ + 'Anh/chị ơi, nặng nề quá. Cứ từ từ thôi. Cần tâm sự gì cứ nói.', + 'Mình rất tiếc. Không có cách "đúng" để cảm nhận chuyện này — mọi cảm xúc đều ổn.', + 'Gửi tình thương. Không cần phải ổn ngay đâu.', + ], + ja: [ + '辛いですね…無理に元気にならなくていいです。いつでも話聞きます', + '本当にお辛いですね。感じることに「正しい」も「間違い」もないですよ', + '何も言えないけど、ここにいます。一人じゃないですよ', + ], + ko: [ + '많이 힘드시겠어요… 억지로 괜찮은 척하지 않아도 돼요. 언제든 얘기 들을게요', + '정말 마음 아프시겠어요. 어떻게 느끼시든 다 괜찮은 감정이에요', + '뭐라 말씀드려야 할지… 여기 있을게요. 혼자 아니에요', + ], + }, + avoid: { + en: 'NO "everything happens for a reason." NO "stay strong" (pressures them). NO unsolicited advice. NO "I know how you feel" (you don\'t).', + vi: 'KHÔNG "mọi chuyện đều có lý do". KHÔNG "cố lên" (tạo áp lực). KHÔNG khuyên khi không được hỏi. KHÔNG "anh hiểu mà" (bạn không).', + ja: '「すべてに意味がある」NG。「頑張って」(プレッシャーになる)NG。求められない助言NG。「気持ち分かるよ」(分からない)NG。', + ko: '"모든 일에는 이유가 있다" 금지. "힘내세요"(부담 줌) 금지. 요청 없는 조언 금지. "마음 알아요"(모름) 금지.', + }, + contextNote: { + en: 'Individual person sharing personal pain. Be a witness, not a fixer.', + vi: 'Cá nhân chia sẻ nỗi đau riêng. Là người chứng kiến, không phải người sửa.', + ja: '個人が個人的な痛みを共有。証人になる、解決者ではなく。', + ko: '개인이 개인적 고통 공유. 증인이 되기, 해결사 아님.', + }, + }, + + [AngleEnum.SHARED_GRIEF]: { + name: { + en: 'Shared grief', + vi: 'Cùng nỗi buồn', + ja: '共有する悲しみ', + ko: '함께하는 슬픔', + }, + instruction: { + en: 'You\'re also affected (fan of same person, member of same community). Share YOUR specific grief briefly. Connects through shared loss.', + vi: 'Bạn cũng bị ảnh hưởng (cùng fan, cùng cộng đồng). Chia sẻ nỗi buồn CỦA BẠN ngắn gọn. Kết nối qua mất mát chung.', + ja: '自分も影響を受けた立場(同じファン、同じコミュニティ)。自分の悲しみを短く共有。共通の喪失で繋がる。', + ko: '자신도 영향받은 입장(같은 팬, 같은 커뮤니티). 자신의 슬픔을 짧게 공유. 공통의 상실로 연결.', + }, + examples: { + en: [ + 'Sitting with this today. Grew up watching him. The [specific moment] stays with me.', + 'Can\'t process this. He was part of my entire fandom journey.', + 'My day stopped when I saw the news. We lost something real.', + ], + vi: [ + 'Ngồi đây với cảm xúc này. Lớn lên xem anh. [Khoảnh khắc cụ thể] sẽ ở mãi với tôi.', + 'Không xử lý nổi. Anh là một phần hành trình fan của tôi.', + 'Ngày của tôi dừng lại khi thấy tin. Chúng ta mất đi điều gì đó thật.', + ], + ja: [ + '今日はこの感情と一緒にいる。子供の頃から見てた。[具体的瞬間]は一生忘れない', + 'まだ受け入れられない。ファン人生の一部だった', + 'ニュース見て時が止まった。本物を失った気がする', + ], + ko: [ + '오늘은 이 감정과 함께 있는 중. 어릴 때부터 봤는데. [구체적 순간]은 평생 못 잊을 듯', + '아직 받아들이지 못하겠어요. 팬 인생의 일부였는데', + '뉴스 보고 시간이 멈췄어요. 진짜 무언가를 잃은 느낌', + ], + }, + avoid: { + en: 'No making it about you exclusively. Specific shared memory > generic "I\'m sad too."', + vi: 'Đừng biến hoàn toàn về mình. Kỷ niệm cụ thể chung > "tôi cũng buồn" chung chung.', + ja: '完全に自分の話にしないこと。具体的な共通の思い出 > 「私も悲しい」だけの一般論。', + ko: '완전히 자기 얘기로 만들지 말기. 구체적인 공유 추억 > "나도 슬프다" 일반론.', + }, + contextNote: { + en: 'Both you and the person posting are affected. Connect via shared experience.', + vi: 'Cả bạn và người đăng đều bị ảnh hưởng. Kết nối qua trải nghiệm chung.', + ja: 'あなたと投稿者の両方が影響を受けた。共通体験で繋がる。', + ko: '본인과 게시자 모두 영향받음. 공유 경험으로 연결.', + }, + }, +}; + +export const get_EMPATHY_ANGLE_TELEGRAM_BUTTONS = () => { + +} + +export function getEmpathyAngleInstruction( + angle: string, + language: Language, +): string { + const spec = EMPATHY_ANGLE_SPECS[angle]; + if (!spec) return ''; + + let s_examples = spec.examples[language]; + if (!!s_examples) { + //move qua language =en; + language = 'en'; + console.log('==> getEmpathyAngleInstruction=>fallback lang to eng') + } + s_examples = spec.examples[language]; + + // @ts-ignore + const examples = s_examples.slice(0, 3) + .map((ex, i) => ` ${i + 1}. ${ex}`).join('\n'); + + return [ + `💝 ANGLE: ${spec.name[language]}`, + `Context: ${spec.contextNote[language]}`, + `Instruction: ${spec.instruction[language]}`, + `Examples (style only, DON'T copy):`, + examples, + `⚠️ AVOID: ${spec.avoid[language]}`, + ].join('\n'); +} + +export function getAngleHint(angle: AngleEnum, language: Language, prefix = '- '): string { + if (!angle) return ''; + if (isEMPATHYToneAngle(angle)) { + return getEmpathyAngleInstruction(angle, language) + } + const agHint = ANGLE_HINTS[language]; + return agHint + ? `${prefix} ${agHint}` + : ``; +} \ No newline at end of file diff --git a/src/modules/content-writer/prompts/empathy-system.ts b/src/modules/content-writer/prompts/empathy-system.ts new file mode 100644 index 0000000..88336b8 --- /dev/null +++ b/src/modules/content-writer/prompts/empathy-system.ts @@ -0,0 +1,49 @@ +// prompts/empathy-system.ts + +import {Language} from "../../../common/interfaces/language.prompt.interface"; + + +export const buildEmpathySystemProgram = (lang: Language) => { + const prompts: Record = { + en: [ + 'You write empathetic comments on sensitive posts (loss, injury, grief, tragedy).', + 'You sound like a real human, not a sympathy card. Be specific, brief, sincere.', + 'Acknowledge feelings WITHOUT trying to fix. Validate WITHOUT minimizing.', + 'Brevity is respectful. 1-3 sentences usually best.', + 'Output ONLY the comment text. No preamble, no quotes, no disclaimers.', + ].join(' '), + cn: [ + 'You write empathetic comments on sensitive posts (loss, injury, grief, tragedy).', + 'You sound like a real human, not a sympathy card. Be specific, brief, sincere.', + 'Acknowledge feelings WITHOUT trying to fix. Validate WITHOUT minimizing.', + 'Brevity is respectful. 1-3 sentences usually best.', + 'Output ONLY the comment text. No preamble, no quotes, no disclaimers.', + ].join(' '), + + vi: [ + 'Bạn viết comment đồng cảm cho post nhạy cảm (mất mát, chấn thương, buồn, tragedy).', + 'Bạn giống người thật, không phải thiệp chia buồn. Cụ thể, ngắn gọn, chân thành.', + 'Ghi nhận cảm xúc, KHÔNG cố "sửa". Validate, KHÔNG giảm nhẹ.', + 'Ngắn gọn là tôn trọng. 1-3 câu thường tốt nhất.', + 'CHỈ output comment bằng Tiếng Việt. Không preamble, không disclaimer.', + ].join(' '), + + ja: [ + 'あなたはセンシティブな投稿(喪失、怪我、悲しみ、悲劇)に共感的なコメントを書きます。', + 'お悔やみカードではなく、生身の人間として書く。具体的、簡潔、誠実に。', + '感情を認める、「解決」しようとしない。validateする、矮小化しない。', + '簡潔さは敬意。1-3文がベスト。', + '日本語のコメント本文のみ出力。前置き・引用符・免責一切なし。', + ].join(' '), + + ko: [ + '민감한 게시물(상실, 부상, 슬픔, 비극)에 공감적 댓글을 작성합니다.', + '위로 카드가 아닌 실제 사람처럼. 구체적이고 간결하며 진실되게.', + '감정 인정, "해결" 시도 말기. validate, 축소 말기.', + '간결함이 존중. 1-3문장이 최선.', + '한국어 댓글 본문만 출력. 서두/인용/면책 없음.', + ].join(' '), + }; + + return prompts[lang]; +} \ No newline at end of file diff --git a/src/modules/content-writer/prompts/quote.templates.ts b/src/modules/content-writer/prompts/quote.templates.ts index 0eed1da..cdeccbe 100644 --- a/src/modules/content-writer/prompts/quote.templates.ts +++ b/src/modules/content-writer/prompts/quote.templates.ts @@ -9,6 +9,10 @@ import {ContentTone, isEdgyTone} from '../enum/tone.enum'; import {Language} from "../../../common/interfaces/language.prompt.interface"; import {getToneInstruction} from "./edgy-tones"; import {getJpContextBlock, JP_X_CULTURE} from "./jp-cultural-context"; +import {AngleEnum, isEMPATHYToneAngle} from "../enum/angle.enum"; +import {buildEmpathySystemProgram} from "./empathy-system"; +import {COMMENT_SYSTEM_PROMPTS} from "./comment.templates"; +import {getAngleHint} from "./empathy-angles"; // ============================================================ // SYSTEM PROMPTS — native per language (chống đổi ngôn ngữ) @@ -606,7 +610,7 @@ export interface QuotePromptParams { language: Language; tone?: ContentTone; persona?: string; - yourAngle?: string; + yourAngle?: AngleEnum; lengthRange: LengthRange; } @@ -670,9 +674,18 @@ export function buildQuotePrompt(params: QuotePromptParams): { } = params; // System prompt: nếu edgy tone, dùng version "unhinged" hơn - const system = isEdgyTone(tone ?? ContentTone.CASUAL) - ? buildEdgySystemPrompt(language) - : QUOTE_SYSTEM_PROMPTS[language]; + // const system = isEdgyTone(tone ?? ContentTone.CASUAL) + // ? buildEdgySystemPrompt(language) + // : QUOTE_SYSTEM_PROMPTS[language]; + + let system = ''; + if (isEdgyTone(tone ?? ContentTone.CASUAL)) { + system = buildEdgySystemPrompt(language); + } else if (isEMPATHYToneAngle(yourAngle!)) { + system = buildEmpathySystemProgram(language); + } else { + system = QUOTE_SYSTEM_PROMPTS[language]; + } const spec = QUOTE_TYPE_SPECS[quoteType]; @@ -702,6 +715,10 @@ export function buildQuotePrompt(params: QuotePromptParams): { // : ''; const toneInstruction = getToneInstruction(tone!, language) + const angleInstruction = getAngleHint(yourAngle!, language, 'Your specific angle:'); + + console.debug({toneInstruction, angleInstruction}); + // const user = [ // `=== ${authorLine} ===`, // `"""${originalPost}"""`, @@ -739,14 +756,15 @@ export function buildQuotePrompt(params: QuotePromptParams): { `"""${params.originalPost}"""`, ``, `=== Your quote-tweet task ===`, - `Quote type: ${spec.name[params.language]}`, - `Instruction: ${spec.instruction[params.language]}`, - `Avoid: ${spec.avoid[params.language]}`, + `Quote type: ${spec.name[language]}`, + `Instruction: ${spec.instruction[language]}`, + `Avoid: ${spec.avoid[language]}`, ``, `Length: ${params.lengthRange.min}-${params.lengthRange.max} chars (aim ~${params.lengthRange.sweet})`, toneInstruction, params.persona ? `Voice/persona: ${params.persona}` : '', - params.yourAngle ? `Your specific angle: ${params.yourAngle}` : '', + angleInstruction, + // params.yourAngle ? `Your specific angle: ${params.yourAngle}` : '', // 👇 INJECT JP CONTEXT jpContext, ``, diff --git a/src/modules/content-writer/prompts/templates.ts b/src/modules/content-writer/prompts/templates.ts index 91e4190..84973c4 100644 --- a/src/modules/content-writer/prompts/templates.ts +++ b/src/modules/content-writer/prompts/templates.ts @@ -72,21 +72,7 @@ export const STYLE_HINTS: Record = { [ContentStyle.THREAD]: 'Thread format. Start with hook tweet. Each point numbered. End with CTA or summary.', }; -const TONE_HINTS_TELEGRAM_BUTTON_DEFAULT = { - [ContentTone.PROFESSIONAL]: {text: 'chuyên nghiệp, rõ ràng, đáng tin cậy'}, - [ContentTone.CASUAL]: {text: 'Giản dị,thân thiện'}, - [ContentTone.HYPE]: {text: 'Hype-Hào hứng,tràn đầy năng lượng'}, - [ContentTone.URGENT]: {text: 'urgent'}, - [ContentTone.HUMOROUS]: {text: 'Dí dỏm, hài hước'}, - [ContentTone.INFORMATIVE]: {text: 'Thông tin, chính xác'}, - [ContentTone.EMPATHETIC]: {text: 'empathetic-Đồng cảm,thấu hiểu cảm xúc,biếttrântrọngngườikhác.'}, - [ContentTone.PROVOCATIVE]: {text: 'provocative-Gợimở suynghĩ,hơi gâytranhcãi,tháchthức cácgiảđịnh.'}, - [ContentTone.AUTHORITATIVE]: {text: 'authoritative-giọng tự tin,uyquyền,chuyênnghiệp'}, - // [ContentStyle.OPINION]: {text: 'Quan điểm táo bạo. \n"Tôi nghĩ / Quan điểm gây tranh cãi:". Mời gọi tranh luận.'}, - // [ContentStyle.STORYTELLING]: {text: 'STORYTELLING-Cấu trúc tự sự. Mở đầu bằng sự căng thẳng → phát triển → kết thúc.\n Có thể là câu chuyện cá nhân hoặc nghiên cứu trường hợp.'}, - // [ContentStyle.THREAD]: {text: 'THREAD-Bắt đầu bằng một tweet thu hút sự chú ý. \nMỗi điểm được đánh số. \nKết thúc bằng lời kêu gọi hành động (CTA) hoặc tóm tắt.'}, -} export const TONE_HINTS: Record = { [ContentTone.PROFESSIONAL]: 'professional, clear, credible', [ContentTone.CASUAL]: 'casual, friendly, conversational', @@ -105,10 +91,27 @@ export const TONE_HINTS: Record = { [ContentTone.INFLAMMATORY]: 'polarizing, controversial takes, generates strong reactions, defensible but provocative', [ContentTone.SAVAGE]: 'brutal roast mode, surgical wit, mocking, punch up only (public figures/ideas)', }; +const TONE_HINTS_TELEGRAM_BUTTON_DEFAULT = { + [ContentTone.PROFESSIONAL]: {text: 'chuyên nghiệp, rõ ràng, đáng tin cậy'}, + [ContentTone.CASUAL]: {text: 'Giản dị,thân thiện'}, + [ContentTone.HYPE]: {text: 'Hype-Hào hứng,tràn đầy năng lượng'}, + [ContentTone.URGENT]: {text: 'urgent'}, + [ContentTone.HUMOROUS]: {text: 'Dí dỏm, hài hước'}, + [ContentTone.INFORMATIVE]: {text: 'Thông tin, chính xác'}, + [ContentTone.EMPATHETIC]: {text: 'empathetic-Đồng cảm,thấu hiểu cảm xúc,biếttrântrọngngườikhác.'}, + [ContentTone.PROVOCATIVE]: {text: 'provocative-Gợimở suynghĩ,hơi gâytranhcãi,tháchthức cácgiảđịnh.'}, + [ContentTone.AUTHORITATIVE]: {text: 'authoritative-giọng tự tin,uyquyền,chuyênnghiệp'}, + // [ContentStyle.OPINION]: {text: 'Quan điểm táo bạo. \n"Tôi nghĩ / Quan điểm gây tranh cãi:". Mời gọi tranh luận.'}, + // [ContentStyle.STORYTELLING]: {text: 'STORYTELLING-Cấu trúc tự sự. Mở đầu bằng sự căng thẳng → phát triển → kết thúc.\n Có thể là câu chuyện cá nhân hoặc nghiên cứu trường hợp.'}, + // [ContentStyle.THREAD]: {text: 'THREAD-Bắt đầu bằng một tweet thu hút sự chú ý. \nMỗi điểm được đánh số. \nKết thúc bằng lời kêu gọi hành động (CTA) hoặc tóm tắt.'}, +} export const get_TONE_HINTS_TELEGRAM_BUTTON = (lang: Language) => { if (lang !== 'ja') { - return TONE_HINTS_TELEGRAM_BUTTON_DEFAULT; + return { + ...TONE_HINTS_TELEGRAM_BUTTON_DEFAULT, + [ContentTone.SPICY]: {text: 'spicy-Tự tin,hơiđốiđầu.KHÔNG giận dữ—chỉ thẳng.'}, // Blunt, sharp, mild profanity OK + }; } return { ...TONE_HINTS_TELEGRAM_BUTTON_DEFAULT, diff --git a/src/modules/content-writer/services/content-safety.service.ts b/src/modules/content-writer/services/content-safety.service.ts index 36b9440..fe0afda 100644 --- a/src/modules/content-writer/services/content-safety.service.ts +++ b/src/modules/content-writer/services/content-safety.service.ts @@ -69,6 +69,8 @@ export class ContentSafetyService { // Hard block check for (const pattern of this.HARD_BLOCK_PATTERNS) { if (pattern.test(content)) { + this.logger.error(`checkOutput ->FALSE -> Output contains prohibited language`); + this.logger.error({content}); return { safe: false, blockReason: 'Output contains prohibited language', diff --git a/src/modules/content-writer/services/provider-router.service.ts b/src/modules/content-writer/services/provider-router.service.ts index 3a0dc89..f6069ad 100644 --- a/src/modules/content-writer/services/provider-router.service.ts +++ b/src/modules/content-writer/services/provider-router.service.ts @@ -4,6 +4,7 @@ import {ContentStyle} from '../enum/style.enum'; import {ProviderName} from '../providers/ai-provider.factory'; import {Language} from "../../../common/interfaces/language.prompt.interface"; import {ContentTone, isEdgyTone} from "../enum/tone.enum"; +import {AngleEnum} from "../enum/angle.enum"; interface ProviderPair { writer: ProviderName; @@ -54,6 +55,15 @@ export class ProviderRouterService { }): RoutingDecision { const { language, contentType, style, tone } = params; + if (tone === ContentTone.EMPATHETIC) { + return { + writer: 'openai', // warmest voice, less "AI-ish" + reviewer: 'openai', + useXEnrichment: false, + reason: 'Empathy context: GPT for warmer human-like tone', + }; + } + // 🔥 EDGY TONES: route mạnh sang Grok (EN) hoặc DeepSeek (others) // GPT thường refuse hoặc water down → tránh if (tone && isEdgyTone(tone)) { diff --git a/src/modules/content-writer/services/quote-writer.service.ts b/src/modules/content-writer/services/quote-writer.service.ts index e981f25..9b65a87 100644 --- a/src/modules/content-writer/services/quote-writer.service.ts +++ b/src/modules/content-writer/services/quote-writer.service.ts @@ -1,5 +1,5 @@ // services/quote-writer.service.ts -import {Injectable, Logger} from '@nestjs/common'; +import {Injectable, Logger, UnprocessableEntityException} from '@nestjs/common'; import {ConfigService} from '@nestjs/config'; import {AIProviderFactory} from '../providers/ai-provider.factory'; import {ProviderRouterService} from './provider-router.service'; @@ -28,8 +28,16 @@ export class QuoteWriterService { private safety: ContentSafetyService ) {} - async generateQuote(dto: GenerateQuoteDto) { - this.logger.debug(`==> QuoteWriterService_generateQuote`); + async generateQuote(dto: GenerateQuoteDto, _retryCount = 0) { + const MAX_RETRIES = 2; + + if (_retryCount >= MAX_RETRIES) { + throw new UnprocessableEntityException( + `Cannot generate safe content after ${MAX_RETRIES + 1} attempts`, + ); + } + + this.logger.debug(`==> QuoteWriterService_generateQuote lần:${_retryCount}`); // 1. Auto-detect quote type nếu không có const quoteType = dto.quoteType ?? suggestQuoteType(dto.originalPost, dto.yourAngle); this.logger.log(`Quote type: ${quoteType}`); @@ -132,7 +140,9 @@ export class QuoteWriterService { return this.generateQuote({ ...dto, tone: this.downgradeTone(dto.tone!), - }); + }, + _retryCount + 1, + ); } return { diff --git a/src/modules/telegram/wizard/comment2.wizard.ts b/src/modules/telegram/wizard/comment2.wizard.ts index 3e517c7..4537adb 100644 --- a/src/modules/telegram/wizard/comment2.wizard.ts +++ b/src/modules/telegram/wizard/comment2.wizard.ts @@ -2,8 +2,8 @@ import {Action, Command, Ctx, On, Wizard, WizardStep} from "nestjs-telegraf"; import {WIZARD_COMMENT2_SCENE_ID} from "../telegram.constants"; import * as scenes from "telegraf/scenes"; import {ManagerService} from "../../manager/manager.service"; -import {ANGLE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/enum/angle.enum"; import {get_TONE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/prompts/templates"; +import {get_ANGLE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/enum/angle.enum"; @Wizard(WIZARD_COMMENT2_SCENE_ID) export class Comment2Wizard { @@ -105,6 +105,7 @@ export class Comment2Wizard { (ctx.wizard.state as any).tone = tone; // @ts-ignore const inline_keyboards = []; + const ANGLE_HINTS_TELEGRAM_BUTTON = get_ANGLE_HINTS_TELEGRAM_BUTTON(tone!); Object.keys(ANGLE_HINTS_TELEGRAM_BUTTON).map(key => { // @ts-ignore inline_keyboards.push([{