@@ -38,102 +38,104 @@ const MIN_TAG_FILTER_ANSWERS = 4;
3838
3939function 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 ) {
0 commit comments