Spaces:
Sleeping
Sleeping
qgallouedec HF Staff
Fix dataset search and the runs list; add model logos; reclaim vertical space
0a5812a | /** | |
| * The contract. | |
| * | |
| * Almost all of this comes straight from the Hub, which is CORS-open. The app has | |
| * two endpoints of its own: `POST /api/train` to start a run, and | |
| * `POST /api/preflight` to measure a dataset — the one read that needs the model's | |
| * real tokenizer and chat template. See `src/hub.ts` for where each shape is | |
| * filled in. | |
| * | |
| * A rule that shapes this file: the user has never trained a model and never | |
| * will read a docs page. Nothing here is measured in tokens, steps or anything | |
| * else they'd have to look up — budgets are minutes and conversation counts, and | |
| * the only structural concept that ever surfaces is "this conversation is too | |
| * long and loses its ending", and only when it actually happens. | |
| */ | |
| /* -------------------------------------------------------------------------- */ | |
| /* Account */ | |
| /* -------------------------------------------------------------------------- */ | |
| export interface HfUser { | |
| /** Hub username, and the namespace the trained model lands in. */ | |
| name: string; | |
| /** | |
| * Whether the account has a way to pay. Jobs are pay-as-you-go on the user's | |
| * own credit, so without this nothing can run — and it's worth knowing that | |
| * before they've picked anything, not after. | |
| */ | |
| canPay: boolean; | |
| } | |
| /* -------------------------------------------------------------------------- */ | |
| /* Recipes */ | |
| /* -------------------------------------------------------------------------- */ | |
| /** One of three curated models. Hyperparameters are never exposed. */ | |
| export interface Model { | |
| /** The Hub id, which is also what `POST /api/train` takes. */ | |
| id: string; | |
| /** "Qwen3 4B" */ | |
| label: string; | |
| /** One short line on what picking this one gets you. */ | |
| note: string; | |
| /** Exactly one model is flagged; it's preselected. */ | |
| recommended: boolean; | |
| } | |
| export type PlanId = "quick" | "balanced" | "full"; | |
| /** | |
| * How much to train on. The only dial the user touches. | |
| * | |
| * Just a label here: the budget itself is a token count that lives in | |
| * PLAN_TOKENS in server/app.py, because turning it into a number of | |
| * conversations needs the dataset measured with a real tokenizer. | |
| */ | |
| export interface Plan { | |
| id: PlanId; | |
| /** "Quick" */ | |
| label: string; | |
| } | |
| /* -------------------------------------------------------------------------- */ | |
| /* Datasets */ | |
| /* -------------------------------------------------------------------------- */ | |
| /** | |
| * Where a dataset came from. Used only for ordering and the example badge — | |
| * there are no tabs, everything lives in one list. | |
| */ | |
| export type DatasetSource = "example" | "yours" | "hub"; | |
| export interface DatasetSummary { | |
| /** "qgallouedec/my-chats" */ | |
| id: string; | |
| private: boolean; | |
| /** Gated datasets need terms accepted before the job can read them. */ | |
| gated: boolean; | |
| rows: number; | |
| description: string; | |
| source: DatasetSource; | |
| /** | |
| * What the dataset's own card claims about having a `messages` column. | |
| * | |
| * "chat" — it declares one, so it can be trained on | |
| * "other" — it declares its columns and `messages` isn't among them, which | |
| * is the one reliable *negative*: it really can't be trained on | |
| * "unknown" — the card says nothing about its columns | |
| * | |
| * "unknown" used to be lumped in with "other" and hidden, which was wrong and | |
| * badly so: of 30 datasets under one real account, 28 declare nothing at all, so | |
| * searching your own name returned "nothing usable matched". A card is a weak | |
| * signal — plenty of perfectly good datasets never write one — and preflight, | |
| * which reads actual rows through the viewer, is the real authority. So unknown | |
| * datasets are listed and selectable, and the Check step decides. | |
| */ | |
| format: "chat" | "other" | "unknown"; | |
| } | |
| /* -------------------------------------------------------------------------- */ | |
| /* Preflight */ | |
| /* -------------------------------------------------------------------------- */ | |
| /** | |
| * `tool` appears in tool-calling datasets: it's the turn carrying what a tool | |
| * returned, which the model reads but is not trained to produce. | |
| */ | |
| export type Role = "system" | "user" | "assistant" | "tool"; | |
| /** | |
| * One call the assistant is being taught to make. `arguments` arrives with values | |
| * already flattened to strings — the server does that so this side only lays them | |
| * out, and so a nested object reads as compact JSON rather than "[object Object]". | |
| */ | |
| export interface ToolCall { | |
| name: string; | |
| arguments: Record<string, string>; | |
| } | |
| export interface Message { | |
| role: Role; | |
| content: string; | |
| /** Set on an assistant turn that calls tools instead of writing a reply. */ | |
| toolCalls?: ToolCall[]; | |
| /** On a `tool` turn, which tool produced it. */ | |
| name?: string; | |
| } | |
| /** | |
| * One conversation, shown to the user as a conversation. | |
| * | |
| * If the length limit falls inside it, `cutFromMessage` marks the first turn | |
| * that gets lost so the preview can fade out at exactly that point. Both cut | |
| * fields are null whenever the conversation fits, which is the common case — | |
| * the idea of a limit never appears unless it actually bites. | |
| */ | |
| export interface PreviewSample { | |
| messages: Message[]; | |
| cutFromMessage: number | null; | |
| /** If the cut lands mid-message, how many characters of it survive. */ | |
| cutAtChar: number | null; | |
| } | |
| export type IssueLevel = "error" | "warning" | "info"; | |
| /** A preflight finding, phrased for someone who has never trained a model. */ | |
| export interface Issue { | |
| level: IssueLevel; | |
| title: string; | |
| detail: string; | |
| } | |
| /** What one plan works out to for this dataset. */ | |
| export interface PlanEstimate { | |
| planId: PlanId; | |
| conversations: number; | |
| estMinutes: number; | |
| /** What it costs on the user's own account, at the flavor's hourly rate. */ | |
| estCostUsd: number; | |
| } | |
| export interface PreflightReport { | |
| datasetId: string; | |
| totalRows: number; | |
| /** Conversations that lose their ending at the shared length limit. */ | |
| truncatedCount: number; | |
| /** Same, as a share of the sample, 0..1. */ | |
| truncatedPct: number; | |
| issues: Issue[]; | |
| samples: PreviewSample[]; | |
| perPlan: Record<PlanId, PlanEstimate>; | |
| recommendedPlan: PlanId; | |
| } | |
| /* -------------------------------------------------------------------------- */ | |
| /* Runs */ | |
| /* -------------------------------------------------------------------------- */ | |
| /** | |
| * The stages a run moves through. The first three come from the Hub's own job | |
| * stage; the rest are the recipe announcing itself on stdout, which the log | |
| * stream carries back. See `[fts] stage` in server/recipe/train.py. | |
| */ | |
| export type RunStatus = | |
| | "queued" | |
| | "starting" | |
| | "training" | |
| | "sampling" | |
| | "pushing" | |
| | "done" | |
| | "error"; | |
| export interface LogLine { | |
| text: string; | |
| level: "info" | "warn" | "error"; | |
| } | |
| /** Base vs tuned on the same prompt, taken from conversations held back from training. */ | |
| export interface SampleComparison { | |
| prompt: string; | |
| before: string; | |
| after: string; | |
| } | |
| export interface Run { | |
| /** The HF Jobs job id — the id every Hub jobs endpoint takes. */ | |
| id: string; | |
| status: RunStatus; | |
| modelId: string; | |
| planId: PlanId; | |
| datasetId: string; | |
| /** Destination repo in the user's namespace. */ | |
| outputRepo: string; | |
| createdAt: string; | |
| /** Overall completion, 0..1. The user never sees a step count. */ | |
| progress: number; | |
| /** Technical detail, available behind a disclosure but never surfaced by default. */ | |
| logs: LogLine[]; | |
| /** Seconds remaining, or null when we can't say. */ | |
| etaSeconds: number | null; | |
| /** Populated once status reaches "done" and the samples file has been read. */ | |
| samples?: SampleComparison[]; | |
| error?: string; | |
| } | |
| /** Anything that hasn't finished — what the header lists. */ | |
| export function isActive(run: Run) { | |
| return run.status !== "done" && run.status !== "error"; | |
| } | |