feat: изменил логику анализа отзывов
Backend CI / build-and-test (push) Failing after 14m19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 12m5s
Frontend CI / build-and-check (push) Failing after 17m58s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 10m11s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 11m3s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m58s
Backend CI / build-and-test (push) Failing after 14m19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 12m5s
Frontend CI / build-and-check (push) Failing after 17m58s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 10m11s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 11m3s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m58s
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import GlassCard from '@/components/ui/GlassCard.vue'
|
||||
import StatsWidget from '@/components/ui/StatsWidget.vue'
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import { lecturesApi, reviewsApi, syncApi, usersApi } from '@/api'
|
||||
import type { LectureDto, SyncStatusDto, UserDto } from '@/api/types'
|
||||
@@ -25,7 +24,7 @@ onMounted(async () => {
|
||||
const [usersResult, lecturesResult, reviewsResult, syncResult] = await Promise.allSettled([
|
||||
usersApi.list({ PageSize: 100 }),
|
||||
lecturesApi.list({ PageSize: 100 }),
|
||||
reviewsApi.pendingPage({ Page: 1, PageSize: 1 }),
|
||||
reviewsApi.listPage({ Page: 1, PageSize: 1, LlmStatus: 'Pending' }),
|
||||
syncApi.status(),
|
||||
])
|
||||
if (usersResult.status === 'fulfilled') users.value = usersResult.value
|
||||
@@ -45,7 +44,7 @@ onMounted(async () => {
|
||||
<StatsWidget label="Лекций" :value="lectures.length" icon="books" color="aqua" />
|
||||
<StatsWidget label="Записей" :value="enrollmentCount" icon="calendar-event" color="orange" />
|
||||
<StatsWidget
|
||||
label="Отзывов в LLM"
|
||||
label="Отзывы на проверке"
|
||||
:value="pendingReviewsCount"
|
||||
icon="message-circle"
|
||||
color="purple"
|
||||
@@ -61,12 +60,6 @@ onMounted(async () => {
|
||||
Ошибка: {{ syncStatus.lastResult.error }}
|
||||
</div>
|
||||
</GlassCard>
|
||||
<GlassCard>
|
||||
<div class="section-title">Очередь LLM-анализа</div>
|
||||
<div class="queue-meta">В очереди: {{ pendingReviewsCount }} отзывов</div>
|
||||
<ProgressBar :value="Math.min(pendingReviewsCount * 10, 100)" :max="100" />
|
||||
<div class="queue-status">Следующая проверка через 12 минут</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -147,14 +140,4 @@ onMounted(async () => {
|
||||
color: var(--color-error);
|
||||
margin-top: 8px;
|
||||
}
|
||||
.queue-meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.queue-status {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import GlassCard from '@/components/ui/GlassCard.vue'
|
||||
import DataTable from '@/components/ui/DataTable.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import { reviewsApi } from '@/api'
|
||||
import { mapApiReview } from '@/api/mappers'
|
||||
import type { Review } from '@/types'
|
||||
|
||||
const columns = [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'lecture', label: 'Лекция' },
|
||||
{ key: 'student', label: 'Студент' },
|
||||
{ key: 'date', label: 'Дата' },
|
||||
{ key: 'status', label: 'Статус', align: 'center' },
|
||||
{ key: 'sentiment', label: 'Sentiment', align: 'center' },
|
||||
{ key: 'quality', label: 'Качество', align: 'center' },
|
||||
{ key: 'coins', label: 'Монеты', align: 'center' },
|
||||
{ key: 'actions', label: 'Действия', align: 'right' },
|
||||
]
|
||||
|
||||
const reviews = ref<Review[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const totalCount = ref(0)
|
||||
const totalPages = ref(0)
|
||||
const error = ref('')
|
||||
|
||||
const rows = computed(() =>
|
||||
reviews.value.map((review) => ({
|
||||
id: review.id,
|
||||
lecture: review.lectureId,
|
||||
student: review.userName,
|
||||
date: new Date(review.createdAt).toLocaleDateString('ru-RU'),
|
||||
status: review.status,
|
||||
sentiment: review.sentiment,
|
||||
quality: review.quality ?? 0,
|
||||
coins: review.coins ?? 0,
|
||||
})),
|
||||
)
|
||||
|
||||
const pageStart = computed(() =>
|
||||
totalCount.value === 0 ? 0 : (page.value - 1) * pageSize.value + 1,
|
||||
)
|
||||
const pageEnd = computed(() => Math.min(page.value * pageSize.value, totalCount.value))
|
||||
const canGoPrev = computed(() => page.value > 1)
|
||||
const canGoNext = computed(() => page.value < totalPages.value)
|
||||
|
||||
async function fetchPending() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const result = await reviewsApi.pendingPage({
|
||||
Page: page.value,
|
||||
PageSize: pageSize.value,
|
||||
})
|
||||
reviews.value = result.items.map(mapApiReview)
|
||||
totalCount.value = result.totalCount
|
||||
totalPages.value = result.totalPages
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Не удалось загрузить очередь LLM.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function goToPage(nextPage: number) {
|
||||
if (nextPage < 1 || (totalPages.value && nextPage > totalPages.value)) return
|
||||
page.value = nextPage
|
||||
await fetchPending()
|
||||
}
|
||||
|
||||
async function reanalyze(id: string) {
|
||||
await reviewsApi.reanalyze(id)
|
||||
await fetchPending()
|
||||
}
|
||||
|
||||
onMounted(fetchPending)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-llm page-content">
|
||||
<div class="header">
|
||||
<h1 class="page-title">Очередь LLM-анализа отзывов</h1>
|
||||
<button class="btn-primary" :disabled="loading" @click="fetchPending">
|
||||
{{ loading ? 'Загрузка...' : 'Обновить очередь' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<GlassCard>
|
||||
<EmptyState v-if="error" title="Не удалось загрузить очередь" :subtitle="error" />
|
||||
<EmptyState
|
||||
v-else-if="!rows.length && !loading"
|
||||
title="Очередь пуста"
|
||||
subtitle="Нет отзывов, ожидающих LLM-анализ."
|
||||
/>
|
||||
<div v-else class="table-section">
|
||||
<div class="pagination-bar">
|
||||
<span> В очереди {{ totalCount }} отзывов; показаны {{ pageStart }}–{{ pageEnd }} </span>
|
||||
<div class="pagination-actions">
|
||||
<button class="btn-ghost" :disabled="loading || !canGoPrev" @click="goToPage(page - 1)">
|
||||
Назад
|
||||
</button>
|
||||
<span>Страница {{ page }} из {{ totalPages || 1 }}</span>
|
||||
<button class="btn-ghost" :disabled="loading || !canGoNext" @click="goToPage(page + 1)">
|
||||
Вперёд
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable :columns="columns" :rows="rows">
|
||||
<template #status="{ value }">
|
||||
<StatusBadge :status="value" />
|
||||
</template>
|
||||
<template #quality="{ value }">
|
||||
<span
|
||||
:class="
|
||||
value >= 0.7
|
||||
? 'badge badge-green'
|
||||
: value >= 0.4
|
||||
? 'badge badge-orange'
|
||||
: 'badge badge-red'
|
||||
"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<button class="btn-ghost" @click="reanalyze(row.id)">Повторить</button>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-llm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.table-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.pagination-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@ import DataTable from '@/components/ui/DataTable.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import { reviewsApi } from '@/api'
|
||||
import type { ReviewDto } from '@/api/types'
|
||||
import type { ApiReviewLlmStatus, ApiReviewSentiment, ReviewDto } from '@/api/types'
|
||||
|
||||
const columns = [
|
||||
{ key: 'id', label: 'ID' },
|
||||
@@ -14,10 +14,7 @@ const columns = [
|
||||
{ key: 'rating', label: 'Оценка', align: 'center' },
|
||||
{ key: 'text', label: 'Текст' },
|
||||
{ key: 'date', label: 'Дата' },
|
||||
{ key: 'status', label: 'LLM-статус', align: 'center' },
|
||||
{ key: 'sentiment', label: 'Sentiment', align: 'center' },
|
||||
{ key: 'quality', label: 'Quality', align: 'center' },
|
||||
{ key: 'informativeTags', label: 'Informative / tags' },
|
||||
{ key: 'analysis', label: 'Результат нейронки' },
|
||||
{ key: 'actions', label: 'Действия', align: 'right' },
|
||||
]
|
||||
|
||||
@@ -33,12 +30,32 @@ const ratingLabel: Record<string, string> = {
|
||||
Dislike: '👎 Dislike',
|
||||
}
|
||||
|
||||
const statusFilters: Array<{ label: string; value: ApiReviewLlmStatus | 'All' }> = [
|
||||
{ label: 'Все', value: 'All' },
|
||||
{ label: 'На проверке', value: 'Pending' },
|
||||
{ label: 'Проверены', value: 'Analyzed' },
|
||||
{ label: 'Отклонены', value: 'Rejected' },
|
||||
]
|
||||
|
||||
const sentimentLabel: Record<ApiReviewSentiment, string> = {
|
||||
Positive: 'Позитивный',
|
||||
Neutral: 'Нейтральный',
|
||||
Negative: 'Негативный',
|
||||
}
|
||||
|
||||
const sentimentClass: Record<ApiReviewSentiment, string> = {
|
||||
Positive: 'badge-green',
|
||||
Neutral: 'badge-orange',
|
||||
Negative: 'badge-red',
|
||||
}
|
||||
|
||||
const reviews = ref<ReviewDto[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const totalCount = ref(0)
|
||||
const totalPages = ref(0)
|
||||
const statusFilter = ref<ApiReviewLlmStatus | 'All'>('All')
|
||||
const reanalyzingId = ref<number | null>(null)
|
||||
const error = ref('')
|
||||
const promptText = ref('')
|
||||
@@ -50,28 +67,36 @@ const promptError = ref('')
|
||||
const promptSuccess = ref('')
|
||||
|
||||
const rows = computed(() =>
|
||||
reviews.value.map((review) => ({
|
||||
id: review.id,
|
||||
lecture: review.lectureTitle || `#${review.lectureId}`,
|
||||
student: review.userName || `#${review.userId}`,
|
||||
rating: ratingLabel[review.rating] ?? review.rating,
|
||||
text: review.text || '—',
|
||||
date: new Date(review.createdAt).toLocaleString('ru-RU'),
|
||||
status: statusMap[review.llmStatus] ?? review.llmStatus,
|
||||
rawStatus: review.llmStatus,
|
||||
sentiment: review.sentiment,
|
||||
quality: review.qualityScore,
|
||||
informative: review.isInformative,
|
||||
tags: review.llmTags ?? [],
|
||||
informativeTags: [
|
||||
review.isInformative === null || review.isInformative === undefined
|
||||
? '—'
|
||||
: review.isInformative
|
||||
? 'Да'
|
||||
: 'Нет',
|
||||
...(review.llmTags ?? []),
|
||||
].join(' / '),
|
||||
})),
|
||||
reviews.value.map((review) => {
|
||||
const analysisReady = review.llmStatus === 'Analyzed'
|
||||
const sentiment = analysisReady ? review.sentiment : null
|
||||
const tags = analysisReady ? normalizeTags(review.llmTags) : []
|
||||
|
||||
return {
|
||||
id: review.id,
|
||||
lecture: review.lectureTitle || `#${review.lectureId}`,
|
||||
student: review.userName || `#${review.userId}`,
|
||||
rating: ratingLabel[review.rating] ?? review.rating,
|
||||
text: review.text || '—',
|
||||
date: new Date(review.createdAt).toLocaleString('ru-RU'),
|
||||
status: statusMap[review.llmStatus] ?? review.llmStatus,
|
||||
rawStatus: review.llmStatus,
|
||||
analysis: review.llmStatus,
|
||||
analysisReady,
|
||||
analysisMessage: getAnalysisMessage(review),
|
||||
sentiment,
|
||||
sentimentLabel: sentiment ? sentimentLabel[sentiment] : '—',
|
||||
sentimentClass: sentiment ? sentimentClass[sentiment] : 'badge-gray',
|
||||
quality: analysisReady ? review.qualityScore : null,
|
||||
qualityLabel: analysisReady ? formatQuality(review.qualityScore) : '—',
|
||||
qualityClass: getQualityClass(analysisReady ? review.qualityScore : null),
|
||||
informative: analysisReady ? review.isInformative : null,
|
||||
informativeLabel: analysisReady ? formatInformative(review.isInformative) : '—',
|
||||
informativeClass: getInformativeClass(analysisReady ? review.isInformative : null),
|
||||
tags,
|
||||
llmRawOutput: analysisReady ? formatRawOutput(review.llmRawOutput) : '',
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const pageStart = computed(() =>
|
||||
@@ -93,9 +118,44 @@ const canSavePrompt = computed(
|
||||
promptText.value !== savedPromptText.value,
|
||||
)
|
||||
|
||||
function normalizeTags(tags: string[] | null | undefined) {
|
||||
return Array.from(new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean)))
|
||||
}
|
||||
|
||||
function formatQuality(value: number | null | undefined) {
|
||||
if (value === null || value === undefined) return '—'
|
||||
return Number(value).toFixed(2)
|
||||
const numeric = Number(value)
|
||||
if (!Number.isFinite(numeric)) return '—'
|
||||
const score = Math.max(0, Math.min(1, numeric))
|
||||
return `${Math.round(score * 100)}%`
|
||||
}
|
||||
|
||||
function getQualityClass(value: number | null | undefined) {
|
||||
if (value === null || value === undefined) return 'badge-gray'
|
||||
if (!Number.isFinite(Number(value))) return 'badge-gray'
|
||||
if (value >= 0.7) return 'badge-green'
|
||||
if (value >= 0.4) return 'badge-orange'
|
||||
return 'badge-red'
|
||||
}
|
||||
|
||||
function formatInformative(value: boolean | null | undefined) {
|
||||
if (value === null || value === undefined) return '—'
|
||||
return value ? 'Информативный' : 'Неинформативный'
|
||||
}
|
||||
|
||||
function getInformativeClass(value: boolean | null | undefined) {
|
||||
if (value === null || value === undefined) return 'badge-gray'
|
||||
return value ? 'badge-green' : 'badge-red'
|
||||
}
|
||||
|
||||
function getAnalysisMessage(review: ReviewDto) {
|
||||
if (review.llmStatus === 'Pending') return 'Ожидает обработки'
|
||||
if (review.llmStatus === 'Rejected') return 'Отзыв отклонён нейронкой'
|
||||
return 'Анализ завершён, данных нет'
|
||||
}
|
||||
|
||||
function formatRawOutput(value: string | null | undefined) {
|
||||
return value?.trim() || ''
|
||||
}
|
||||
|
||||
async function fetchPrompt() {
|
||||
@@ -144,6 +204,7 @@ async function fetchReviews() {
|
||||
const result = await reviewsApi.listPage({
|
||||
Page: page.value,
|
||||
PageSize: pageSize.value,
|
||||
...(statusFilter.value === 'All' ? {} : { LlmStatus: statusFilter.value }),
|
||||
})
|
||||
reviews.value = result.items
|
||||
totalCount.value = result.totalCount
|
||||
@@ -155,6 +216,13 @@ async function fetchReviews() {
|
||||
}
|
||||
}
|
||||
|
||||
async function selectStatusFilter(nextStatus: ApiReviewLlmStatus | 'All') {
|
||||
if (statusFilter.value === nextStatus) return
|
||||
statusFilter.value = nextStatus
|
||||
page.value = 1
|
||||
await fetchReviews()
|
||||
}
|
||||
|
||||
async function goToPage(nextPage: number) {
|
||||
if (nextPage < 1 || (totalPages.value && nextPage > totalPages.value)) return
|
||||
page.value = nextPage
|
||||
@@ -224,6 +292,21 @@ onMounted(() => {
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard>
|
||||
<div class="review-toolbar">
|
||||
<div class="status-filters" aria-label="Фильтр статуса LLM">
|
||||
<button
|
||||
v-for="filter in statusFilters"
|
||||
:key="filter.value"
|
||||
type="button"
|
||||
:class="{ active: statusFilter === filter.value }"
|
||||
:disabled="loading"
|
||||
@click="selectStatusFilter(filter.value)"
|
||||
>
|
||||
{{ filter.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EmptyState v-if="error" title="Не удалось загрузить отзывы" :subtitle="error" />
|
||||
<EmptyState
|
||||
v-else-if="!rows.length && !loading"
|
||||
@@ -247,58 +330,33 @@ onMounted(() => {
|
||||
<template #text="{ value }">
|
||||
<span class="review-text" :title="value">{{ value }}</span>
|
||||
</template>
|
||||
<template #status="{ value, row }">
|
||||
<StatusBadge :status="value" />
|
||||
<span class="raw-status">{{ row.rawStatus }}</span>
|
||||
</template>
|
||||
<template #sentiment="{ value }">
|
||||
<span
|
||||
class="badge"
|
||||
:class="
|
||||
value === 'Positive'
|
||||
? 'badge-green'
|
||||
: value === 'Negative'
|
||||
? 'badge-red'
|
||||
: 'badge-orange'
|
||||
"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
<template #quality="{ value }">
|
||||
<span
|
||||
:class="
|
||||
(value ?? 0) >= 0.7
|
||||
? 'badge badge-green'
|
||||
: (value ?? 0) >= 0.4
|
||||
? 'badge badge-orange'
|
||||
: 'badge badge-red'
|
||||
"
|
||||
>
|
||||
{{ formatQuality(value) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #informativeTags="{ row }">
|
||||
<div class="tags-cell">
|
||||
<span
|
||||
class="badge"
|
||||
:class="
|
||||
row.informative
|
||||
? 'badge-green'
|
||||
: row.informative === false
|
||||
? 'badge-red'
|
||||
: 'badge-gray'
|
||||
"
|
||||
>
|
||||
{{
|
||||
row.informative === null || row.informative === undefined
|
||||
? '—'
|
||||
: row.informative
|
||||
? 'Informative'
|
||||
: 'Not informative'
|
||||
}}
|
||||
</span>
|
||||
<span v-for="tag in row.tags" :key="tag" class="badge badge-blue">{{ tag }}</span>
|
||||
<template #analysis="{ row }">
|
||||
<div class="analysis-cell">
|
||||
<div v-if="!row.analysisReady" class="analysis-head">
|
||||
<StatusBadge :status="row.status" />
|
||||
<span class="raw-status">{{ row.rawStatus }}</span>
|
||||
</div>
|
||||
<div v-if="row.analysisReady" class="analysis-result">
|
||||
<div class="analysis-metrics">
|
||||
<span class="badge" :class="row.qualityClass">
|
||||
Качество {{ row.qualityLabel }}
|
||||
</span>
|
||||
<span class="badge" :class="row.sentimentClass">{{ row.sentimentLabel }}</span>
|
||||
<span class="badge" :class="row.informativeClass">
|
||||
{{ row.informativeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="row.tags.length" class="analysis-tags">
|
||||
<span v-for="tag in row.tags" :key="tag" class="badge badge-blue">{{ tag }}</span>
|
||||
</div>
|
||||
<span v-else class="analysis-note">Теги не найдены</span>
|
||||
<details v-if="row.llmRawOutput" class="raw-output">
|
||||
<summary>Текст ответа нейронки</summary>
|
||||
<pre>{{ row.llmRawOutput }}</pre>
|
||||
</details>
|
||||
<span v-else class="analysis-note">Текст ответа нейронки не сохранён</span>
|
||||
</div>
|
||||
<span v-else class="analysis-note">{{ row.analysisMessage }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
@@ -405,8 +463,7 @@ onMounted(() => {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.raw-status {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
display: inline-flex;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -415,6 +472,34 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.review-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.status-filters {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--color-border-glass);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.status-filters button {
|
||||
background: var(--color-white-a70);
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
.status-filters button.active {
|
||||
background: var(--color-primary-a18);
|
||||
color: var(--color-primary-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
.status-filters button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -429,10 +514,50 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.tags-cell {
|
||||
.analysis-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 260px;
|
||||
}
|
||||
.analysis-head,
|
||||
.analysis-metrics,
|
||||
.analysis-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
min-width: 160px;
|
||||
gap: 6px;
|
||||
}
|
||||
.analysis-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.analysis-note {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.raw-output {
|
||||
max-width: 640px;
|
||||
}
|
||||
.raw-output summary {
|
||||
color: var(--color-primary-dark);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.raw-output pre {
|
||||
margin: 6px 0 0;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
border: 1px solid var(--color-border-glass);
|
||||
border-radius: 8px;
|
||||
background: var(--color-white-a60);
|
||||
padding: 10px;
|
||||
color: var(--color-text);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user