Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 4,337 Bytes
5ae37d4 9b65d3b 5ae37d4 9b65d3b 5ae37d4 9b65d3b 5ae37d4 9b65d3b 5ae37d4 9b65d3b 5ae37d4 9b65d3b 5ae37d4 9b65d3b 5ae37d4 9b65d3b 5ae37d4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | "use client";
import { useCallback, useRef, useState } from "react";
import type { Model } from "@/lib/data";
import { elideMiddle } from "./elide";
// The field is a fixed 420px with 14px of padding either side. It does not
// grow with the name: a field that resized would shove the rest of the
// sentence around on every keystroke.
const TEXT_PX = 420 - 28;
const TRACKING = "-1.6px"; // -0.04em at 40px
/**
* The canvas cannot resolve `var(--font-geist-sans)`, so take the font the
* browser actually resolved on the field itself.
*/
function fontOf(el: HTMLInputElement | null): string | null {
if (!el) return null;
const s = getComputedStyle(el);
return s.font || `${s.fontWeight} ${s.fontSize} ${s.fontFamily}`;
}
interface Props {
onResolve: (model: Model | null) => void;
}
type Status =
| { kind: "empty" }
| { kind: "loading" }
| { kind: "error"; message: string }
| { kind: "ok" };
/**
* A free-text field for a Hub repo id. There are millions of models on the
* Hub, so there is no list to pick from — the id is typed, and the sizes come
* back from /api/model.
*/
export default function ModelInput({ onResolve }: Props) {
const [text, setText] = useState("");
const [status, setStatus] = useState<Status>({ kind: "empty" });
const [focused, setFocused] = useState(false);
const [font, setFont] = useState<string | null>(null);
// A callback ref runs at commit, which is where the resolved font can be read.
const attachInput = useCallback((el: HTMLInputElement | null) => {
if (el) setFont(fontOf(el));
}, []);
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
// Only the newest lookup may write state; earlier ones lose the race.
const latest = useRef(0);
async function lookup(id: string, ticket: number) {
try {
const res = await fetch(`/api/model?id=${encodeURIComponent(id)}`);
const body = await res.json();
if (ticket !== latest.current) return;
if (!res.ok) {
setStatus({ kind: "error", message: body.error ?? "Lookup failed." });
onResolve(null);
return;
}
setStatus({ kind: "ok" });
onResolve(body as Model);
} catch {
if (ticket !== latest.current) return;
setStatus({ kind: "error", message: "Could not reach the Hub." });
onResolve(null);
}
}
function handleChange(next: string) {
setText(next);
clearTimeout(timer.current);
const id = next.trim();
const ticket = ++latest.current;
if (!id) {
setStatus({ kind: "empty" });
onResolve(null);
return;
}
setStatus({ kind: "loading" });
timer.current = setTimeout(() => lookup(id, ticket), 400);
}
// While the field is not being edited, a name too long for the box shows its
// middle elided so the tail — the part that names the checkpoint — stays
// visible. The input keeps the real value throughout.
const elided =
focused || !font ? text : elideMiddle(text, TEXT_PX, font, TRACKING);
const isElided = elided !== text;
return (
<span className="relative inline-flex flex-col">
<input
ref={attachInput}
aria-label="Model"
value={text}
onChange={(e) => handleChange(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="Qwen/Qwen3-32B"
spellCheck={false}
autoCapitalize="off"
autoComplete="off"
title={isElided ? text : undefined}
className={`h-[52px] w-[420px] rounded-md bg-paper px-3.5 text-[40px] font-medium leading-[44px] tracking-[-0.04em] ring-raised outline-none placeholder:text-ash ${
isElided ? "text-transparent" : "text-obsidian"
}`}
/>
{isElided && (
<span
aria-hidden
className="pointer-events-none absolute inset-y-0 left-3.5 flex items-center text-[40px] font-medium leading-[44px] tracking-[-0.04em] text-obsidian"
>
{elided}
</span>
)}
<span
className="label absolute top-[56px] left-1 whitespace-nowrap text-graphite"
role={status.kind === "error" ? "alert" : undefined}
>
{status.kind === "loading" && "Looking up"}
{status.kind === "error" && status.message}
</span>
</span>
);
}
|