bugfixes
This commit is contained in:
parent
03ff0029f2
commit
a598ac025f
@ -784,9 +784,9 @@ func trainingStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
canTrain := feedbackCount >= minTrainingFeedbackCount
|
canTrain := feedbackCount >= minTrainingFeedbackCount
|
||||||
|
|
||||||
// Pipeline:
|
// Pipeline:
|
||||||
// - YOLO trainiert nur BodyParts, Objects, Clothing und deren Boxen.
|
// - YOLO erkennt Personen/Gender für die Counts.
|
||||||
// - CLIP + Logistic Regression/KNN trainiert die Sexposition.
|
// - Automatisch erkannte Personenboxen werden nicht an das Frontend als sichtbare Boxen zurückgegeben.
|
||||||
// - Personen/Gender werden manuell korrigiert und nicht per YOLO erkannt.
|
// - Manuell gezeichnete Personenboxen werden trotzdem als Trainingsdaten gespeichert.
|
||||||
trainingWriteJSON(w, http.StatusOK, map[string]any{
|
trainingWriteJSON(w, http.StatusOK, map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"feedbackCount": feedbackCount,
|
"feedbackCount": feedbackCount,
|
||||||
@ -800,8 +800,8 @@ func trainingStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"usesSceneKNN": false,
|
"usesSceneKNN": false,
|
||||||
"usesResNet18KNN": false,
|
"usesResNet18KNN": false,
|
||||||
|
|
||||||
"detectsPeople": false,
|
"detectsPeople": true,
|
||||||
"detectsGender": false,
|
"detectsGender": true,
|
||||||
"detectsBodyParts": true,
|
"detectsBodyParts": true,
|
||||||
"detectsObjects": true,
|
"detectsObjects": true,
|
||||||
"detectsClothing": true,
|
"detectsClothing": true,
|
||||||
@ -856,8 +856,8 @@ func trainingStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"pipeline": map[string]any{
|
"pipeline": map[string]any{
|
||||||
"variant": "B",
|
"variant": "B",
|
||||||
|
|
||||||
"peopleSource": "manual",
|
"peopleSource": "yolo_detector",
|
||||||
"genderSource": "manual",
|
"genderSource": "yolo_detector",
|
||||||
"bodyPartsSource": "yolo_detector",
|
"bodyPartsSource": "yolo_detector",
|
||||||
"objectsSource": "yolo_detector",
|
"objectsSource": "yolo_detector",
|
||||||
"clothingSource": "yolo_detector",
|
"clothingSource": "yolo_detector",
|
||||||
@ -1304,9 +1304,9 @@ func trainingApplyScenePosition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPrediction {
|
func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPrediction {
|
||||||
boxes := det.Boxes
|
rawBoxes := det.Boxes
|
||||||
if boxes == nil {
|
if rawBoxes == nil {
|
||||||
boxes = []TrainingBox{}
|
rawBoxes = []TrainingBox{}
|
||||||
}
|
}
|
||||||
|
|
||||||
pred := TrainingPrediction{
|
pred := TrainingPrediction{
|
||||||
@ -1321,7 +1321,7 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred
|
|||||||
BodyPartsPresent: []TrainingScoredLabel{},
|
BodyPartsPresent: []TrainingScoredLabel{},
|
||||||
ObjectsPresent: []TrainingScoredLabel{},
|
ObjectsPresent: []TrainingScoredLabel{},
|
||||||
ClothingPresent: []TrainingScoredLabel{},
|
ClothingPresent: []TrainingScoredLabel{},
|
||||||
Boxes: boxes,
|
Boxes: []TrainingBox{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if pred.Source == "" {
|
if pred.Source == "" {
|
||||||
@ -1338,36 +1338,78 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred
|
|||||||
return pred
|
return pred
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed := map[string]bool{}
|
peopleSet := map[string]bool{}
|
||||||
for _, label := range grouped.BodyParts {
|
for _, label := range grouped.People {
|
||||||
allowed[strings.TrimSpace(label)] = true
|
clean := strings.TrimSpace(label)
|
||||||
}
|
if clean != "" {
|
||||||
for _, label := range grouped.Objects {
|
peopleSet[clean] = true
|
||||||
allowed[strings.TrimSpace(label)] = true
|
|
||||||
}
|
|
||||||
for _, label := range grouped.Clothing {
|
|
||||||
allowed[strings.TrimSpace(label)] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredBoxes := []TrainingBox{}
|
|
||||||
for _, box := range boxes {
|
|
||||||
label := strings.TrimSpace(box.Label)
|
|
||||||
if allowed[label] {
|
|
||||||
filteredBoxes = append(filteredBoxes, box)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boxes = filteredBoxes
|
detectionSet := map[string]bool{}
|
||||||
pred.Boxes = boxes
|
|
||||||
|
|
||||||
pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.BodyParts)
|
for _, label := range grouped.BodyParts {
|
||||||
pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Objects)
|
clean := strings.TrimSpace(label)
|
||||||
pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Clothing)
|
if clean != "" {
|
||||||
|
detectionSet[clean] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pred.UnknownCount = 0
|
for _, label := range grouped.Objects {
|
||||||
pred.PeopleCount = 0
|
clean := strings.TrimSpace(label)
|
||||||
pred.MaleCount = 0
|
if clean != "" {
|
||||||
pred.FemaleCount = 0
|
detectionSet[clean] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range grouped.Clothing {
|
||||||
|
clean := strings.TrimSpace(label)
|
||||||
|
if clean != "" {
|
||||||
|
detectionSet[clean] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleBoxes := []TrainingBox{}
|
||||||
|
|
||||||
|
for _, box := range rawBoxes {
|
||||||
|
label := strings.TrimSpace(box.Label)
|
||||||
|
if label == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
box.Label = label
|
||||||
|
|
||||||
|
// Personen erkennen und zählen.
|
||||||
|
// Personenboxen werden jetzt auch sichtbar zurückgegeben,
|
||||||
|
// damit sie im Frontend gezeichnet, verschoben, gelöscht und trainiert werden können.
|
||||||
|
if peopleSet[label] {
|
||||||
|
switch label {
|
||||||
|
case "person_male", "male_person":
|
||||||
|
pred.MaleCount++
|
||||||
|
|
||||||
|
case "person_female", "female_person":
|
||||||
|
pred.FemaleCount++
|
||||||
|
|
||||||
|
case "person", "person_unknown":
|
||||||
|
pred.UnknownCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleBoxes = append(visibleBoxes, box)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur Bodyparts/Objects/Clothing bleiben als Boxen sichtbar.
|
||||||
|
if detectionSet[label] {
|
||||||
|
visibleBoxes = append(visibleBoxes, box)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pred.PeopleCount = pred.MaleCount + pred.FemaleCount + pred.UnknownCount
|
||||||
|
pred.Boxes = visibleBoxes
|
||||||
|
|
||||||
|
pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.BodyParts)
|
||||||
|
pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Objects)
|
||||||
|
pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Clothing)
|
||||||
|
|
||||||
return pred
|
return pred
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,7 +87,10 @@ func trainingDetectorLabels() ([]string, error) {
|
|||||||
|
|
||||||
labels := []string{}
|
labels := []string{}
|
||||||
|
|
||||||
// Bestehende Reihenfolge beibehalten, damit alte Class-IDs stabil bleiben.
|
// Wichtig:
|
||||||
|
// People zuerst oder zuletzt ist egal, aber die Reihenfolge bestimmt YOLO-Class-IDs.
|
||||||
|
// Wenn du schon ein bestehendes Detector-Modell hast, musst du danach neu trainieren.
|
||||||
|
labels = append(labels, grouped.People...)
|
||||||
labels = append(labels, grouped.BodyParts...)
|
labels = append(labels, grouped.BodyParts...)
|
||||||
labels = append(labels, grouped.Objects...)
|
labels = append(labels, grouped.Objects...)
|
||||||
labels = append(labels, grouped.Clothing...)
|
labels = append(labels, grouped.Clothing...)
|
||||||
|
|||||||
@ -124,6 +124,88 @@ function percent(v: number) {
|
|||||||
return `${Math.round(v * 100)}%`
|
return `${Math.round(v * 100)}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scoreLevel(score?: number | null): 'none' | 'low' | 'mid' | 'high' {
|
||||||
|
const n = Number(score)
|
||||||
|
|
||||||
|
if (!Number.isFinite(n)) return 'none'
|
||||||
|
if (n < 0.5) return 'low'
|
||||||
|
if (n < 0.75) return 'mid'
|
||||||
|
return 'high'
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreBorderClass(score?: number | null, opts?: { draft?: boolean }) {
|
||||||
|
if (opts?.draft) return 'border-amber-400'
|
||||||
|
|
||||||
|
switch (scoreLevel(score)) {
|
||||||
|
case 'low':
|
||||||
|
return 'border-red-500'
|
||||||
|
case 'mid':
|
||||||
|
return 'border-yellow-400'
|
||||||
|
case 'high':
|
||||||
|
return 'border-emerald-400'
|
||||||
|
default:
|
||||||
|
return 'border-gray-300'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreBgClass(score?: number | null, opts?: { draft?: boolean }) {
|
||||||
|
if (opts?.draft) return 'bg-amber-400'
|
||||||
|
|
||||||
|
switch (scoreLevel(score)) {
|
||||||
|
case 'low':
|
||||||
|
return 'bg-red-500'
|
||||||
|
case 'mid':
|
||||||
|
return 'bg-yellow-400'
|
||||||
|
case 'high':
|
||||||
|
return 'bg-emerald-400'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-300'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreHoverClass(score?: number | null, opts?: { draft?: boolean }) {
|
||||||
|
if (opts?.draft) return ''
|
||||||
|
|
||||||
|
switch (scoreLevel(score)) {
|
||||||
|
case 'low':
|
||||||
|
return 'hover:bg-red-400'
|
||||||
|
case 'mid':
|
||||||
|
return 'hover:bg-yellow-300'
|
||||||
|
case 'high':
|
||||||
|
return 'hover:bg-emerald-300'
|
||||||
|
default:
|
||||||
|
return 'hover:bg-gray-200'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreRingClass(score?: number | null, opts?: { draft?: boolean }) {
|
||||||
|
if (opts?.draft) return 'ring-amber-500'
|
||||||
|
|
||||||
|
switch (scoreLevel(score)) {
|
||||||
|
case 'low':
|
||||||
|
return 'ring-red-500'
|
||||||
|
case 'mid':
|
||||||
|
return 'ring-yellow-400'
|
||||||
|
case 'high':
|
||||||
|
return 'ring-emerald-500'
|
||||||
|
default:
|
||||||
|
return 'ring-gray-400'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreDetectionPillClass(score?: number | null) {
|
||||||
|
switch (scoreLevel(score)) {
|
||||||
|
case 'low':
|
||||||
|
return 'bg-red-50 text-red-800 ring-red-200 dark:bg-red-500/15 dark:text-red-100 dark:ring-red-400/30'
|
||||||
|
case 'mid':
|
||||||
|
return 'bg-yellow-50 text-yellow-900 ring-yellow-200 dark:bg-yellow-500/15 dark:text-yellow-100 dark:ring-yellow-400/30'
|
||||||
|
case 'high':
|
||||||
|
return 'bg-emerald-50 text-emerald-800 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-100 dark:ring-emerald-400/30'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-50 text-gray-700 ring-gray-200 dark:bg-white/5 dark:text-gray-200 dark:ring-white/10'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeMovedBox(box: TrainingBox): TrainingBox {
|
function normalizeMovedBox(box: TrainingBox): TrainingBox {
|
||||||
const w = clamp01(box.w)
|
const w = clamp01(box.w)
|
||||||
const h = clamp01(box.h)
|
const h = clamp01(box.h)
|
||||||
@ -137,22 +219,6 @@ function normalizeMovedBox(box: TrainingBox): TrainingBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoredLabelsText(items?: ScoredLabel[] | null) {
|
|
||||||
const list = Array.isArray(items) ? items : []
|
|
||||||
|
|
||||||
if (list.length === 0) return '—'
|
|
||||||
|
|
||||||
return list
|
|
||||||
.map((x) => {
|
|
||||||
const label = String(x?.label ?? '').trim()
|
|
||||||
if (!label) return null
|
|
||||||
|
|
||||||
return `${label} (${percent(Number(x?.score ?? 0))})`
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
function clampPercent(v: number) {
|
function clampPercent(v: number) {
|
||||||
if (!Number.isFinite(v)) return 0
|
if (!Number.isFinite(v)) return 0
|
||||||
return Math.max(0, Math.min(100, v))
|
return Math.max(0, Math.min(100, v))
|
||||||
@ -209,19 +275,6 @@ function uniqStrings(values: string[]) {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPersonBoxLabel(label?: string) {
|
|
||||||
const normalized = String(label || '').trim().toLowerCase()
|
|
||||||
|
|
||||||
return (
|
|
||||||
normalized === 'person' ||
|
|
||||||
normalized === 'person_male' ||
|
|
||||||
normalized === 'person_female' ||
|
|
||||||
normalized === 'person_unknown' ||
|
|
||||||
normalized === 'male_person' ||
|
|
||||||
normalized === 'female_person'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function predictionToCorrection(sample: TrainingSample | null): CorrectionState {
|
function predictionToCorrection(sample: TrainingSample | null): CorrectionState {
|
||||||
const p = sample?.prediction
|
const p = sample?.prediction
|
||||||
|
|
||||||
@ -246,8 +299,7 @@ function predictionToCorrection(sample: TrainingSample | null): CorrectionState
|
|||||||
w: clamp01(Number(box.w)),
|
w: clamp01(Number(box.w)),
|
||||||
h: clamp01(Number(box.h)),
|
h: clamp01(Number(box.h)),
|
||||||
}))
|
}))
|
||||||
.filter((box) => box.label && box.w > 0 && box.h > 0)
|
.filter((box) => box.label && box.w > 0 && box.h > 0),
|
||||||
.filter((box) => !isPersonBoxLabel(box.label)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,6 +584,52 @@ function LabelToggleGrid(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ScoredLabelChips(props: {
|
||||||
|
items?: ScoredLabel[] | null
|
||||||
|
}) {
|
||||||
|
const list = Array.isArray(props.items) ? props.items : []
|
||||||
|
|
||||||
|
if (list.length === 0) {
|
||||||
|
return <span className="text-gray-500 dark:text-gray-400">—</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
|
{list.map((entry, index) => {
|
||||||
|
const label = String(entry?.label ?? '').trim()
|
||||||
|
if (!label) return null
|
||||||
|
|
||||||
|
const score = Number(entry?.score ?? NaN)
|
||||||
|
const item = getSegmentLabelItem(label)
|
||||||
|
const Icon = item.icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={`${label}-${index}`}
|
||||||
|
title={`${label} ${Number.isFinite(score) ? percent(score) : ''}`}
|
||||||
|
className={[
|
||||||
|
'inline-flex max-w-full items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-semibold ring-1',
|
||||||
|
scoreDetectionPillClass(score),
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||||
|
|
||||||
|
<span className="max-w-[92px] truncate">
|
||||||
|
{item.text}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{Number.isFinite(score) ? (
|
||||||
|
<span className="shrink-0 opacity-80">
|
||||||
|
{percent(score)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function DetectorBoxLabelSelect(props: {
|
function DetectorBoxLabelSelect(props: {
|
||||||
values: string[]
|
values: string[]
|
||||||
value: string
|
value: string
|
||||||
@ -1388,20 +1486,48 @@ export default function TrainingTab() {
|
|||||||
<div>Weiblich: {sample?.prediction.femaleCount ?? '—'}</div>
|
<div>Weiblich: {sample?.prediction.femaleCount ?? '—'}</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Position: {sample?.prediction.sexPosition ?? '—'}{' '}
|
<div className="font-medium text-gray-700 dark:text-gray-200">
|
||||||
{sample ? `(${percent(sample.prediction.sexPositionScore)})` : ''}
|
Position
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sample ? (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
'mt-1 inline-flex max-w-full items-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold ring-1',
|
||||||
|
scoreDetectionPillClass(sample.prediction.sexPositionScore),
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{sample.prediction.sexPosition || '—'}
|
||||||
|
</span>
|
||||||
|
<span className="ml-1 shrink-0 opacity-80">
|
||||||
|
{percent(sample.prediction.sexPositionScore)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>—</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Körperteile: {scoredLabelsText(sample?.prediction.bodyPartsPresent)}
|
<div className="font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
Körperteile
|
||||||
|
</div>
|
||||||
|
<ScoredLabelChips items={sample?.prediction.bodyPartsPresent} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Gegenstände: {scoredLabelsText(sample?.prediction.objectsPresent)}
|
<div className="font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
Gegenstände
|
||||||
|
</div>
|
||||||
|
<ScoredLabelChips items={sample?.prediction.objectsPresent} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Kleidung: {scoredLabelsText(sample?.prediction.clothingPresent)}
|
<div className="font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
Kleidung
|
||||||
|
</div>
|
||||||
|
<ScoredLabelChips items={sample?.prediction.clothingPresent} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1441,7 +1567,10 @@ export default function TrainingTab() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${box.label}-${index}`}
|
key={`${box.label}-${index}`}
|
||||||
className="flex items-center justify-between gap-2 rounded-md bg-white px-2 py-1 text-[11px] ring-1 ring-gray-200 dark:bg-gray-950 dark:ring-white/10"
|
className={[
|
||||||
|
'flex items-center justify-between gap-2 rounded-md px-2 py-1 text-[11px] ring-1',
|
||||||
|
scoreDetectionPillClass(box.score),
|
||||||
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 items-center justify-between gap-2 text-gray-700 dark:text-gray-200">
|
<div className="flex min-w-0 flex-1 items-center justify-between gap-2 text-gray-700 dark:text-gray-200">
|
||||||
<span className="flex min-w-0 items-center gap-1.5">
|
<span className="flex min-w-0 items-center gap-1.5">
|
||||||
@ -1583,9 +1712,7 @@ export default function TrainingTab() {
|
|||||||
key={`${box.label}-${index}-${isDraft ? 'draft' : 'box'}`}
|
key={`${box.label}-${index}-${isDraft ? 'draft' : 'box'}`}
|
||||||
className={[
|
className={[
|
||||||
'pointer-events-none absolute rounded border-2 shadow-[0_0_0_1px_rgba(0,0,0,0.75)]',
|
'pointer-events-none absolute rounded border-2 shadow-[0_0_0_1px_rgba(0,0,0,0.75)]',
|
||||||
isDraft
|
scoreBorderClass(box.score, { draft: isDraft }),
|
||||||
? 'border-amber-400'
|
|
||||||
: 'border-emerald-400',
|
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
style={{
|
style={{
|
||||||
left: `${left}%`,
|
left: `${left}%`,
|
||||||
@ -1600,9 +1727,7 @@ export default function TrainingTab() {
|
|||||||
data-box-control="true"
|
data-box-control="true"
|
||||||
className={[
|
className={[
|
||||||
'pointer-events-auto absolute left-0 top-0 flex h-5 max-w-[min(180px,calc(100vw-24px))] -translate-y-full touch-none items-stretch overflow-hidden rounded-t-md text-[10px] font-semibold leading-none text-black shadow',
|
'pointer-events-auto absolute left-0 top-0 flex h-5 max-w-[min(180px,calc(100vw-24px))] -translate-y-full touch-none items-stretch overflow-hidden rounded-t-md text-[10px] font-semibold leading-none text-black shadow',
|
||||||
isDraft
|
scoreBgClass(box.score, { draft: isDraft }),
|
||||||
? 'bg-amber-400'
|
|
||||||
: 'bg-emerald-400',
|
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
title={isDraft ? box.label : `${item.text} verschieben`}
|
title={isDraft ? box.label : `${item.text} verschieben`}
|
||||||
>
|
>
|
||||||
@ -1612,7 +1737,7 @@ export default function TrainingTab() {
|
|||||||
'flex min-w-0 touch-none items-center gap-1 px-1.5 text-left',
|
'flex min-w-0 touch-none items-center gap-1 px-1.5 text-left',
|
||||||
isDraft
|
isDraft
|
||||||
? 'cursor-default'
|
? 'cursor-default'
|
||||||
: 'cursor-move hover:bg-emerald-300',
|
: ['cursor-move', scoreHoverClass(box.score)].join(' '),
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
disabled={Boolean(isDraft) || uiLocked}
|
disabled={Boolean(isDraft) || uiLocked}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
@ -1741,7 +1866,12 @@ export default function TrainingTab() {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="block h-2 w-2 rounded-full bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.75)] ring-2 ring-emerald-500 sm:h-2 sm:w-2" />
|
<span
|
||||||
|
className={[
|
||||||
|
'block h-2 w-2 rounded-full bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.75)] ring-2 sm:h-2 sm:w-2',
|
||||||
|
scoreRingClass(box.score),
|
||||||
|
].join(' ')}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@ -1848,7 +1978,10 @@ export default function TrainingTab() {
|
|||||||
|
|
||||||
{hasUsableBox ? (
|
{hasUsableBox ? (
|
||||||
<div
|
<div
|
||||||
className="absolute rounded border-2 border-amber-400 bg-amber-400/10 shadow-[0_0_0_1px_rgba(0,0,0,0.85)]"
|
className={[
|
||||||
|
'absolute rounded border-2 bg-black/0 shadow-[0_0_0_1px_rgba(0,0,0,0.85)]',
|
||||||
|
scoreBorderClass(activeBox.score, { draft: Boolean(drawingBox) }),
|
||||||
|
].join(' ')}
|
||||||
style={{
|
style={{
|
||||||
left: boxLeft,
|
left: boxLeft,
|
||||||
top: boxTop,
|
top: boxTop,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user