Add AI photo import via Ollama Vision

Users can upload a photo of a nail polish bottle; a background queue
job sends it to Ollama (llama3.2-vision at 192.168.30.172:11434),
extracts name, number and manufacturer, then creates the nail polish
and adds it to the user's collection automatically.

- ProcessNailPolishImage job (180s timeout, 2 tries)
- NailPolishImport model + migration (status tracking)
- AiSetting model + migration (Ollama URL + model, admin-configurable)
- NailPolishImportController: upload, status page (auto-refresh every 5s)
- Admin\AiSettingController: configure Ollama connection
- Views: drag-drop upload form, live status page, import history
- Navbar: "Foto importieren" link for all users
- Admin dropdown: "KI-Einstellungen"
- neonail-worker.conf: Supervisor config for the queue worker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Housemann
2026-06-13 17:37:26 +02:00
co-authored by Claude Sonnet 4.6
parent 7a24e554b0
commit f715bc4552
15 changed files with 738 additions and 0 deletions
@@ -0,0 +1,115 @@
@extends('layouts.app')
@section('title', 'Import-Status NeoNail DB')
@php $pending = $import->isPending(); @endphp
@if($pending)
<meta http-equiv="refresh" content="5">
@endif
@section('content')
<div class="page-hero mb-4">
<h1><i class="fas fa-magic me-2"></i>KI-Analyse</h1>
<p>
<span class="badge bg-{{ $import->statusColor() }} fs-6">
{{ $import->statusLabel() }}
</span>
</p>
</div>
<div class="row g-4 justify-content-center">
{{-- Bild --}}
<div class="col-md-4">
<div class="card">
<div class="card-body p-3 text-center">
<img src="{{ Storage::url($import->image_path) }}"
alt="Importiertes Bild"
style="max-width:100%; border-radius:12px; max-height:320px; object-fit:contain;">
</div>
</div>
</div>
{{-- Status / Ergebnis --}}
<div class="col-md-6">
<div class="card">
<div class="card-header">
@if($pending)
<i class="fas fa-spinner fa-spin me-2"></i>KI analysiert das Bild…
@elseif($import->status === 'done')
<i class="fas fa-check-circle text-success me-2"></i>Ergebnis
@else
<i class="fas fa-exclamation-circle text-danger me-2"></i>Fehler
@endif
</div>
<div class="card-body p-4">
@if($pending)
<p style="color:#6b7280;">
Ollama verarbeitet das Bild gerade. Diese Seite aktualisiert sich automatisch alle 5 Sekunden.
</p>
<div class="progress" style="height:6px; border-radius:3px;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
style="width:100%; background: linear-gradient(135deg,#7c3aed,#ec4899);"></div>
</div>
@elseif($import->status === 'done')
@php $result = $import->result; @endphp
<table class="table table-borderless mb-0">
<tr>
<th style="width:130px; color:#6b7280; font-size:.85rem;">Name</th>
<td style="font-weight:600;">{{ $result['name'] ?? '—' }}</td>
</tr>
<tr>
<th style="color:#6b7280; font-size:.85rem;">Nummer</th>
<td>{{ $result['number'] ?? '—' }}</td>
</tr>
<tr>
<th style="color:#6b7280; font-size:.85rem;">Hersteller</th>
<td>{{ $result['manufacturer'] ?? '—' }}</td>
</tr>
@if($import->nailPolish)
<tr>
<th style="color:#6b7280; font-size:.85rem;">In Sammlung</th>
<td>
<span class="badge" style="background:linear-gradient(135deg,#7c3aed,#ec4899);">
<i class="fas fa-check me-1"></i>Hinzugefügt
</span>
</td>
</tr>
@endif
</table>
@if($import->nailPolish)
<div class="mt-3">
<a href="{{ route('user-nail-polishes.index') }}" class="btn btn-primary btn-sm">
<i class="fas fa-home me-1"></i>Zur Sammlung
</a>
</div>
@endif
@else
<div class="alert alert-danger mb-3">
<i class="fas fa-exclamation-circle me-2"></i>
{{ $import->error_message ?? 'Unbekannter Fehler' }}
</div>
<a href="{{ route('nail-polish-imports.create') }}" class="btn btn-primary btn-sm">
<i class="fas fa-redo me-1"></i>Nochmal versuchen
</a>
@endif
</div>
</div>
<div class="mt-3">
<a href="{{ route('nail-polish-imports.index') }}" style="color:rgba(255,255,255,.65); font-size:.85rem;">
<i class="fas fa-arrow-left me-1"></i>Alle Importe
</a>
</div>
</div>
</div>
@endsection