small ui fixes

This commit is contained in:
2025-11-23 12:03:07 +01:00
parent 596731a8a7
commit 012b72527b
15 changed files with 712 additions and 578 deletions
+178 -148
View File
@@ -5,24 +5,32 @@
<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 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 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 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 class="stat-value text-success">
{{ Math.round(stats.validation_rate * 100) }}%
</div>
</div>
</div>
@@ -30,11 +38,11 @@
<i class="mdi mdi-check-circle mr-1"></i>
Auto validation for all responses
</button>
<!-- 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>
@@ -46,39 +54,50 @@
</tr>
</thead>
<tbody>
<tr v-for="response in responsesNeedingValidation" :key="response.id">
<tr
v-for="response in responsesNeedingValidation"
:key="response.id"
>
<td>
<div class="font-bold">{{ response.puzzle_title }}</div>
<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 class="flex justify-between items-center">
<span>Cost: {{ response.cost || '-' }}</span>
<span
v-if="response.ocr_confidence_cost"
<span>Cost: {{ response.cost || "-" }}</span>
<span
v-if="response.ocr_confidence_cost"
class="badge badge-xs"
:class="getConfidenceBadgeClass(response.ocr_confidence_cost)"
:class="
getConfidenceBadgeClass(response.ocr_confidence_cost)
"
>
{{ Math.round(response.ocr_confidence_cost * 100) }}%
</span>
</div>
<div class="flex justify-between items-center">
<span>Cycles: {{ response.cycles || '-' }}</span>
<span
v-if="response.ocr_confidence_cycles"
<span>Cycles: {{ response.cycles || "-" }}</span>
<span
v-if="response.ocr_confidence_cycles"
class="badge badge-xs"
:class="getConfidenceBadgeClass(response.ocr_confidence_cycles)"
:class="
getConfidenceBadgeClass(
response.ocr_confidence_cycles,
)
"
>
{{ Math.round(response.ocr_confidence_cycles * 100) }}%
</span>
</div>
<div class="flex justify-between items-center">
<span>Area: {{ response.area || '-' }}</span>
<span
v-if="response.ocr_confidence_area"
<span>Area: {{ response.area || "-" }}</span>
<span
v-if="response.ocr_confidence_area"
class="badge badge-xs"
:class="getConfidenceBadgeClass(response.ocr_confidence_area)"
:class="
getConfidenceBadgeClass(response.ocr_confidence_area)
"
>
{{ Math.round(response.ocr_confidence_area * 100) }}%
</span>
@@ -91,8 +110,8 @@
</div>
</td>
<td>
<button
@click="openValidationModal(response)"
<button
@click="openValidationModal(response)"
class="btn btn-sm btn-primary"
>
<i class="mdi mdi-check-circle mr-1"></i>
@@ -104,50 +123,53 @@
</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>
<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 w-11/12 max-w-5xl">
<h3 class="font-bold text-lg mb-4">Validate Response</h3>
<div v-for="file in validationModal.response.files">
<img :src="file.file_url">
<div v-for="file in validationModal.response?.files ?? []">
<img :src="file.file_url" />
</div>
<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_title }}</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-4 gap-4">
<div class="grid grid-cols-4 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Puzzle</span>
</label>
<select
<select
v-model="validationModal.data.puzzle"
class="select select-bordered select-sm w-full"
>
<option value="">Select puzzle...</option>
<option
v-for="puzzle in puzzlesStore.puzzles"
:key="puzzle.id"
<option
v-for="puzzle in puzzlesStore.puzzles"
:key="puzzle.id"
:value="puzzle.id"
>
{{ puzzle.title }}
{{ puzzle.title }}
</option>
</select>
</div>
@@ -156,48 +178,53 @@
<label class="label">
<span class="label-text">Cost</span>
</label>
<input
v-model="validationModal.data.validated_cost"
type="text"
<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"
<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"
<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"
<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' }}
<span
v-if="isValidating"
class="loading loading-spinner loading-sm"
></span>
{{ isValidating ? "Validating..." : "Validate" }}
</button>
</div>
</div>
@@ -207,11 +234,11 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { apiService } from '@/services/apiService'
import type { PuzzleResponse } from '@/types'
import {usePuzzlesStore} from '@/stores/puzzles'
const puzzlesStore = usePuzzlesStore()
import { ref, onMounted } from "vue";
import { apiService } from "@/services/apiService";
import type { PuzzleResponse } from "@/types";
import { usePuzzlesStore } from "@/stores/puzzles";
const puzzlesStore = usePuzzlesStore();
// Reactive data
const stats = ref({
@@ -219,160 +246,163 @@ const stats = ref({
total_responses: 0,
needs_validation: 0,
validated_submissions: 0,
validation_rate: 0
})
validation_rate: 0,
});
const responsesNeedingValidation = ref<PuzzleResponse[]>([])
const isLoading = ref(false)
const isValidating = ref(false)
const responsesNeedingValidation = ref<PuzzleResponse[]>([]);
const isLoading = ref(false);
const isValidating = ref(false);
const validationModal = ref({
show: false,
response: null as PuzzleResponse | null,
data: {
puzzle_title: '',
validated_cost: '',
validated_cycles: '',
validated_area: ''
}
})
puzzle: -1,
validated_cost: "",
validated_cycles: "",
validated_area: "",
},
});
// Methods
const loadData = async () => {
try {
isLoading.value = true
isLoading.value = true;
// Load stats (skip if endpoint doesn't exist)
try {
const statsResponse = await apiService.getStats()
const statsResponse = await apiService.getStats();
if (statsResponse.data) {
stats.value = statsResponse.data
stats.value = statsResponse.data;
}
} catch (error) {
console.warn('Stats endpoint not available:', 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
}
validation_rate: 0,
};
}
// Load responses needing validation
const responsesResponse = await apiService.getResponsesNeedingValidation()
const responsesResponse = await apiService.getResponsesNeedingValidation();
if (responsesResponse.data) {
responsesNeedingValidation.value = responsesResponse.data
responsesNeedingValidation.value = responsesResponse.data;
}
} catch (error) {
console.error('Failed to load admin data:', error)
console.error("Failed to load admin data:", error);
} finally {
isLoading.value = false
isLoading.value = false;
}
}
};
const autoValidationResponse = async () => {
for (const response of Array.from(responsesNeedingValidation.value)) {
const {data, error} = await apiService.autoValidateResponses(response.id)
if (!response.id) {
continue;
}
const { data, error } = await apiService.autoValidateResponses(response.id);
if (data && !data.needs_manual_validation) {
// Remove from validation list
responsesNeedingValidation.value = responsesNeedingValidation.value.filter(
r => r.id !== response.id
)
stats.value.needs_validation -= 1
responsesNeedingValidation.value =
responsesNeedingValidation.value.filter((r) => r.id !== response.id);
stats.value.needs_validation -= 1;
} else if (error) {
break
break;
}
}
}
};
const openValidationModal = (response: PuzzleResponse) => {
validationModal.value.response = response
validationModal.value.response = response;
validationModal.value.data = {
puzzle: response.puzzle || '',
validated_cost: response.cost || '',
validated_cycles: response.cycles || '',
validated_area: response.area || ''
}
validationModal.value.show = true
}
puzzle: response.puzzle || -1,
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.show = false;
validationModal.value.response = null;
validationModal.value.data = {
puzzle: '',
validated_cost: '',
validated_cycles: '',
validated_area: ''
}
}
puzzle: -1,
validated_cost: "",
validated_cycles: "",
validated_area: "",
};
};
const submitValidation = async () => {
if (!validationModal.value.response?.id) return
if (!validationModal.value.response?.id) return;
try {
isValidating.value = true
isValidating.value = true;
const response = await apiService.validateResponse(
validationModal.value.response.id,
validationModal.value.data
)
validationModal.value.data,
);
if (response.error) {
alert(`Validation failed: ${response.error}`)
return
alert(`Validation failed: ${response.error}`);
return;
}
// Remove from validation list
responsesNeedingValidation.value = responsesNeedingValidation.value.filter(
r => r.id !== validationModal.value.response?.id
)
(r) => r.id !== validationModal.value.response?.id,
);
// Update stats
stats.value.needs_validation = Math.max(0, stats.value.needs_validation - 1)
closeValidationModal()
stats.value.needs_validation = Math.max(
0,
stats.value.needs_validation - 1,
);
closeValidationModal();
} catch (error) {
console.error('Validation error:', error)
alert('Validation failed')
console.error("Validation error:", error);
alert("Validation failed");
} finally {
isValidating.value = false
isValidating.value = false;
}
}
};
// Lifecycle
onMounted(() => {
loadData()
})
loadData();
});
// Helper functions for confidence display
const getConfidenceBadgeClass = (confidence: number): string => {
if (confidence >= 0.8) return 'badge-success'
if (confidence >= 0.6) return 'badge-warning'
return 'badge-error'
}
if (confidence >= 0.8) return "badge-success";
if (confidence >= 0.6) return "badge-warning";
return "badge-error";
};
const getOverallConfidence = (response: PuzzleResponse): number => {
const confidences = [
response.ocr_confidence_cost,
response.ocr_confidence_cycles,
response.ocr_confidence_area
].filter(conf => conf !== undefined && conf !== null) as number[]
if (confidences.length === 0) return 0
const average = confidences.reduce((sum, conf) => sum + conf, 0) / confidences.length
return Math.round(average * 100)
}
response.ocr_confidence_area,
].filter((conf) => conf !== undefined && conf !== null) as number[];
if (confidences.length === 0) return 0;
const average =
confidences.reduce((sum, conf) => sum + conf, 0) / confidences.length;
return Math.round(average * 100);
};
// Expose refresh method
defineExpose({
refresh: loadData
})
refresh: loadData,
});
</script>