Feat: "Modelle finden" mit gleichwertigem Stöbern-&-Suchen-Modus

Profi-Suche raus aus dem versteckten Akkordeon → eigener Modus-Umschalter
(Empfohlen | Stöbern & Suchen) im "Modelle finden"-Tab.

- Stöbern & Suchen: immer sichtbare Suchleiste, Filter-Chips (Beliebt/Coder/
  Vision/Reasoning/Klein), reiche Treffer (Autor · Downloads · Likes · installiert-
  Badge), aufklappbar → Quant + Rolle + Fit-Ampel + Download (OOM-Gate), plus
  Direkt-Eingabe für exaktes Repo/URL.
- Trending ohne Query: hf.search('') liefert jetzt Top-GGUF nach Downloads;
  Route /api/hf/search?q wird optional.
- Empfohlen = unveränderte Rollen-Sockets (Default). AddModel.tsx in ModelBrowse
  aufgegangen → gelöscht.

Verifiziert: npm run build (tsc strict) clean; Backend-Smoke (empty-q → Top-GGUF);
live gegen die Box (Toggle, Chips, Suche, installiert-Erkennung, Fit-Ampel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-29 19:51:16 +02:00
parent a894a4e952
commit 8108d3fd28
9 changed files with 676 additions and 592 deletions
+31 -31
View File
@@ -1,11 +1,11 @@
import { useState } from "react"
import { Download, Search, Star, Layers, Check, Zap, Eye, Code, Brain, Compass, ChevronDown, ChevronUp } from "lucide-react"
import { Download, Star, Layers, Check, Zap, Eye, Code, Brain, Compass, ChevronDown, ChevronUp } from "lucide-react"
import { api } from "@/lib/api"
import { useModels, useUpdates, useDiscover } from "@/lib/queries"
import { cn } from "@/lib/utils"
import { fmtBytes } from "@/lib/format"
import { FitBadge } from "@/components/models/ModelBadges"
import { AddModel } from "./AddModel"
import { ModelBrowse } from "./ModelBrowse"
// Die 5 kanonischen Rollen (identisch zu sources.py / ModelBadges.ROLES).
const ROLE_METADATA: Record<string, { title: string; desc: string; icon: any }> = {
@@ -44,7 +44,7 @@ export function Discover() {
const error = loadErr ? String(loadErr) : ""
const [installing, setInstalling] = useState<Record<string, string>>({})
const [expandedAlternatives, setExpandedAlternatives] = useState<Record<string, boolean>>({})
const [showExpert, setShowExpert] = useState(false)
const [mode, setMode] = useState<"recommended" | "browse">("recommended")
async function install(repo: string, role: string, quant: string, toolCapable: boolean) {
setInstalling((s) => ({ ...s, [repo]: "Starte..." }))
@@ -59,16 +59,34 @@ export function Discover() {
}
}
if (loading) return <div className="text-xs text-muted-foreground py-12 text-center">Analysiere Hardware und suche passende GGUF-Empfehlungen</div>
if (error || !data)
return (
<div className="rounded-xl border border-red-500/20 bg-red-500/5 p-4 text-xs text-red-400">
Empfehlungsdienst temporär nicht erreichbar ({error}).
</div>
)
return (
<div className="space-y-8">
<div className="space-y-6">
{/* Modus-Umschalter: geführt (Empfohlen) vs. Stöbern & Suchen */}
<div className="flex rounded-xl border border-border/60 bg-card/45 backdrop-blur-md p-1 w-fit">
{([["recommended", "Empfohlen"], ["browse", "Stöbern & Suchen"]] as const).map(([m, label]) => (
<button
key={m}
onClick={() => setMode(m)}
className={cn(
"rounded-lg px-4 py-1.5 text-xs font-semibold tracking-wide transition-all cursor-pointer",
mode === m ? "bg-primary text-primary-foreground shadow-md shadow-primary/10" : "text-muted-foreground hover:text-foreground",
)}
>
{label}
</button>
))}
</div>
{mode === "browse" ? (
<ModelBrowse />
) : loading ? (
<div className="text-xs text-muted-foreground py-12 text-center">Analysiere Hardware und suche passende GGUF-Empfehlungen</div>
) : (error || !data) ? (
<div className="rounded-xl border border-red-500/20 bg-red-500/5 p-4 text-xs text-red-400">
Empfehlungsdienst temporär nicht erreichbar ({error}).
</div>
) : (
<div className="space-y-8">
{/* Informational Header */}
<div className="text-[10px] font-mono text-muted-foreground bg-background/25 border border-border/40 p-4 rounded-xl flex flex-col sm:flex-row sm:items-center justify-between gap-3 shadow-sm">
<div>
@@ -261,26 +279,8 @@ export function Discover() {
})}
</div>
{/* Collapsible Custom Hugging Face Downloader */}
<div className="border border-border/50 bg-card/20 rounded-2xl overflow-hidden shadow-md mt-4">
<button
onClick={() => setShowExpert(!showExpert)}
className="w-full px-5 py-4 flex items-center justify-between text-xs font-bold uppercase tracking-wider text-muted-foreground hover:bg-card/30 transition-colors cursor-pointer"
>
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-primary" />
<span>Eigenes Modell von Hugging Face laden (Erweiterte Ansicht)</span>
</div>
<span className="text-[10px] text-primary hover:underline">
{showExpert ? "Ausblenden ▲" : "Anzeigen ▼"}
</span>
</button>
{showExpert && (
<div className="p-5 border-t border-border/20 bg-card/10">
<AddModel />
</div>
)}
</div>
)}
</div>
)
}