Skip to content

Commit daa2220

Browse files
update db 2
1 parent d8b4221 commit daa2220

4 files changed

Lines changed: 149 additions & 128 deletions

File tree

backend/data/careerLibrary.js

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ const CAREER_LIBRARY = {
3333
'Cloud Engineer', 'Network Engineer', 'Cybersecurity Analyst', 'AI Engineer',
3434
'Embedded Engineer', 'Systems Engineer', 'Site Reliability Engineer', 'IT Support',
3535
'IT Administrator', 'Database Administrator', 'Security Engineer', 'Automation Engineer',
36-
'AI Researcher', 'MLOps Engineer', 'AR/VR Developer', 'Blockchain Developer',
37-
'Solutions Architect', 'Platform Engineer', 'DevSecOps Engineer', 'Product Security Engineer',
38-
'Systems Administrator', 'Technical Support Engineer'
36+
'Technical Support Engineer', 'Web Developer', 'System Administrator', 'IT Consultant',
37+
'IT Project Manager', 'Data Center Engineer', 'IT Trainer', 'IT Auditor', 'IT Sales Specialist',
38+
'IT Procurement Specialist', 'IT Business Analyst', 'IT Operations Specialist', 'IT Service Manager'
3939
],
4040
Data: [
4141
'Data Analyst', 'Data Engineer', 'Data Scientist', 'Business Intelligence Analyst',
@@ -48,15 +48,21 @@ const CAREER_LIBRARY = {
4848
'UI/UX Designer', 'Graphic Designer', 'Product Designer', 'UX Researcher',
4949
'Interaction Designer', 'Motion Designer', '3D Designer', 'Brand Designer',
5050
'Illustrator', 'Game Artist', 'Service Designer', 'Design Systems Designer',
51-
'Visual Designer', 'UX Writer', 'Creative Technologist', 'Art Director',
52-
'Industrial Designer', 'Packaging Designer'
51+
'Visual Designer', 'UX Writer', 'Art Director', 'Industrial Designer', 'Packaging Designer',
52+
'Fashion Designer', 'Interior Designer', 'Web Designer', 'Advertising Designer',
53+
'Animation Designer', 'Event Designer', 'Video Designer', 'Sound Designer',
54+
'Book Designer', 'Magazine Designer', 'Film Designer', 'Game Designer'
5355
],
5456
Business: [
5557
'Business Analyst', 'Product Manager', 'Project Manager', 'Operations Manager',
5658
'HR Specialist', 'Recruiter', 'Sales Representative', 'Account Manager',
5759
'Customer Success', 'Business Development', 'Office Manager', 'Procurement Specialist',
5860
'Strategy Analyst', 'Operations Analyst', 'Management Consultant',
59-
'Customer Experience Manager', 'Supply Chain Manager'
61+
'Customer Experience Manager', 'Supply Chain Manager',
62+
'CEO', 'CFO', 'CMO', 'CHRO', 'Sales Manager', 'Project Manager', 'HR Manager', 'Marketing Manager',
63+
'Market Development Specialist', 'Business Data Analyst', 'Supply Chain Specialist', 'Risk Manager',
64+
'Quality Manager', 'Customer Manager', 'Product Manager', 'Finance Manager', 'Operations Manager',
65+
'Strategy Manager', 'Brand Manager', 'Sales Specialist', 'Contract Manager', 'Purchasing Manager', 'Office Manager'
6066
],
6167
Marketing: [
6268
'Marketing Specialist', 'SEO Specialist', 'Social Media Manager', 'Content Creator',
@@ -72,10 +78,7 @@ const CAREER_LIBRARY = {
7278
'Investment Banker', 'Equity Research Analyst', 'Actuary', 'Risk Manager'
7379
],
7480
Education: [
75-
'Teacher', 'English Teacher', 'Math Teacher', 'Academic Advisor',
76-
'Education Counselor', 'Trainer', 'Instructional Designer', 'School Administrator',
77-
'STEM Teacher', 'Career Coach', 'E-learning Specialist', 'Academic Researcher',
78-
'School Psychologist', 'Education Content Developer'
81+
'Giáo viên Toán', 'Giáo viên Văn', 'Giáo viên Tiếng Anh', 'Giáo viên Lịch sử', 'Giáo viên Địa lý', 'Giáo viên Sinh học', 'Giáo viên Hóa học', 'Giáo viên Vật lý', 'Giáo viên Tin học', 'Giáo viên Giáo dục công dân', 'Giáo viên Mầm non', 'Giáo viên Tiểu học', 'Giáo viên Trung học', 'Giáo viên Nghệ thuật', 'Giáo viên Âm nhạc', 'Giáo viên Thể dục', 'Giáo viên Kỹ năng sống', 'Giảng viên Đại học', 'Giảng viên Cao đẳng', 'Giảng viên Trung cấp', 'Giáo viên Đào tạo nghề', 'Giáo viên Giáo dục đặc biệt', 'Giáo viên Giáo dục quốc tế', 'Giáo viên Giáo dục hướng nghiệp'
7982
],
8083
Healthcare: [
8184
'Nurse', 'Pharmacist', 'Lab Technician', 'Medical Assistant',
@@ -105,7 +108,10 @@ const CAREER_LIBRARY = {
105108
Media: [
106109
'Journalist', 'Video Editor', 'Photographer', 'Media Producer',
107110
'Content Strategist', 'Podcast Producer', 'Animator', 'Scriptwriter',
108-
'Social Media Producer', 'Sound Designer', 'Media Planner'
111+
'Social Media Producer', 'Sound Designer', 'Media Planner',
112+
'Editor', 'Reporter', 'TV Host', 'Director', 'Cameraman', 'PR Specialist', 'Advertising Specialist',
113+
'Program Producer', 'Video Producer', 'Audio Producer', 'Image Producer', 'Press Producer', 'Film Producer',
114+
'Game Producer', 'Animation Producer', 'Interactive Producer', 'Brand Producer'
109115
],
110116
Government: [
111117
'Policy Analyst', 'Public Relations Officer', 'Civil Servant',

backend/routes/chat.js

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,15 @@ router.post('/message', optionalAuth, async (req, res) => {
140140
});
141141
}
142142

143-
const nextQuestion = getNextQuestion(state, { force: wantsMore });
144-
if (!nextQuestion) {
145-
const profile = await getProfile(userId);
146-
const mergedProfile = buildProfileFromState(profile, state, effectiveUserType);
147-
const answersText = buildAnswersText(state, message);
148-
let recommendations = await matchCareerAsync(mergedProfile, {
143+
let nextQuestion = getNextQuestion(state, { force: wantsMore });
144+
let finished = false;
145+
let recommendations = [];
146+
let answersText = buildAnswersText(state, message);
147+
const profile = await getProfile(userId);
148+
const mergedProfile = buildProfileFromState(profile, state, effectiveUserType);
149+
// Lặp hỏi thêm cho đến khi xác suất nghề nghiệp đủ rõ ràng (ví dụ: nghề top 1 > 40% và chênh lệch với top 2 > 15%)
150+
while (!nextQuestion && !finished) {
151+
recommendations = await matchCareerAsync(mergedProfile, {
149152
message,
150153
answers: state.answers,
151154
answersText,
@@ -156,6 +159,30 @@ router.post('/message', optionalAuth, async (req, res) => {
156159
const llmScores = await scoreCareersWithLLM({ profile: mergedProfile, answersText, candidates });
157160
recommendations = mergeScores(recommendations, llmScores);
158161
}
162+
const sorted = recommendations.sort((a, b) => b.probability - a.probability);
163+
if (sorted.length > 1 && sorted[0].probability > 0.4 && (sorted[0].probability - sorted[1].probability) > 0.15) {
164+
finished = true;
165+
break;
166+
}
167+
// Nếu chưa đủ rõ ràng, hỏi thêm câu hỏi
168+
nextQuestion = getNextQuestion(state, { force: true });
169+
if (nextQuestion) {
170+
if (userId) {
171+
await saveMessage(convId, userId, 'bot', nextQuestion.text, nextQuestion.id);
172+
}
173+
return res.json({
174+
success: true,
175+
data: {
176+
bot_reply: nextQuestion.text,
177+
options: nextQuestion.options,
178+
next_node: nextQuestion.id,
179+
conversation_id: convId
180+
}
181+
});
182+
}
183+
}
184+
// Nếu đã đủ xác suất rõ ràng hoặc hết câu hỏi
185+
if (finished || !nextQuestion) {
159186
if (userId) {
160187
await saveMessage(convId, userId, 'bot', 'Tôi đã gợi ý nghề nghiệp phù hợp cho bạn.', null);
161188
await saveRecommendations(convId, userId, recommendations);
@@ -172,20 +199,6 @@ router.post('/message', optionalAuth, async (req, res) => {
172199
}
173200
});
174201
}
175-
176-
if (userId) {
177-
await saveMessage(convId, userId, 'bot', nextQuestion.text, nextQuestion.id);
178-
}
179-
180-
res.json({
181-
success: true,
182-
data: {
183-
bot_reply: nextQuestion.text,
184-
options: nextQuestion.options,
185-
next_node: nextQuestion.id,
186-
conversation_id: convId
187-
}
188-
});
189202
} catch (error) {
190203
console.error('Chat error:', error);
191204
res.status(500).json({ success: false, error: error.message });

backend/services/questionEngine.js

Lines changed: 98 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -38,102 +38,104 @@ const MIN_TAG_FILTER_ANSWERS = 4;
3838

3939
function getConversationState(conversationId, userType) {
4040
if (!CONVERSATIONS.has(conversationId)) {
41-
CONVERSATIONS.set(conversationId, {
42-
askedIds: new Set(),
43-
coverage: {},
44-
answers: [],
45-
lastQuestion: null,
46-
userType: userType || null,
47-
tags: {},
48-
collectedSkills: new Set(),
49-
collectedInterests: new Set(),
50-
seed: hashCode(conversationId)
51-
});
52-
}
53-
const state = CONVERSATIONS.get(conversationId);
54-
if (userType) state.userType = userType;
55-
return state;
56-
}
57-
58-
function recordAnswer(state, message) {
59-
const raw = String(message || '');
60-
const text = raw.toLowerCase();
61-
const normalized = normalizeText(raw);
62-
if (state.lastQuestion) {
63-
const category = state.lastQuestion.category;
64-
state.coverage[category] = (state.coverage[category] || 0) + 1;
65-
state.answers.push({ q: state.lastQuestion.id, a: message, category });
66-
if (state.lastQuestion.item && !isNegativeAnswer(normalized)) {
67-
if (category === 'skills') state.collectedSkills.add(state.lastQuestion.item);
68-
if (category === 'interests') state.collectedInterests.add(state.lastQuestion.item);
69-
}
70-
}
71-
// basic keyword tagging for branching
72-
if (text.includes('công nghệ') || normalized.includes('cong nghe') || text.includes('tech') || text.includes('it')) state.tags.tech = true;
73-
if (text.includes('kinh doanh') || normalized.includes('kinh doanh') || text.includes('business') || text.includes('marketing')) state.tags.business = true;
74-
if (text.includes('thiết kế') || normalized.includes('thiet ke') || text.includes('design') || text.includes('ui') || text.includes('ux')) state.tags.design = true;
75-
if (text.includes('y tế') || normalized.includes('y te') || text.includes('dược') || normalized.includes('duoc') || text.includes('điều dưỡng') || normalized.includes('dieu duong')) state.tags.health = true;
76-
if (text.includes('giáo dục') || normalized.includes('giao duc') || text.includes('dạy') || normalized.includes('day') || text.includes('giảng')) state.tags.education = true;
77-
}
78-
79-
function normalizeText(value) {
80-
return String(value || '')
81-
.toLowerCase()
82-
.normalize('NFD')
83-
.replace(/[\u0300-\u036f]/g, '');
84-
}
85-
86-
function isNegativeAnswer(normalizedText) {
87-
const negatives = ['khong', 'không', 'chua', 'chưa', 'it', 'ít', 'kho', 'khó', 'khong thich', 'không thích'];
88-
return negatives.some((t) => normalizedText.includes(t));
89-
}
90-
91-
function getCategoryOrder(userType) {
92-
if (userType && CATEGORY_PRIORITY_BY_USER_TYPE[userType]) {
93-
return CATEGORY_PRIORITY_BY_USER_TYPE[userType];
94-
}
95-
return CATEGORY_PRIORITY;
96-
}
97-
98-
function getCategoryLimit(userType, category) {
99-
const byType = CATEGORY_LIMITS_BY_USER_TYPE[userType] || {};
100-
if (byType && byType[category]) return byType[category];
101-
return MAX_PER_CATEGORY;
102-
}
103-
104-
function isRequiredMet(state) {
105-
const req = REQUIRED_BY_USER_TYPE[state.userType] || {};
106-
return Object.entries(req).every(([category, min]) => (state.coverage[category] || 0) >= min);
107-
}
108-
109-
function pickRequiredCategory(state, userType, options) {
110-
if (options.force) return null;
111-
const req = REQUIRED_BY_USER_TYPE[userType] || {};
112-
const unmet = Object.entries(req)
113-
.filter(([category, min]) => (state.coverage[category] || 0) < min)
114-
.map(([category]) => category);
115-
if (unmet.length === 0) return null;
116-
return unmet[state.answers.length % unmet.length];
117-
}
118-
119-
function isEnoughInfo(state) {
120-
if (!isRequiredMet(state)) return false;
121-
const covered = Object.keys(state.coverage).length;
122-
return covered >= 9 || state.answers.length >= 16;
123-
}
124-
125-
function getNextQuestion(state, options = {}) {
126-
const forceContinue = options.force === true;
127-
if (!forceContinue && isEnoughInfo(state)) return null;
128-
129-
const userType = state.userType;
130-
const categories = getCategoryOrder(userType);
131-
const coverageCounts = categories.map((category) => ({
132-
category,
133-
count: state.coverage[category] || 0
134-
})).filter(item => item.count < getCategoryLimit(userType, item.category));
135-
136-
if (coverageCounts.length === 0) return null;
41+
const QUESTION_BANK = [
42+
// ...existing code...
43+
// Các câu hỏi phân biệt từng nghề nghiệp phổ biến
44+
{
45+
id: 'q1',
46+
question: 'Bạn thích làm việc với con người hay máy móc?',
47+
options: ['Con người', 'Máy móc', 'Cả hai'],
48+
weights: {
49+
'Giáo viên Toán': 10, 'Giáo viên Văn': 10, 'Giáo viên Tiếng Anh': 10, 'Giáo viên Lịch sử': 10, 'Giáo viên Địa lý': 10,
50+
'Lập trình viên': 10, 'Chuyên viên phát triển phần mềm': 10, 'Chuyên viên quản trị mạng': 10, 'Chuyên viên bảo mật thông tin': 10,
51+
'UI/UX Designer': 10, 'Thiết kế đồ họa': 10, 'Thiết kế web': 10, 'Biên tập viên': 10, 'Phóng viên': 10, 'MC truyền hình': 10,
52+
'Giám đốc điều hành (CEO)': 10, 'Giám đốc tài chính (CFO)': 10, 'Giám đốc marketing (CMO)': 10, 'Trưởng phòng kinh doanh': 10
53+
}
54+
},
55+
{
56+
id: 'q2',
57+
question: 'Bạn thích giải quyết vấn đề logic hay sáng tạo?',
58+
options: ['Logic', 'Sáng tạo', 'Cả hai'],
59+
weights: {
60+
'Giáo viên Toán': 10, 'Lập trình viên': 10, 'Chuyên viên phân tích hệ thống': 10, 'Chuyên viên phát triển phần mềm': 10,
61+
'Thiết kế đồ họa': 10, 'Thiết kế web': 10, 'Thiết kế sáng tạo': 10, 'Biên tập viên': 10, 'Phóng viên': 10, 'MC truyền hình': 10
62+
}
63+
},
64+
{
65+
id: 'q3',
66+
question: 'Bạn thích làm việc độc lập hay theo nhóm?',
67+
options: ['Độc lập', 'Theo nhóm', 'Cả hai'],
68+
weights: {
69+
'Lập trình viên': 10, 'Chuyên viên phát triển phần mềm': 10, 'Chuyên viên quản trị mạng': 10, 'Chuyên viên bảo mật thông tin': 10,
70+
'Giáo viên Toán': 10, 'Giáo viên Văn': 10, 'Giáo viên Tiếng Anh': 10, 'Giáo viên Lịch sử': 10, 'Giáo viên Địa lý': 10,
71+
'UI/UX Designer': 10, 'Thiết kế đồ họa': 10, 'Thiết kế web': 10, 'Biên tập viên': 10, 'Phóng viên': 10, 'MC truyền hình': 10,
72+
'Giám đốc điều hành (CEO)': 10, 'Giám đốc tài chính (CFO)': 10, 'Giám đốc marketing (CMO)': 10, 'Trưởng phòng kinh doanh': 10
73+
}
74+
},
75+
{
76+
id: 'q4',
77+
question: 'Bạn có thích quản lý, lãnh đạo không?',
78+
options: ['Có', 'Không', 'Tùy tình huống'],
79+
weights: {
80+
'Giám đốc điều hành (CEO)': 15, 'Giám đốc tài chính (CFO)': 15, 'Giám đốc marketing (CMO)': 15, 'Trưởng phòng kinh doanh': 15,
81+
'Trưởng phòng dự án': 15, 'Trưởng phòng nhân sự': 15, 'Trưởng phòng marketing': 15
82+
}
83+
},
84+
{
85+
id: 'q5',
86+
question: 'Bạn có thích sáng tạo nội dung, truyền thông?',
87+
options: ['Có', 'Không', 'Tùy tình huống'],
88+
weights: {
89+
'Biên tập viên': 15, 'Phóng viên': 15, 'MC truyền hình': 15, 'Đạo diễn': 15, 'Quay phim': 15,
90+
'Chuyên viên truyền thông': 15, 'Chuyên viên PR': 15, 'Chuyên viên quảng cáo': 15, 'Chuyên viên sản xuất chương trình': 15
91+
}
92+
},
93+
{
94+
id: 'q6',
95+
question: 'Bạn có thích thiết kế, mỹ thuật, sáng tạo hình ảnh?',
96+
options: ['Có', 'Không', 'Tùy tình huống'],
97+
weights: {
98+
'UI/UX Designer': 15, 'Thiết kế đồ họa': 15, 'Thiết kế web': 15, 'Thiết kế sáng tạo': 15, 'Thiết kế thời trang': 15,
99+
'Thiết kế nội thất': 15, 'Thiết kế sản phẩm': 15, 'Thiết kế bao bì': 15, 'Thiết kế quảng cáo': 15
100+
}
101+
},
102+
{
103+
id: 'q7',
104+
question: 'Bạn có thích phân tích dữ liệu, số liệu, tài chính?',
105+
options: ['Có', 'Không', 'Tùy tình huống'],
106+
weights: {
107+
'Chuyên viên phân tích dữ liệu kinh doanh': 15, 'Chuyên viên quản lý tài chính': 15, 'Chuyên viên quản lý chất lượng': 15,
108+
'Giám đốc tài chính (CFO)': 15, 'Business Analyst': 15, 'Financial Analyst': 15, 'Accountant': 15
109+
}
110+
},
111+
{
112+
id: 'q8',
113+
question: 'Bạn có thích phát triển phần mềm, lập trình?',
114+
options: ['Có', 'Không', 'Tùy tình huống'],
115+
weights: {
116+
'Lập trình viên': 20, 'Chuyên viên phát triển phần mềm': 20, 'Software Engineer': 20, 'Backend Developer': 20, 'Frontend Developer': 20,
117+
'Full Stack Developer': 20, 'Mobile Developer': 20, 'Game Developer': 20, 'QA Engineer': 20, 'DevOps Engineer': 20
118+
}
119+
},
120+
{
121+
id: 'q9',
122+
question: 'Bạn có thích quản trị hệ thống, bảo mật thông tin?',
123+
options: ['Có', 'Không', 'Tùy tình huống'],
124+
weights: {
125+
'Chuyên viên quản trị hệ thống': 20, 'Chuyên viên bảo mật thông tin': 20, 'Security Engineer': 20, 'Network Engineer': 20,
126+
'Database Administrator': 20, 'Cloud Engineer': 20, 'DevSecOps Engineer': 20
127+
}
128+
},
129+
{
130+
id: 'q10',
131+
question: 'Bạn có thích phát triển game, ứng dụng di động, AI, IoT?',
132+
options: ['Có', 'Không', 'Tùy tình huống'],
133+
weights: {
134+
'Chuyên viên phát triển game': 20, 'Chuyên viên phát triển ứng dụng di động': 20, 'Chuyên viên phát triển AI': 20,
135+
'Chuyên viên phát triển IoT': 20, 'Game Developer': 20, 'Mobile Developer': 20, 'AI Engineer': 20
136+
}
137+
}
138+
];
137139

138140
const requiredPick = pickRequiredCategory(state, userType, options);
139141
if (requiredPick) {

frontend/src/components/JobCard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function JobCard({ job }) {
2323
onError={() => setImgSrc(`${baseUrl}career-icons/default.svg`)}
2424
/>
2525
<div className="min-w-0">
26-
<div className="text-sm font-semibold truncate">{job.title}</div>
26+
<div className="text-sm font-semibold break-words whitespace-normal">{job.title}</div>
2727
<div className="text-xs text-[#5B5B57]">{job.category || 'Khác'}</div>
2828
</div>
2929
</div>

0 commit comments

Comments
 (0)