feat: обновил шаблон отзыва и улучшил анализ отзывов преподавателя

This commit is contained in:
2026-06-01 12:31:30 +03:00
parent 09d3d2778d
commit 450f2e2418
3 changed files with 41 additions and 59 deletions
@@ -6,11 +6,37 @@ public static class ReviewPromptTemplate
public const string ReviewTextPlaceholder = "{reviewText}";
public const string Default = """
Проанализируй отзыв студента о лекции. Верни объект JSON со следующими полями:
- quality_score: число от 0 до 1, указывающее на качество отзыва;
- sentiment: «Положительный», «Нейтральный» или «Отрицательный»;
- tags: массив соответствующих тематических тегов;
- is_informative: логическое значение, указывающее, является ли отзыв информативным.
Проанализируй отзыв студента о лекции. Главная задача - определить, насколько отзыв информативен и полезен для аналитики качества лекции и обратной связи преподавателю.
Верни только валидный JSON-объект без Markdown, пояснений и дополнительного текста:
{
"quality_score": 0.0,
"sentiment": "Нейтральный",
"tags": [],
"is_informative": false
}
Правила оценки:
- quality_score: число от 0 до 1. Оценивай содержательность, конкретику, аргументацию, конструктивность и развернутость отзыва, а не оценку лекции как таковой.
- is_informative: true, если отзыв содержит конкретные наблюдения о лекции, преподавании, структуре, материалах, темпе, сложности, практике, организации или полезности. false для односложных, шаблонных, эмоциональных без конкретики или нерелевантных отзывов.
- sentiment: строго одно из значений "Положительный", "Нейтральный", "Отрицательный".
- tags: массив коротких тематических тегов на русском языке. Используй 1-5 тегов, если они подходят; для неинформативного отзыва можно вернуть пустой массив.
Базовые теги:
- "структура лекции"
- "понятность объяснения"
- "темп"
- "сложность"
- "практические примеры"
- "материалы"
- "актуальность темы"
- "вовлеченность"
- "организация"
- "технические проблемы"
- "польза для обучения"
- "неинформативный отзыв"
Можно добавлять новые теги, если они точнее отражают содержание отзыва. Не добавляй теги, которых нет в тексте отзыва или контексте лекции.
Контекст лекции: {lectureContext}
Текст отзыва: {reviewText}
@@ -9,7 +9,6 @@ import { mapApiReview } from '@/api/mappers'
import { useLecturesStore } from '@/stores/lectures'
import { useAuthStore } from '@/stores/auth'
const ratingTrend = [4.2, 4.5, 4.6, 4.8, 4.7]
const lecturesStore = useLecturesStore()
const auth = useAuthStore()
const reviews = ref<Review[]>([])
@@ -17,8 +16,9 @@ const reviews = ref<Review[]>([])
const positive = computed(() => reviews.value.filter((r) => r.sentiment === 'positive').length)
const neutral = computed(() => reviews.value.filter((r) => r.sentiment === 'neutral').length)
const negative = computed(() => reviews.value.filter((r) => r.sentiment === 'negative').length)
const total = computed(() => reviews.value.length || 1)
const pct = (value: number) => Math.round((value / total.value) * 100)
const total = computed(() => reviews.value.length)
const pct = (value: number) => (total.value ? Math.round((value / total.value) * 100) : 0)
const ratio = (value: number) => `${value}/${total.value}`
async function fetchTeacherAnalytics() {
if (!auth.user?.id) return
@@ -39,37 +39,28 @@ watch(() => auth.user?.id, fetchTeacherAnalytics)
<h1 class="page-title">Аналитика преподавателя</h1>
<div class="grid">
<GlassCard>
<div class="section-title">Динамика оценок</div>
<div class="chart">
<div v-for="(value, i) in ratingTrend" :key="i" class="bar">
<div class="bar-fill" :style="{ height: `${value * 18}px` }"></div>
<span class="bar-label">Нед {{ i + 1 }}</span>
</div>
</div>
<div class="avg">Средняя оценка: 4.6</div>
</GlassCard>
<GlassCard>
<div class="section-title">Sentiment-анализ отзывов</div>
<div class="sentiment">
<div>
<div class="sentiment-label">Позитивные {{ pct(positive) }}%</div>
<ProgressBar :value="pct(positive)" :max="100" />
<div class="sentiment-label">Позитивные {{ ratio(positive) }}</div>
<ProgressBar :value="pct(positive)" :max="100" :text="ratio(positive)" />
</div>
<div>
<div class="sentiment-label">Нейтральные {{ pct(neutral) }}%</div>
<div class="sentiment-label">Нейтральные {{ ratio(neutral) }}</div>
<ProgressBar
:value="pct(neutral)"
:max="100"
:text="ratio(neutral)"
color="linear-gradient(90deg, #7DD3FC, #BAE6FD)"
/>
</div>
<div>
<div class="sentiment-label">Негативные {{ pct(negative) }}%</div>
<div class="sentiment-label">Негативные {{ ratio(negative) }}</div>
<ProgressBar
:value="pct(negative)"
:max="100"
:text="ratio(negative)"
color="linear-gradient(90deg, #FCA5A5, #FECACA)"
/>
</div>
@@ -117,32 +108,6 @@ watch(() => auth.user?.id, fetchTeacherAnalytics)
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.chart {
display: flex;
gap: 12px;
align-items: flex-end;
height: 160px;
padding: 10px 0;
}
.bar {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.bar-fill {
width: 26px;
border-radius: 6px 6px 0 0;
background: linear-gradient(180deg, #22c55e, #86efac);
}
.bar-label {
font-size: 11px;
color: var(--color-text-secondary);
}
.avg {
margin-top: 6px;
font-weight: 600;
}
.sentiment {
display: flex;
flex-direction: column;
@@ -20,9 +20,6 @@ const upcoming = computed(() =>
const enrolledTotal = computed(() =>
teacherLectures.value.reduce((sum, l) => sum + l.enrolledSeats, 0),
)
const visibility = computed(() =>
teacherLectures.value.length ? Math.min(100, Math.round(enrolledTotal.value * 4)) : 0,
)
function fetchTeacherLectures() {
if (!auth.user?.id) return
@@ -48,13 +45,7 @@ watch(() => auth.user?.id, fetchTeacherLectures)
<div class="stats-row">
<StatsWidget label="Предстоящие лекции" :value="upcoming.length" icon="📅" color="green" />
<StatsWidget label="Записавшихся" :value="enrolledTotal" icon="👥" color="aqua" />
<StatsWidget label="Средняя оценка" :value="'—'" icon="⭐" color="orange" />
<StatsWidget
label="Вовлеченность вне направления"
:value="`${visibility}%`"
icon="🌍"
color="purple"
/>
<StatsWidget label="Средняя оценка (0-1)" :value="'—'" icon="⭐" color="orange" />
</div>
<GlassCard>