Files
opus-submitter/opus_submitter/src/components/AdminPanel.vue
T
2025-10-29 03:31:32 +01:00

281 lines
8.5 KiB
Vue

<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">
<i class="mdi mdi-shield-account text-2xl text-warning"></i>
Admin Panel
</h2>
<!-- Stats -->
<div class="stats stats-vertical lg:stats-horizontal shadow mb-6">
<div class="stat">
<div class="stat-title">Total Submissions</div>
<div class="stat-value text-primary">{{ stats.total_submissions }}</div>
</div>
<div class="stat">
<div class="stat-title">Total Responses</div>
<div class="stat-value text-secondary">{{ stats.total_responses }}</div>
</div>
<div class="stat">
<div class="stat-title">Need Validation</div>
<div class="stat-value text-warning">{{ stats.needs_validation }}</div>
</div>
<div class="stat">
<div class="stat-title">Validation Rate</div>
<div class="stat-value text-success">{{ Math.round(stats.validation_rate * 100) }}%</div>
</div>
</div>
<!-- Responses Needing Validation -->
<div v-if="responsesNeedingValidation.length > 0">
<h3 class="text-lg font-bold mb-4">Responses Needing Validation</h3>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Puzzle</th>
<th>OCR Data</th>
<th>Confidence</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="response in responsesNeedingValidation" :key="response.id">
<td>
<div class="font-bold">{{ response.puzzle_name }}</div>
<div class="text-sm opacity-50">ID: {{ response.id }}</div>
</td>
<td>
<div class="text-sm space-y-1">
<div>Cost: {{ response.cost || '-' }}</div>
<div>Cycles: {{ response.cycles || '-' }}</div>
<div>Area: {{ response.area || '-' }}</div>
</div>
</td>
<td>
<div class="badge badge-warning badge-sm">
{{ response.ocr_confidence_score ? Math.round(response.ocr_confidence_score * 100) + '%' : 'Low' }}
</div>
</td>
<td>
<button
@click="openValidationModal(response)"
class="btn btn-sm btn-primary"
>
<i class="mdi mdi-check-circle mr-1"></i>
Validate
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-else class="text-center py-8">
<i class="mdi mdi-check-all text-6xl text-success opacity-50"></i>
<p class="text-lg font-medium mt-2">All responses validated!</p>
<p class="text-sm opacity-70">No responses currently need manual validation.</p>
</div>
</div>
</div>
<!-- Validation Modal -->
<div v-if="validationModal.show" class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Validate Response</h3>
<div v-if="validationModal.response" class="space-y-4">
<div class="alert alert-info">
<i class="mdi mdi-information-outline"></i>
<div>
<div class="font-bold">{{ validationModal.response.puzzle_name }}</div>
<div class="text-sm">Review and correct the OCR data below</div>
</div>
</div>
<div class="grid grid-cols-3 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Cost</span>
</label>
<input
v-model="validationModal.data.validated_cost"
type="text"
class="input input-bordered input-sm"
:placeholder="validationModal.response.cost || 'Enter cost'"
>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Cycles</span>
</label>
<input
v-model="validationModal.data.validated_cycles"
type="text"
class="input input-bordered input-sm"
:placeholder="validationModal.response.cycles || 'Enter cycles'"
>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Area</span>
</label>
<input
v-model="validationModal.data.validated_area"
type="text"
class="input input-bordered input-sm"
:placeholder="validationModal.response.area || 'Enter area'"
>
</div>
</div>
<div class="modal-action">
<button @click="closeValidationModal" class="btn btn-ghost">Cancel</button>
<button
@click="submitValidation"
class="btn btn-primary"
:disabled="isValidating"
>
<span v-if="isValidating" class="loading loading-spinner loading-sm"></span>
{{ isValidating ? 'Validating...' : 'Validate' }}
</button>
</div>
</div>
</div>
<div class="modal-backdrop" @click="closeValidationModal"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { apiService } from '../services/apiService'
import type { PuzzleResponse } from '../types'
// Reactive data
const stats = ref({
total_submissions: 0,
total_responses: 0,
needs_validation: 0,
validated_submissions: 0,
validation_rate: 0
})
const responsesNeedingValidation = ref<PuzzleResponse[]>([])
const isLoading = ref(false)
const isValidating = ref(false)
const validationModal = ref({
show: false,
response: null as PuzzleResponse | null,
data: {
validated_cost: '',
validated_cycles: '',
validated_area: ''
}
})
// Methods
const loadData = async () => {
try {
isLoading.value = true
// Load stats (skip if endpoint doesn't exist)
try {
const statsResponse = await apiService.getStats()
if (statsResponse.data) {
stats.value = statsResponse.data
}
} catch (error) {
console.warn('Stats endpoint not available:', error)
// Set default stats
stats.value = {
total_submissions: 0,
total_responses: 0,
needs_validation: 0,
validated_submissions: 0,
validation_rate: 0
}
}
// Load responses needing validation
const responsesResponse = await apiService.getResponsesNeedingValidation()
if (responsesResponse.data) {
responsesNeedingValidation.value = responsesResponse.data
}
} catch (error) {
console.error('Failed to load admin data:', error)
} finally {
isLoading.value = false
}
}
const openValidationModal = (response: PuzzleResponse) => {
validationModal.value.response = response
validationModal.value.data = {
validated_cost: response.cost || '',
validated_cycles: response.cycles || '',
validated_area: response.area || ''
}
validationModal.value.show = true
}
const closeValidationModal = () => {
validationModal.value.show = false
validationModal.value.response = null
validationModal.value.data = {
validated_cost: '',
validated_cycles: '',
validated_area: ''
}
}
const submitValidation = async () => {
if (!validationModal.value.response?.id) return
try {
isValidating.value = true
const response = await apiService.validateResponse(
validationModal.value.response.id,
validationModal.value.data
)
if (response.error) {
alert(`Validation failed: ${response.error}`)
return
}
// Remove from validation list
responsesNeedingValidation.value = responsesNeedingValidation.value.filter(
r => r.id !== validationModal.value.response?.id
)
// Update stats
stats.value.needs_validation = Math.max(0, stats.value.needs_validation - 1)
closeValidationModal()
} catch (error) {
console.error('Validation error:', error)
alert('Validation failed')
} finally {
isValidating.value = false
}
}
// Lifecycle
onMounted(() => {
loadData()
})
// Expose refresh method
defineExpose({
refresh: loadData
})
</script>