U
This commit is contained in:
+33
-30
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>([
|
||||
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<AngleEnum, string> = {
|
||||
@@ -21,21 +42,44 @@ export const ANGLE_HINTS: Record<AngleEnum, string> = {
|
||||
[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<AngleEnum, Object> = {
|
||||
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<Record<AngleEnum, Object>> = {
|
||||
[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<Record<AngleEnum, Object>> = {
|
||||
[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'},
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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<Record<Language, string>>;
|
||||
instruction: Partial<Record<Language, string>>;
|
||||
examples: Partial<Record<Language, string[]>>;
|
||||
avoid: Partial<Record<Language, string>>;
|
||||
contextNote: Partial<Record<Language, string>>;
|
||||
}
|
||||
|
||||
export const EMPATHY_ANGLE_SPECS: Partial<Record<AngleEnum, EmpathyAngleSpec>> = {
|
||||
[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}`
|
||||
: ``;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// prompts/empathy-system.ts
|
||||
|
||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||
|
||||
|
||||
export const buildEmpathySystemProgram = (lang: Language) => {
|
||||
const prompts: Record<Language, string> = {
|
||||
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];
|
||||
}
|
||||
@@ -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,
|
||||
``,
|
||||
|
||||
@@ -72,21 +72,7 @@ export const STYLE_HINTS: Record<ContentStyle, string> = {
|
||||
[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, string> = {
|
||||
[ContentTone.PROFESSIONAL]: 'professional, clear, credible',
|
||||
[ContentTone.CASUAL]: 'casual, friendly, conversational',
|
||||
@@ -105,10 +91,27 @@ export const TONE_HINTS: Record<ContentTone, string> = {
|
||||
[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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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([{
|
||||
|
||||
Reference in New Issue
Block a user