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>
  );
}