File size: 6,099 Bytes
173ff89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**

 * The five tools, declared once.

 *

 * Each Space linked to the others from its own footer, which meant the

 * same five URLs written out in five files — twenty-five places for a

 * rename to break, and a Space rename is invisible from the linking

 * side: the link keeps returning a page, and the page is a 404.

 *

 * So the registry lives here and is copied into each Space at deploy

 * time, next to `progress.js`. One file to change when a tool moves.

 *

 * The order is the order somebody actually needs them: find a model,

 * see how much context it can serve, learn how to run it, then check

 * what the pack really contains before spending the download.

 */

export const TOOLS = {
  search: {
    space: 'model-search-that-answers',
    title: 'Find a model that fits',
    asks: 'which models fit my card at all',
    // What the visitor has just learned, and why the next tool follows.
    leadsTo: 'context',
    takes: 'query',
  },
  context: {
    space: 'what-context-fits',
    title: 'What context will actually fit?',
    asks: 'how long a conversation this one can serve',
    leadsTo: 'parser',
    takes: 'repo',
  },
  parser: {
    space: 'reasoning-parser-advisor',
    title: 'Does this model need a reasoning parser?',
    asks: 'how to serve it without silently empty answers',
    leadsTo: 'precision',
    takes: 'repo',
  },
  precision: {
    space: 'pack-precision-map',
    title: 'How much of this pack is actually 4-bit?',
    asks: 'what a quantized pack really compressed, and what it kept',
    leadsTo: 'integrity',
    takes: 'repo',
  },
  integrity: {
    space: 'pack-integrity-check',
    title: 'Does this pack keep what it claims?',
    asks: 'whether anything was dropped or left undeclared',
    leadsTo: null,
    takes: 'repo',
  },
};

export const ORDER = ['search', 'context', 'parser', 'precision', 'integrity'];

const BASE = 'https://huggingface.co/spaces/aleada';
const HUB = `${BASE}/start-here`;

/**

 * A link to one tool, carrying the model with it.

 *

 * A funnel where each step starts empty is five separate tools with

 * links between them. `?repo=` is read on load by every page that takes

 * one, so clicking through keeps the model you were looking at.

 */
export function url(key, repo = null) {
  const t = TOOLS[key];
  if (!t) return BASE;
  const q = repo && t.takes === 'repo'
    ? `?repo=${encodeURIComponent(repo)}`
    : repo && t.takes === 'query'
      ? `?q=${encodeURIComponent(repo)}` : '';
  return `${BASE}/${t.space}${q}`;
}

/** The model this page was opened with, if a link carried one over. */
export function repoFromUrl() {
  const p = new URLSearchParams(window.location.search);
  return (p.get('repo') || p.get('q') || '').trim() || null;
}

const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
  ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));

/**

 * The next questions, rendered under a result.

 *

 * The whole chain is rendered, not just the parts that come after: the

 * current step is marked so a visitor who arrived from a link can see

 * where they are, and the steps behind stay reachable because the order

 * is a suggestion and not a gate.

 *

 * Offered only once there is an answer on screen — a next step shown

 * before the current one is finished is clutter, and one that cannot

 * carry the model over is a link to an empty form.

 */
export function nextSteps(fromKey, repo = null) {
  const here = ORDER.indexOf(fromKey);
  const items = ORDER.map((k, i) => {
    const t = TOOLS[k];
    if (i === here) {
      // The step you are on, shown rather than hidden. A list of "other
      // tools" tells you what exists; a chain with your place marked
      // tells you how far along you are, which is the thing somebody
      // arriving from a link actually wants to know.
      return `<li class="ns-here"><span class="ns-mark">✓</span>

        <span class="ns-title">${esc(t.title)}</span>

        <span class="ns-why">you are here</span></li>`;
    }
    const ahead = i > here;
    return `<li class="${ahead ? 'ns-next' : 'ns-back'}">

      <span class="ns-mark">${ahead ? '→' : '·'}</span>

      <a href="${url(k, repo)}" target="_blank" rel="noopener">${esc(t.title)}</a>

      <span class="ns-why">${esc(t.asks)}</span></li>`;
  }).join('');

  return `<nav class="nextsteps" aria-label="The rest of the chain">

      <h4>Your next question${repo ? ` for <code>${esc(repo)}</code>` : ''}</h4>

      <ul>${items}</ul>

      <a class="ns-hub" href="${HUB}${repo ? `?repo=${encodeURIComponent(repo)}` : ''}"

         target="_blank" rel="noopener">All five, and why they are in this order →</a>

    </nav>`;
}

/** Styles for the block above, injected once so each Space need not carry them. */
export function ensureNextStepStyles() {
  if (document.getElementById('ns-style')) return;
  const el = document.createElement('style');
  el.id = 'ns-style';
  el.textContent = `

.nextsteps { margin: 1.6rem 0 0; padding-top: 1rem; border-top: 1px solid var(--line); }

.nextsteps h4 { margin: 0 0 .5rem; font-size: .74rem; font-weight: 600;

                text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }

.nextsteps h4 code { text-transform: none; letter-spacing: 0; }

.nextsteps ul { list-style: none; margin: 0; padding: 0; }

.nextsteps li { margin: .32rem 0; font-size: .89rem; display: flex;

                gap: .5rem; align-items: baseline; }

.ns-mark { font-family: var(--mono); font-size: .8rem; color: var(--muted);

           width: 1rem; flex: 0 0 1rem; }

.ns-here .ns-mark { color: var(--accent); }

.ns-here .ns-title { font-weight: 600; }

.ns-back { opacity: .72; }

.ns-hub { display: inline-block; margin-top: .7rem; font-size: .84rem; }

.nextsteps a { color: var(--accent); text-decoration: none; }

.nextsteps a:hover { text-decoration: underline; }

.ns-why { color: var(--muted); }

.ns-why::before { content: " — "; }`;
  document.head.appendChild(el);
}