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
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:
@@ -11,10 +11,12 @@ import type {
|
||||
PagedResult,
|
||||
ReviewDto,
|
||||
ReviewQuery,
|
||||
ReviewPromptDto,
|
||||
SyncResultDto,
|
||||
SyncScheduleRequest,
|
||||
SyncStatusDto,
|
||||
TagDto,
|
||||
UpdateReviewPromptRequest,
|
||||
UserAchievementDto,
|
||||
CurrentUserDto,
|
||||
UserDto,
|
||||
@@ -183,6 +185,12 @@ export const reviewsApi = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ lectureId: Number(lectureId), rating, text }),
|
||||
}),
|
||||
getPrompt: () => apiRequest<ReviewPromptDto>('/reviews/llm-prompt'),
|
||||
updatePrompt: (payload: UpdateReviewPromptRequest) =>
|
||||
apiRequest<ReviewPromptDto>('/reviews/llm-prompt', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
listPage: listReviewsPage,
|
||||
async list(query: ReviewQuery = { PageSize: 100 }) {
|
||||
return (await listReviewsPage(query)).items
|
||||
|
||||
@@ -136,6 +136,15 @@ export interface ReviewDto {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface ReviewPromptDto {
|
||||
prompt: string
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateReviewPromptRequest {
|
||||
prompt: string
|
||||
}
|
||||
|
||||
export interface AchievementDto {
|
||||
id: number
|
||||
name?: string | null
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user