feat: добавил изменение промта для админа
Backend CI / build-and-test (push) Failing after 11m26s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 14m2s
Frontend CI / build-and-check (push) Failing after 19m55s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 14m7s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 14m59s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 15m0s

This commit is contained in:
2026-05-21 21:58:33 +03:00
parent 27a2811806
commit 935e4ed37a
22 changed files with 1880 additions and 15 deletions
+157 -1
View File
@@ -41,6 +41,13 @@ const totalCount = ref(0)
const totalPages = ref(0)
const reanalyzingId = ref<number | null>(null)
const error = ref('')
const promptText = ref('')
const savedPromptText = ref('')
const promptUpdatedAt = ref<string | null>(null)
const promptLoading = ref(false)
const promptSaving = ref(false)
const promptError = ref('')
const promptSuccess = ref('')
const rows = computed(() =>
reviews.value.map((review) => ({
@@ -73,12 +80,63 @@ const pageStart = computed(() =>
const pageEnd = computed(() => Math.min(page.value * pageSize.value, totalCount.value))
const canGoPrev = computed(() => page.value > 1)
const canGoNext = computed(() => page.value < totalPages.value)
const promptStatusLabel = computed(() => {
if (promptLoading.value) return 'Загрузка...'
if (!promptUpdatedAt.value) return 'Базовый промпт'
return `Обновлён ${new Date(promptUpdatedAt.value).toLocaleString('ru-RU')}`
})
const canSavePrompt = computed(
() =>
!promptLoading.value &&
!promptSaving.value &&
promptText.value.trim().length > 0 &&
promptText.value !== savedPromptText.value,
)
function formatQuality(value: number | null | undefined) {
if (value === null || value === undefined) return '—'
return Number(value).toFixed(2)
}
async function fetchPrompt() {
promptLoading.value = true
promptError.value = ''
promptSuccess.value = ''
try {
const result = await reviewsApi.getPrompt()
promptText.value = result.prompt
savedPromptText.value = result.prompt
promptUpdatedAt.value = result.updatedAt ?? null
} catch (err) {
promptError.value = err instanceof Error ? err.message : 'Не удалось загрузить промпт.'
} finally {
promptLoading.value = false
}
}
async function savePrompt() {
promptError.value = ''
promptSuccess.value = ''
if (!promptText.value.trim()) {
promptError.value = 'Промпт не должен быть пустым.'
return
}
promptSaving.value = true
try {
const result = await reviewsApi.updatePrompt({ prompt: promptText.value })
promptText.value = result.prompt
savedPromptText.value = result.prompt
promptUpdatedAt.value = result.updatedAt ?? null
promptSuccess.value = 'Промпт сохранён.'
} catch (err) {
promptError.value = err instanceof Error ? err.message : 'Не удалось сохранить промпт.'
} finally {
promptSaving.value = false
}
}
async function fetchReviews() {
loading.value = true
error.value = ''
@@ -115,7 +173,10 @@ async function reanalyze(id: number) {
}
}
onMounted(fetchReviews)
onMounted(() => {
void fetchPrompt()
void fetchReviews()
})
</script>
<template>
@@ -130,6 +191,38 @@ onMounted(fetchReviews)
</button>
</div>
<GlassCard>
<div class="prompt-header">
<div>
<div class="section-title">Промпт LLM-анализа</div>
<p class="prompt-subtitle">
Шаблон применяется к новым проверкам и ручному повторному анализу отзывов.
</p>
</div>
<span class="prompt-status">{{ promptStatusLabel }}</span>
</div>
<form class="prompt-form" @submit.prevent="savePrompt">
<textarea
v-model="promptText"
class="glass-input prompt-textarea"
rows="9"
:disabled="promptLoading || promptSaving"
placeholder="Загрузка промпта..."
></textarea>
<div class="prompt-footer">
<div class="prompt-messages">
<span class="prompt-hint">Обязательные плейсхолдеры: {lectureContext}, {reviewText}</span>
<span v-if="promptError" class="prompt-error">{{ promptError }}</span>
<span v-else-if="promptSuccess" class="prompt-success">{{ promptSuccess }}</span>
</div>
<button class="btn-primary" type="submit" :disabled="!canSavePrompt">
{{ promptSaving ? 'Сохраняем...' : 'Сохранить промпт' }}
</button>
</div>
</form>
</GlassCard>
<GlassCard>
<EmptyState v-if="error" title="Не удалось загрузить отзывы" :subtitle="error" />
<EmptyState
@@ -240,6 +333,69 @@ onMounted(fetchReviews)
margin: 4px 0 0;
color: var(--color-text-secondary);
}
.section-title {
font-size: 16px;
font-weight: 700;
}
.prompt-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.prompt-subtitle {
margin: 4px 0 0;
color: var(--color-text-secondary);
font-size: 13px;
}
.prompt-status {
flex: 0 0 auto;
border: 1px solid var(--color-border-glass);
border-radius: 999px;
padding: 5px 10px;
background: var(--color-white-a72);
color: var(--color-text-secondary);
font-size: 12px;
}
.prompt-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.prompt-textarea {
min-height: 250px;
resize: vertical;
line-height: 1.45;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
}
.prompt-textarea:disabled {
color: var(--color-text-secondary);
cursor: wait;
}
.prompt-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.prompt-messages {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 240px;
color: var(--color-text-secondary);
font-size: 12px;
}
.prompt-error {
color: var(--color-error);
}
.prompt-success {
color: var(--color-success-text);
}
.review-text {
display: inline-block;
max-width: 320px;