- add locale file, ru
- AI in Quizes, now it is off
- bug of translation in Courses
- fronted of tags in Course Detail in Mobile ver
- another locale bugs
This commit is contained in:
Alexandrina-Kuzeleva
2025-11-20 12:44:53 +03:00
parent 6b13b1231a
commit c4d185f2d6
8 changed files with 3015 additions and 2979 deletions

View File

@@ -52,9 +52,9 @@ const contentMap = {
'To upload Image, Video, Audio or PDF from your system, click on the add icon and select upload from the menu. Then choose the file you want to add to the lesson and it gets added to your lesson.',
},
youtube: {
title: 'How to add a YouTube Video?',
title: 'How to add a YouTube Video/RuTube?',
description:
'Copy the URL of the video from YouTube and paste it in the editor.',
'Copy the URL of the video from YouTube/RuTube and paste it in the editor.',
},
remove: {
title: 'How to remove an embed?',

View File

@@ -1,11 +1,11 @@
<template>
<Dialog
:options="{
title: 'Edit your profile',
title: __('Edit your profile'),
size: '3xl',
actions: [
{
label: 'Save',
label: __('Save'),
variant: 'solid',
onClick: (close) => saveProfile(close),
},

View File

@@ -56,7 +56,7 @@
<CourseInstructors :instructors="course.data.instructors" />
</div>
</div>
<div v-if="course.data.tags" class="flex my-4 w-fit">
<div v-if="course.data.tags" class="flex flex-wrap gap-2 mt-3 mb-3 max-w-full">
<Badge
theme="gray"
size="lg"

View File

@@ -229,7 +229,7 @@ const updateTabFilter = () => {
delete filters.value['published_on']
delete filters.value['upcoming']
if (currentTab.value == 'Enrolled' && user.data?.is_student) {
if ((currentTab.value == 'Enrolled' && user.data?.is_student) || (currentTab.value == 'Зачислен' && user.data?.is_student)) {
filters.value['enrolled'] = 1
delete filters.value['published']
} else {
@@ -240,24 +240,25 @@ const updateTabFilter = () => {
filters.value['published'] = 1
filters.value['upcoming'] = 0
filters.value['live'] = 1
} else if (currentTab.value == 'Upcoming') {
} else if (currentTab.value == 'Upcoming' || currentTab.value == 'Предстоящие') {
filters.value['upcoming'] = 1
} else if (currentTab.value == 'New') {
filters.value['published'] = 1
} else if (currentTab.value == 'New' || currentTab.value == 'Новый') {
filters.value['published'] = 1
filters.value['published_on'] = [
'>=',
dayjs().add(-3, 'month').format('YYYY-MM-DD'),
]
} else if (currentTab.value == 'Created') {
} else if (currentTab.value == 'Created' || currentTab.value == 'Создано') {
filters.value['created'] = 1
} else if (currentTab.value == 'Unpublished') {
} else if (currentTab.value == 'Unpublished' || currentTab.value == 'Неопубликовано') {
filters.value['published'] = 0
}
}
}
const updateStudentFilter = () => {
if (!user.data || (user.data?.is_student && currentTab.value != 'Enrolled')) {
if (!user.data || (user.data?.is_student && currentTab.value != 'Enrolled') || (currentTab.value != 'Зачислен' && user.data?.is_student)) {
filters.value['published'] = 1
}
}

View File

@@ -18,14 +18,14 @@
<div class="w-5/6 mx-auto">
<FormControl
v-model="lesson.title"
label="Title"
:label="__('Title')"
class="mb-4"
:required="true"
/>
<FormControl
v-model="lesson.include_in_preview"
type="checkbox"
label="Include in Preview"
:label="__('Include in Preview')"
/>
</div>
<div class="border-t mt-4">
@@ -485,17 +485,17 @@ const editCurrentLesson = () => {
const validateLesson = () => {
if (!lesson.title) {
return 'Title is required'
return __('Title is required')
}
if (!lesson.content) {
return 'Content is required'
return __('Content is required')
}
}
const breadcrumbs = computed(() => {
let crumbs = [
{
label: 'Courses',
label: __('Courses'),
route: { name: 'Courses' },
},
{
@@ -506,7 +506,7 @@ const breadcrumbs = computed(() => {
if (lessonDetails?.data?.lesson) {
crumbs.push({
label: lessonDetails.data.lesson.title,
label: __(lessonDetails.data.lesson.title),
route: {
name: 'Lesson',
params: {
@@ -518,7 +518,7 @@ const breadcrumbs = computed(() => {
})
}
crumbs.push({
label: lessonDetails?.data?.lesson ? 'Edit Lesson' : 'Create Lesson',
label: lessonDetails?.data?.lesson ? __('Edit Lesson') : __('Create Lesson'),
route: {
name: 'LessonForm',
params: {
@@ -534,8 +534,8 @@ const breadcrumbs = computed(() => {
usePageMeta(() => {
return {
title: lessonDetails?.data?.lesson
? lessonDetails.data.lesson.title
: 'New Lesson',
? __(lessonDetails.data.lesson.title)
: __('New Lesson'),
icon: brand.favicon,
}
})

View File

@@ -206,7 +206,7 @@ const getTabButtons = () => {
const breadcrumbs = computed(() => {
let crumbs = [
{
label: 'People',
label: __('People'),
},
{
label: profile.data?.full_name,

View File

@@ -11,6 +11,32 @@
>
<Quiz :quizName="quizID" />
</div>
<div>
<!--<button @click="toggleChatGPT" class="btn btn-primary">Решить с ChatGPT</button>-->
</div>
<div v-if="showChat" class="chat-container mt-4">
<h2>AI-тьютор</h2>
<div class="chat-window">
<div id="chat-box" class="chat-box">
<div v-for="(message, index) in chatHistory" :key="index" class="message">
<span :class="{ 'user-message': message.isUser, 'ai-message': !message.isUser }">
{{ message.text }}
</span>
</div>
<div v-if="chatHistory.length === 0" class="placeholder-text">Загрузка ответа...</div>
</div>
</div>
<div class="chat-input mt-4">
<input
v-model="userInput"
type="text"
placeholder="Введите ваше сообщение..."
class="w-full p-2 border rounded-md"
@keyup.enter="sendMessage"
/>
<button @click="sendMessage" class="btn btn-primary ml-2">Отправить</button>
</div>
</div>
</template>
<script setup>
import Quiz from '@/components/Quiz.vue'
@@ -24,6 +50,12 @@ const user = inject('$user')
const router = useRouter()
const fromLesson = ref(false)
const showChat = ref(false)
const chatResponse = ref(null) // Временная переменная для текущего ответа
const currentQuestionIndex = ref(0) // Индекс текущего вопроса
const userInput = ref('') // Поле для ввода сообщения
const chatHistory = ref([]) // История сообщений
onMounted(() => {
if (!user.data) {
router.push({ name: 'Courses' })
@@ -41,6 +73,21 @@ const props = defineProps({
},
})
const quizData = createResource({
url: 'frappe.client.get',
params: {
doctype: 'LMS Quiz',
name: props.quizID,
},
auto: true,
onSuccess: (data) => {
console.log('[DEBUG] quizData onSuccess:', data)
},
onError: (err) => {
console.error('[DEBUG] quizData onError:', err)
},
})
const title = createResource({
url: 'frappe.client.get_value',
params: {
@@ -63,4 +110,283 @@ usePageMeta(() => {
icon: brand.favicon,
}
})
const handleCurrentQuestion = (index) => {
currentQuestionIndex.value = index
console.log('[DEBUG] Текущий индекс вопроса из Quiz.vue:', index)
}
const sendMessage = async () => {
if (!userInput.value.trim()) return;
// Добавляем сообщение пользователя в историю
chatHistory.value.push({ text: userInput.value, isUser: true });
console.log('[DEBUG] Пользовательское сообщение:', userInput.value);
// Сбрасываем поле ввода
const userMessage = userInput.value;
userInput.value = '';
try {
console.log('[DEBUG] Отправка запроса к прокси с пользовательским сообщением...');
const res = await fetch('https://openai.enlightrussia.ru/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': 'a38ed6248c82e31f014b7459479d4c75154e41a331f8a4d9b4afc4b10cbc884a'
},
body: JSON.stringify({
prompt: generatePrompt(userMessage),
model: 'gpt-4o',
system_prompt: `Ты — опытный, доброжелательный и очень терпеливый учитель. Твоя задача — сопровождать ученика в самостоятельном разборе задачи, не раскрывая ему готовое решение и не выдавая правильного ответа напрямую.
Ни при каких условиях нельзя выдавать готовый ответ. Ученик должен сам дойти до него. Обязательно нужно разбирать процесс решений по шагам, задавая ученику по одному вопросу за один шаг и ожидая от него ответа.
Говори и отвечай на русском языке, простыми и понятными формулировками. Будь вежлив, отзывчив и дружелюбен. Показывай искреннее желание помочь. Подбадривай ученика, если он ошибается или затрудняется.`
})
});
console.log('[DEBUG] Ответ от прокси:', res.status, res.statusText);
if (!res.ok) {
const errorText = await res.text();
console.log('[DEBUG] Ошибка ответа от прокси:', errorText);
throw new Error(`Ошибка API: ${res.status} ${errorText}`);
}
const data = await res.json();
console.log('[DEBUG] Получен ответ от GPT:', data);
chatHistory.value.push({ text: data.answer, isUser: false });
} catch (err) {
console.error('[DEBUG] Ошибка в sendMessage:', err);
chatHistory.value.push({ text: `Ошибка: ${err.message}`, isUser: false });
}
};
const toggleChatGPT = async () => {
console.log('[DEBUG] toggleChatGPT вызван, showChat:', showChat.value);
showChat.value = !showChat.value;
if (!showChat.value) {
chatResponse.value = null;
chatHistory.value = [];
console.log('[DEBUG] Чат закрыт, chatResponse и история сброшены');
return;
}
try {
console.log('[DEBUG] Начало обработки, quizData:', quizData);
if (quizData.loading) {
chatHistory.value.push({ text: 'Загрузка данных квиза...', isUser: false });
console.log('[DEBUG] Данные квиза загружаются');
return;
}
if (quizData.error) {
throw new Error(`Ошибка загрузки квиза: ${quizData.error.message}`);
}
const quiz = quizData.data;
if (!quiz) {
chatHistory.value.push({ text: 'Данные квиза не загружены', isUser: false });
console.log('[DEBUG] quizData.data отсутствует');
return;
}
const quiz_title = quiz.title || 'Без названия';
console.log('[DEBUG] Получен quiz:', quiz, quiz_title);
if (!quiz.questions || quiz.questions.length === 0) {
chatHistory.value.push({ text: 'Вопросы не найдены', isUser: false });
console.log('[DEBUG] Вопросы не найдены в quiz:', quiz);
return;
}
// Используем текущий индекс
const currentQuestion = quiz.questions[currentQuestionIndex.value];
if (!currentQuestion?.question_detail) {
chatHistory.value.push({ text: 'Детали вопроса не найдены', isUser: false });
console.log('[DEBUG] Текущий вопрос отсутствует:', currentQuestion);
return;
}
// Создаём и ждём загрузки данных вопроса
const questionData = await new Promise((resolve) => {
const resource = createResource({
url: 'frappe.client.get',
params: {
doctype: 'LMS Question',
name: currentQuestion.question,
},
auto: true,
onSuccess: (data) => {
console.log('[DEBUG] questionData onSuccess:', data);
resolve(data);
},
onError: (err) => {
console.error('[DEBUG] questionData onError:', err);
resolve(null);
},
});
});
if (!questionData) {
chatHistory.value.push({ text: 'Данные вопроса не загружены', isUser: false });
console.log('[DEBUG] Данные вопроса отсутствуют');
return;
}
let questionDataPrompt, question, prompt, correct_answer, options = [], possibilitys = [];
if (questionData.type === "Choices") {
question = currentQuestion.question_detail;
options = [
questionData.option_1 || '',
questionData.option_2 || '',
questionData.option_3 || '',
questionData.option_4 || '',
].filter(Boolean);
correct_answer = null;
for (let i = 1; i <= 4; i++) {
if (questionData[`is_correct_${i}`] === 1) {
correct_answer = questionData[`option_${i}`];
console.log(`Правильный ответ ${i}:`, correct_answer);
break;
}
}
console.log('[DEBUG] Получен вопрос, варианты и ответ:', { question, options, correct_answer });
questionDataPrompt = `Это вопрос типа: ${questionData.type} из квиза под названием ${quiz_title}.\n${question}\nВарианты ответа: ${options.join(', ')}\равильный ответ: ${correct_answer}`;
console.log('[DEBUG] Данные для промта:', questionDataPrompt)
prompt = generatePrompt(questionDataPrompt);
} else if (questionData.type === "User Input") {
question = currentQuestion.question_detail;
possibilitys = [
questionData.possibility_1 || '',
questionData.possibility_2 || '',
questionData.possibility_3 || '',
questionData.possibility_4 || '',
].filter(Boolean);
console.log('[DEBUG] Получен вопрос и возможные варианты:', { question, possibilitys });
questionDataPrompt = `Это вопрос типа: ${questionData.type} из квиза под названием ${quiz_title}.\n${question}\nВозможные варианты ответа: ${possibilitys.join(', ')}`;
console.log('[DEBUG] Данные для промта:', questionDataPrompt)
prompt = generatePrompt(questionDataPrompt);
} else {
question = currentQuestion.question_detail;
console.log('[DEBUG] Получен вопрос:', { question });
questionDataPrompt = `Это вопрос типа: ${questionData.type} из квиза под названием ${quiz_title}.\n${question}`;
console.log('[DEBUG] Данные для промта:', questionDataPrompt)
prompt = generatePrompt(questionDataPrompt);
}
if (!question) {
chatHistory.value.push({ text: 'Текст вопроса не найден', isUser: false });
console.log('[DEBUG] Текст вопроса отсутствует в currentQuestion:', currentQuestion);
return;
}
console.log('[DEBUG] Сформирован промпт:', prompt);
console.log('[DEBUG] Отправка запроса к прокси...');
const res = await fetch('https://openai.enlightrussia.ru/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': 'a38ed6248c82e31f014b7459479d4c75154e41a331f8a4d9b4afc4b10cbc884a'
},
body: JSON.stringify({
prompt: prompt,
model: 'gpt-4o',
system_prompt: `1. Роль
Ты — опытный, доброжелательный и очень терпеливый учитель. Твоя задача — сопровождать ученика в самостоятельном разборе задачи, не раскрывая ему готовое решение и не выдавая правильного ответа напрямую.
Ни при каких условиях нельзя выдавать готовый ответ. Ученик должен сам дойти до него. Обязательно нужно разбирать процесс решений по шагам, задавая ученику по одному вопросу за один шаг и ожидая от него ответа.
2. Стиль общения
Говори и отвечай на русском языке, простыми и понятными формулировками.
Будь вежлив, отзывчив и дружелюбен.
Показывай искреннее желание помочь.
Подбадривай ученика, если он ошибается или затрудняется.
3. Пошаговый подход
Начинай с запроса к ученику: попроси его показать или описать задание. Если возможно, пусть он загрузит скриншот, документ или текст задания.
Не говори финальный ответ сразу. Вместо этого:
1. Спроси, что ученик уже знает или какие идеи у него есть.
2. Дай наводящие вопросы, чтобы понять, в каком месте он испытывает затруднение.
3. Подсказывай принципы или формулы, которые могут помочь, но не окончательный результат.
4. Делай это пошагово, пока ученик не найдёт решение самостоятельно (или не выскажет версию, близкую к правильной).
При необходимости:
Повтори ключевые определения и формулы.
Предложи разобрать аналогичный, но более простой пример. Обязательно нужно разбирать процесс решений по шагам, задавая ученику по одному вопросу за один шаг и ожидая от него ответа. Ни в коем случае не давай сразу решение.`
})
});
console.log('[DEBUG] Ответ от прокси:', res.status, res.statusText);
if (!res.ok) {
const errorText = await res.text();
console.log('[DEBUG] Ошибка ответа от прокси:', errorText);
throw new Error(`Ошибка API: ${res.status} ${errorText}`);
}
const data = await res.json();
console.log('[DEBUG] Получен ответ от GPT:', data);
chatHistory.value.push({ text: data.answer, isUser: false });
chatResponse.value = null; // Сбрасываем chatResponse после добавления в историю
} catch (err) {
console.error('[DEBUG] Ошибка в toggleChatGPT:', err);
chatHistory.value.push({ text: `Ошибка: ${err.message}`, isUser: false });
chatResponse.value = null;
}
};
// Функция для генерации промпта с историей
const generatePrompt = (userMsg) => {
let prompt = `Текущая задача: "${userMsg}".\n`;
prompt += 'История диалога:\n';
chatHistory.value.forEach(msg => {
prompt += `${msg.isUser ? 'Ученик' : 'Учитель'}: ${msg.text}\n`;
});
prompt += `\nНовое сообщение от ученика: ${userMsg}`;
return prompt;
};
updateDocumentTitle(pageMeta)
</script>
<style scoped>
.chat-container {
max-width: 700px;
margin: 0 auto;
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
}
.chat-window {
max-height: 400px;
overflow-y: auto;
padding: 10px;
border: 1px solid #e5e7eb;
border-radius: 4px;
}
.chat-box {
min-height: 100px;
}
.placeholder-text {
color: #94a3b8;
text-align: center;
}
.message {
margin: 5px 0;
}
.user-message {
background-color: #e3f2fd;
padding: 5px 10px;
border-radius: 5px;
display: inline-block;
}
.ai-message {
background-color: #f0f0f0;
padding: 5px 10px;
border-radius: 5px;
display: inline-block;
}
.chat-input input {
border: 1px solid #e5e7eb;
}
.btn-primary {
background-color: #2563eb;
color: white;
padding: 8px 16px;
border-radius: 4px;
}
</style>