Compare commits
No commits in common. "1117d55245f36b8a02339201a21d05d7e1f585a8" and "39295e59241e7995aec2f3de24f85194c4c4596b" have entirely different histories.
1117d55245
...
39295e5924
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +0,0 @@
|
|||||||
.env
|
|
||||||
node_modules/
|
|
||||||
.vibe42-run.log
|
|
||||||
.vibe42-run.pid
|
|
||||||
data.json
|
|
||||||
30
index.html
30
index.html
@ -15,12 +15,6 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<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">
|
<section id="levels-section" class="section">
|
||||||
<h2>Выберите ваш уровень</h2>
|
<h2>Выберите ваш уровень</h2>
|
||||||
@ -85,30 +79,6 @@
|
|||||||
<div id="practice-content" class="content-grid"></div>
|
<div id="practice-content" class="content-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</main>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "english-learning",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "english-learning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "english-learning",
|
|
||||||
"private": true,
|
|
||||||
"type": "commonjs",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node server.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
127
script.js
127
script.js
@ -274,7 +274,6 @@ const levelsData = {
|
|||||||
|
|
||||||
// Текущее состояние
|
// Текущее состояние
|
||||||
let currentLevel = null;
|
let currentLevel = null;
|
||||||
let chatSessionId = null;
|
|
||||||
|
|
||||||
// DOM элементы
|
// DOM элементы
|
||||||
const levelsSection = document.getElementById('levels-section');
|
const levelsSection = document.getElementById('levels-section');
|
||||||
@ -285,14 +284,6 @@ const tabBtns = document.querySelectorAll('.tab-btn');
|
|||||||
const tabContents = document.querySelectorAll('.tab-content');
|
const tabContents = document.querySelectorAll('.tab-content');
|
||||||
const vocabSearch = document.getElementById('vocab-search');
|
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() {
|
function init() {
|
||||||
// Клик по уровням
|
// Клик по уровням
|
||||||
@ -322,14 +313,6 @@ function init() {
|
|||||||
vocabSearch.addEventListener('input', (e) => {
|
vocabSearch.addEventListener('input', (e) => {
|
||||||
filterVocabulary(e.target.value);
|
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показать уровень
|
// Показать уровень
|
||||||
@ -433,115 +416,5 @@ 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();
|
init();
|
||||||
|
|||||||
75
server.js
75
server.js
@ -1,75 +0,0 @@
|
|||||||
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
190
style.css
@ -88,196 +88,6 @@ body {
|
|||||||
display: none;
|
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 {
|
.btn-back {
|
||||||
background: white;
|
background: white;
|
||||||
border: 2px solid #667eea;
|
border: 2px solid #667eea;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user