v2: add AI teacher chat

This commit is contained in:
Ruslan 2026-09-09 10:51:39 +00:00
parent 39295e5924
commit 08c3d00aed
6 changed files with 435 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.env
node_modules/
.vibe42-run.log
.vibe42-run.pid
data.json

View File

@ -15,6 +15,12 @@
</header>
<main class="container">
<!-- Кнопка учителя -->
<button id="teacher-btn" class="teacher-fab">
<span class="teacher-icon">👨‍🏫</span>
<span class="teacher-text">Учитель</span>
</button>
<!-- Выбор уровня -->
<section id="levels-section" class="section">
<h2>Выберите ваш уровень</h2>
@ -79,6 +85,30 @@
<div id="practice-content" class="content-grid"></div>
</div>
</section>
<!-- Чат с учителем -->
<div id="teacher-chat" class="teacher-chat hidden">
<div class="chat-header">
<h3>👨‍🏫 Виртуальный учитель</h3>
<button id="close-chat" class="chat-close">✕</button>
</div>
<div class="chat-messages" id="chat-messages">
<div class="message teacher">
<div class="message-bubble">
Привет! Я твой учитель английского. Могу помочь с:
<br>• Разговорной практикой
<br>• Исправлением ошибок
<br>• Объяснением грамматики
<br>• Подбором слов
<br><br>Напиши мне на английском или русском!
</div>
</div>
</div>
<div class="chat-input-area">
<input type="text" id="chat-input" placeholder="Напиши сообщение..." class="chat-input">
<button id="send-btn" class="send-btn">➤</button>
</div>
</div>
</main>
<footer class="footer">

8
package.json Normal file
View File

@ -0,0 +1,8 @@
{
"name": "english-learning",
"private": true,
"type": "commonjs",
"scripts": {
"start": "node server.js"
}
}

127
script.js
View File

@ -274,6 +274,7 @@ const levelsData = {
// Текущее состояние
let currentLevel = null;
let chatSessionId = null;
// DOM элементы
const levelsSection = document.getElementById('levels-section');
@ -284,6 +285,14 @@ const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
const vocabSearch = document.getElementById('vocab-search');
// Учитель
const teacherBtn = document.getElementById('teacher-btn');
const teacherChat = document.getElementById('teacher-chat');
const closeChat = document.getElementById('close-chat');
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
// Инициализация
function init() {
// Клик по уровням
@ -313,6 +322,14 @@ function init() {
vocabSearch.addEventListener('input', (e) => {
filterVocabulary(e.target.value);
});
// Учитель
teacherBtn.addEventListener('click', toggleChat);
closeChat.addEventListener('click', toggleChat);
sendBtn.addEventListener('click', sendMessage);
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
}
// Показать уровень
@ -416,5 +433,115 @@ function handleExerciseClick(e) {
}
}
// Учитель - функции
function toggleChat() {
teacherChat.classList.toggle('hidden');
if (!teacherChat.classList.contains('hidden')) {
chatInput.focus();
}
}
async function sendMessage() {
const text = chatInput.value.trim();
if (!text) return;
// Добавляем сообщение студента
addMessage(text, 'student');
chatInput.value = '';
// Показываем индикатор набора
showTypingIndicator();
try {
// Получаем ответ от ИИ
const response = await askTeacher(text);
hideTypingIndicator();
addMessage(response, 'teacher');
} catch (error) {
hideTypingIndicator();
addMessage('Извини, я временно недоступен. Попробуй ещё раз!', 'teacher');
console.error('Teacher error:', error);
}
}
function addMessage(text, type) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
messageDiv.innerHTML = `<div class="message-bubble">${text}</div>`;
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function showTypingIndicator() {
const typing = document.createElement('div');
typing.className = 'message teacher';
typing.id = 'typing-indicator';
typing.innerHTML = `
<div class="message-bubble">
<div class="typing-indicator">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
`;
chatMessages.appendChild(typing);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function hideTypingIndicator() {
const typing = document.getElementById('typing-indicator');
if (typing) typing.remove();
}
async function askTeacher(message) {
// Получаем контекст уровня
const levelContext = currentLevel ? `Current student level: ${currentLevel}. ` : '';
const messages = [
{
role: 'system',
content: `You are a friendly and supportive English teacher. Your tasks:
1. Communicate at the student's level
2. Correct mistakes gently and explain grammar rules
3. Help with vocabulary and constructions
4. Encourage and motivate
5. Answer grammar questions
${levelContext}Respond briefly (2-4 sentences), in a friendly manner. If the student writes in Russian, respond in Russian with English examples. If in English, respond in English but you can add translations of difficult words.`
},
{
role: 'user',
content: message
}
];
try {
const response = await fetch('/api/ai', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ messages })
});
if (!response.ok) {
throw new Error('AI error: ' + response.status);
}
const data = await response.json();
const answer = data.choices?.[0]?.message?.content;
if (!answer) {
throw new Error('Empty AI response');
}
return answer.replace(/\n/g, '<br>');
} catch (error) {
console.error('AI request failed:', error);
return 'Sorry, I can\'t respond right now. But I\'m here to help you with English!';
}
}
// Запуск
init();

75
server.js Normal file
View File

@ -0,0 +1,75 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = process.env.PORT || 3000;
function body(req) {
return new Promise((resolve) => {
let s = '';
req.on('data', (c) => (s += c));
req.on('end', () => {
try {
resolve(JSON.parse(s || '{}'));
} catch (_) {
resolve({});
}
});
});
}
http.createServer(async (req, res) => {
// CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
return res.end();
}
// API: AI запрос
if (req.method === 'POST' && req.url === '/api/ai') {
try {
const data = await body(req);
const aiResponse = await fetch(process.env.AI_BASE_URL + '/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + process.env.AI_API_KEY
},
body: JSON.stringify({
model: process.env.AI_MODEL,
messages: data.messages || []
})
});
if (!aiResponse.ok) {
throw new Error('AI error: ' + aiResponse.status);
}
const result = await aiResponse.json();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: error.message }));
}
return;
}
// Статика
let file = req.url === '/' ? '/index.html' : req.url.split('?')[0];
const full = path.join(__dirname, file);
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
const ext = path.extname(full);
const type = ext === '.css' ? 'text/css' : ext === '.js' ? 'application/javascript' : 'text/html';
res.writeHead(200, { 'Content-Type': type + '; charset=utf-8' });
return res.end(fs.readFileSync(full));
}
res.writeHead(404);
res.end('Not found');
}).listen(PORT, () => console.log('Сервер запущен на порту ' + PORT));

190
style.css
View File

@ -88,6 +88,196 @@ body {
display: none;
}
/* Кнопка учителя */
.teacher-fab {
position: fixed;
bottom: 30px;
right: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50px;
padding: 16px 24px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.4);
transition: all 0.3s;
display: flex;
align-items: center;
gap: 10px;
z-index: 999;
}
.teacher-fab:hover {
transform: scale(1.05);
box-shadow: 0 12px 40px rgba(102, 126, 234, 0.6);
}
.teacher-icon {
font-size: 24px;
}
/* Чат учителя */
.teacher-chat {
position: fixed;
bottom: 100px;
right: 30px;
width: 400px;
max-height: 600px;
background: white;
border-radius: 16px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
z-index: 1000;
overflow: hidden;
}
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h3 {
margin: 0;
font-size: 18px;
}
.chat-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
transition: all 0.3s;
}
.chat-close:hover {
background: rgba(255, 255, 255, 0.3);
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #f8f9ff;
max-height: 400px;
}
.message {
margin-bottom: 16px;
display: flex;
}
.message.student {
justify-content: flex-end;
}
.message.teacher {
justify-content: flex-start;
}
.message-bubble {
max-width: 80%;
padding: 12px 16px;
border-radius: 16px;
line-height: 1.5;
}
.message.teacher .message-bubble {
background: white;
border: 1px solid #e0e0e0;
color: #333;
border-bottom-left-radius: 4px;
}
.message.student .message-bubble {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 4px;
}
.chat-input-area {
padding: 16px;
background: white;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 10px;
}
.chat-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 24px;
font-size: 14px;
}
.chat-input:focus {
outline: none;
border-color: #667eea;
}
.send-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
width: 44px;
height: 44px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
transition: all 0.3s;
}
.send-btn:hover {
transform: scale(1.1);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 12px 16px;
}
.typing-dot {
width: 8px;
height: 8px;
background: #ccc;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-8px);
}
}
.btn-back {
background: white;
border: 2px solid #667eea;