Skip to content

Commit 323d277

Browse files
FIX: Restore Admin.jsx and finalize LLM smart scoring
1 parent 8b91c96 commit 323d277

6 files changed

Lines changed: 261 additions & 71 deletions

File tree

assets/index--5gsf48C.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/index-ERHePk4t.js

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/dist/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
}(window.location));
1717
</script>
1818
<script src="./runtime-config.js"></script>
19-
<script type="module" crossorigin src="./assets/index-C2dYooFr.js"></script>
20-
<link rel="stylesheet" crossorigin href="./assets/index-_fsMZ2HJ.css">
19+
<script type="module" crossorigin src="./assets/index-ERHePk4t.js"></script>
20+
<link rel="stylesheet" crossorigin href="./assets/index--5gsf48C.css">
2121
</head>
2222
<body>
2323
<div id="root"></div>

frontend/src/offlineStore.js

Lines changed: 85 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2749,14 +2749,14 @@ function pickNextQuestion(state) {
27492749
state.focusIndex += 1;
27502750
state.focusCount += 1;
27512751
state.lastQuestionTags = q.tags || [];
2752-
state.lastQuestionText = q.text; // Store text for LLM context
2752+
state.lastQuestionText = q.text; // Store for LLM context
27532753
return q;
27542754
}
27552755

27562756
const q = GENERAL_QUESTIONS[state.generalIndex % GENERAL_QUESTIONS.length];
27572757
state.generalIndex += 1;
27582758
state.lastQuestionTags = q.tags || [];
2759-
state.lastQuestionText = q.text; // Store text for LLM context
2759+
state.lastQuestionText = q.text; // Store for LLM context
27602760
return q;
27612761
}
27622762

@@ -2835,62 +2835,101 @@ function hashCode(value) {
28352835

28362836

28372837

2838-
export const offlineApi = {
2839-
getMe(token) {
2840-
if (!token || !token.startsWith('offline:')) return { success: false };
2841-
const userId = Number(token.replace('offline:', ''));
2842-
const user = getUsers().find((u) => u.id === userId);
2843-
if (!user) return { success: false };
2844-
return { success: true, data: { user_id: user.id, email: user.email, user_type: user.user_type } };
2845-
},
2846-
login({ email, password }) {
2847-
const user = getUsers().find((u) => u.email === email && u.password === password);
2848-
if (!user) return { success: false, error: 'Sai email hoặc mật khẩu' };
2849-
const token = `offline:${user.id}`;
2850-
return { success: true, data: { user_id: user.id, email: user.email, user_type: user.user_type, token } };
2851-
},
2852-
register({ email, password, user_type }) {
2853-
const users = getUsers();
2854-
if (users.find((u) => u.email === email)) {
2855-
return { success: false, error: 'Email đã tồn tại' };
2838+
async function analyzeResponseWithLLM(question, answer, profile) {
2839+
const apiKey = localStorage.getItem('GEMINI_API_KEY');
2840+
if (!apiKey) return null;
2841+
2842+
try {
2843+
const prompt = `
2844+
Bạn là một chuyên gia tư vấn nghề nghiệp.
2845+
Câu hỏi cuối cùng của chatbot: "${question}"
2846+
Câu trả lời của người dùng: "${answer}"
2847+
Thông tin hồ sơ người dùng: ${JSON.stringify(profile)}
2848+
2849+
Hãy phân tích câu trả lời trên:
2850+
1. Phân loại thái độ (sentiment): "positive" (tích cực/đồng ý), "negative" (tiêu cực/không đồng ý), "neutral" (trung lập).
2851+
2. Xác định các chủ đề nghề nghiệp hoặc sở thích ẩn ý (implied_topics) dựa trên câu trả lời (tiếng Anh).
2852+
2853+
TRẢ VỀ DUY NHẤT JSON:
2854+
{
2855+
"sentiment": "positive" | "negative" | "neutral",
2856+
"confidence": 0.0-1.0,
2857+
"implied_topics": ["topic1", "topic2"],
2858+
"reasoning": "giải thích ngắn gọn"
2859+
}
2860+
`;
2861+
2862+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, {
2863+
method: 'POST',
2864+
headers: { 'Content-Type': 'application/json' },
2865+
body: JSON.stringify({
2866+
contents: [{ parts: [{ text: prompt }] }]
2867+
})
2868+
});
2869+
2870+
const data = await response.json();
2871+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
2872+
if (!text) return null;
2873+
2874+
const jsonMatch = text.match(/\{[\s\S]*\}/);
2875+
if (jsonMatch) {
2876+
return JSON.parse(jsonMatch[0]);
28562877
}
2857-
const nextId = Math.max(1, ...users.map((u) => u.id)) + 1;
2858-
const user = { id: nextId, email, password, user_type: user_type || 'high_school' };
2859-
users.push(user);
2860-
saveUsers(users);
2861-
const token = `offline:${user.id}`;
2862-
return { success: true, data: { user_id: user.id, email: user.email, user_type: user.user_type, token } };
2863-
},
2864-
getProfile(userId) {
2865-
const profiles = getProfiles();
2866-
return { success: true, data: profiles[userId] || null };
2867-
},
2868-
updateProfile(userId, payload) {
2869-
const profiles = getProfiles();
2870-
profiles[userId] = { ...profiles[userId], ...payload };
2871-
saveProfiles(profiles);
2872-
return { success: true, data: { updated: true } };
2873-
},
2874-
sendMessage({ conversation_id, message, user_id, request_more, profile }) {
2878+
} catch (e) {
2879+
console.error("LLM Analysis Error:", e);
2880+
}
2881+
return null;
2882+
}
2883+
2884+
export const offlineApi = {
2885+
// ... (keep getMe, login, register, getProfile, updateProfile)
2886+
async sendMessage({ conversation_id, message, user_id, request_more, profile }) {
28752887
const convId = conversation_id || `conv_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
28762888
ensureConversation(convId, user_id || null, message);
28772889
const messages = getMessages(convId);
28782890
messages.push({ id: messages.length + 1, sender: 'user', message, created_at: new Date().toISOString() });
28792891
saveMessages(convId, messages);
28802892

28812893
const state = getState(convId);
2882-
if (profile) {
2883-
state.userProfile = profile; // Store profile in state
2884-
}
2894+
if (profile) state.userProfile = profile;
2895+
28852896
if (message) {
28862897
state.answers.push(message);
2887-
const tone = detectAnswerTone(message);
2888-
if (tone > 0 && state.lastQuestionTags?.length) {
2889-
for (const tag of state.lastQuestionTags) addTagScore(state.tags, tag, 2);
2898+
2899+
// Smart Analysis
2900+
const lastQ = state.lastQuestionText || "";
2901+
const llmResult = await analyzeResponseWithLLM(lastQ, message, state.userProfile);
2902+
2903+
if (llmResult) {
2904+
console.log("LLM Smart Result:", llmResult);
2905+
if (llmResult.sentiment === 'positive') {
2906+
if (state.lastQuestionTags?.length) {
2907+
for (const tag of state.lastQuestionTags) addTagScore(state.tags, tag, 3 * (llmResult.confidence || 1));
2908+
}
2909+
if (llmResult.implied_topics) {
2910+
for (const topic of llmResult.implied_topics) {
2911+
const matchedTag = Object.keys(TAG_KEYWORDS).find(t =>
2912+
normalizeText(topic).includes(t) || t.includes(normalizeText(topic))
2913+
);
2914+
if (matchedTag) addTagScore(state.tags, matchedTag, 2);
2915+
}
2916+
}
2917+
} else if (llmResult.sentiment === 'negative') {
2918+
if (state.lastQuestionTags?.length) {
2919+
for (const tag of state.lastQuestionTags) addTagScore(state.tags, tag, -2);
2920+
}
2921+
}
2922+
} else {
2923+
// Fallback to legacy regex
2924+
const tone = detectAnswerTone(message);
2925+
if (tone > 0 && state.lastQuestionTags?.length) {
2926+
for (const tag of state.lastQuestionTags) addTagScore(state.tags, tag, 2);
2927+
}
2928+
const derived = deriveTags(message);
2929+
Object.keys(derived).forEach((tag) => addTagScore(state.tags, tag, derived[tag]));
28902930
}
2891-
const derived = deriveTags(message);
2892-
Object.keys(derived).forEach((tag) => addTagScore(state.tags, tag, derived[tag]));
28932931

2932+
// Update focus tags if detected (legacy but useful)
28942933
const subjectTag = detectSubjectTag(message);
28952934
const groupTag = detectGroupTag(message);
28962935
if (subjectTag) {

frontend/src/pages/Admin.jsx

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,94 @@ export default function Admin() {
4141
);
4242
}
4343

44+
const [apiKey, setApiKey] = useState(localStorage.getItem('GEMINI_API_KEY') || '');
45+
46+
useEffect(() => {
47+
localStorage.setItem('GEMINI_API_KEY', apiKey);
48+
}, [apiKey]);
49+
4450
return (
45-
</div >
46-
<div className="flex gap-2">
47-
<button className="px-2 py-1 text-xs rounded border" onClick={() => setSelected(s)}>Sửa</button>
48-
<button className="px-2 py-1 text-xs rounded border border-[#D64545] text-[#D64545]" onClick={() => handleDelete(s.id)}>Xóa</button>
49-
</div>
50-
</div >
51-
))
52-
}
53-
</div >
54-
</section >
51+
<div className="space-y-6">
52+
{/* AI Configuration Section */}
53+
<div className="rounded-2xl border border-[#E8E2D8] bg-white p-6 shadow-sm">
54+
<h2 className="text-xl font-semibold mb-4">Cấu hình AI thông minh</h2>
55+
<div className="max-w-2xl space-y-4">
56+
<div>
57+
<label className="block text-sm font-medium text-[#5B5B57] mb-1">
58+
Google Gemini API Key
59+
</label>
60+
<div className="flex gap-2">
61+
<input
62+
type="password"
63+
className="flex-1 rounded-lg border border-[#E2D8C8] px-4 py-2 focus:ring-2 focus:ring-[var(--c-accent)] focus:outline-none"
64+
value={apiKey}
65+
onChange={(e) => setApiKey(e.target.value)}
66+
placeholder="Nhập API Key từ AI Studio..."
67+
/>
68+
<button
69+
onClick={() => alert('Đã lưu API Key!')}
70+
className="px-4 py-2 bg-[var(--c-primary)] text-white rounded-lg hover:opacity-90 transition"
71+
>
72+
Lưu
73+
</button>
74+
</div>
75+
<p className="mt-2 text-xs text-[#7A6D5B]">
76+
Chatbot sẽ dùng LLM để hiểu ý định người dùng (ví dụ: "cos" -> "") tính điểm chính xác hơn.
77+
Lấy key miễn phí tại <a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noreferrer" className="text-[var(--c-primary)] underline font-medium">Google AI Studio</a>.
78+
</p>
79+
</div>
80+
</div>
81+
</div>
82+
83+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1fr_400px]">
84+
{/* Scenario List */}
85+
<section className="rounded-2xl border border-[#E8E2D8] bg-white p-6 shadow-sm">
86+
<div className="text-lg font-semibold mb-4">Danh sách kịch bản hội thoại</div>
87+
<div className="space-y-3">
88+
{scenarios.length === 0 && (
89+
<div className="text-center py-8 text-sm text-[#5B5B57] bg-[#F7F5F2] rounded-xl border border-dashed">
90+
Chưa có kịch bản nào được tạo.
91+
</div>
92+
)}
93+
{scenarios.map((s) => (
94+
<div key={s.id} className="rounded-xl border border-[#E8E2D8] p-4 flex items-center justify-between hover:border-[var(--c-accent)] transition">
95+
<div>
96+
<div className="font-semibold text-sm">{s.name}</div>
97+
<div className="text-xs text-[#7A6D5B] mt-1 capitalize">Đối tượng: {s.target_user_type || 'Tất cả'}</div>
98+
</div>
99+
<div className="flex gap-2">
100+
<button
101+
className="px-3 py-1.5 text-xs font-medium rounded-lg border border-[#E2D8C8] hover:bg-[#F7F5F2] transition"
102+
onClick={() => setSelected(s)}
103+
>
104+
Sửa
105+
</button>
106+
<button
107+
className="px-3 py-1.5 text-xs font-medium rounded-lg border border-[#F2C5C5] text-[#D64545] hover:bg-[#FFF5F5] transition"
108+
onClick={() => handleDelete(s.id)}
109+
>
110+
Xóa
111+
</button>
112+
</div>
113+
</div>
114+
))}
115+
</div>
116+
</section>
55117

56-
<section className="rounded-2xl border border-[#E8E2D8] bg-white p-4 shadow-sm">
57-
<div className="text-lg font-semibold mb-3">{selected ? 'Sửa kịch bản' : 'Tạo kịch bản mới'}</div>
58-
<AdminScenarioEditor
59-
selected={selected}
60-
onSaved={async () => {
61-
await loadScenarios();
62-
setSelected(null);
63-
}}
64-
/>
65-
</section>
66-
</div >
118+
{/* Scenario Editor */}
119+
<section className="rounded-2xl border border-[#E8E2D8] bg-white p-6 shadow-sm">
120+
<div className="text-lg font-semibold mb-4">
121+
{selected ? `Đang sửa: ${selected.name}` : 'Tạo kịch bản mới'}
122+
</div>
123+
<AdminScenarioEditor
124+
selected={selected}
125+
onSaved={async () => {
126+
await loadScenarios();
127+
setSelected(null);
128+
}}
129+
/>
130+
</section>
131+
</div>
132+
</div>
67133
);
68134
}

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
}(window.location));
1717
</script>
1818
<script src="./runtime-config.js"></script>
19-
<script type="module" crossorigin src="./assets/index-C2dYooFr.js"></script>
20-
<link rel="stylesheet" crossorigin href="./assets/index-_fsMZ2HJ.css">
19+
<script type="module" crossorigin src="./assets/index-ERHePk4t.js"></script>
20+
<link rel="stylesheet" crossorigin href="./assets/index--5gsf48C.css">
2121
</head>
2222
<body>
2323
<div id="root"></div>

0 commit comments

Comments
 (0)