Studio: rename GitHub recipe to 'GitHub Scraper' and add Easy mode
Changes the recipe framing from a single-purpose 'Support Bot' pipeline
to a general-purpose scraper that produces {user_request,
grounded_response} training pairs. Aligns with the canonical
github_data_gatherer dataset (11 enrichment tasks mirrored in pr_requests_20
/ issue_requests_20 on the input side and explain_pr / issue_fix_plan /
issue_solution on the output side).
Recipe JSON changes:
- columns[0] renamed normalized_question -> user_request, prompt now
inverts a GitHub thread into a realistic user ask instead of
normalising it.
- columns[1] renamed support_answer -> coauthor_response, emits
{response, followups, cites, task, confidence} and branches on
issue vs PR thread type.
- Notes rewritten to document the 11-task catalog and the canonical
production prompt to paste in for a full dataset backfill.
Frontend: Easy mode for github_repo recipes. The drag-and-drop canvas is
hidden behind an 'Advanced' tab; Easy mode is the default for any recipe
whose seed_source_type is github_repo. The Easy form reuses the existing
GithubRepoSeedForm (promoted to exported), adds a rows input bound to
previewRows, a model field bound to the model_config, and a single Run
button that calls runPreview() directly (no modal). Non-github recipes
see the same Editor / Runs tabs as before.
View mode persists per-recipe-id in localStorage under
recipe-studio:view-mode:<recipeId>.
This commit is contained in:
parent
f2d047ff06
commit
dba29845de
8 changed files with 261 additions and 40 deletions
|
|
@ -41,29 +41,30 @@
|
|||
"columns": [
|
||||
{
|
||||
"column_type": "llm-text",
|
||||
"name": "normalized_question",
|
||||
"name": "user_request",
|
||||
"drop": false,
|
||||
"model_alias": "model_1",
|
||||
"prompt": "Rewrite the following GitHub {{ item_type }} into a single, standalone Unsloth support question a user would ask on Discord. Keep the key technical details (code snippets, tracebacks, versions) verbatim. Keep it 1-5 sentences. Output ONLY the rewritten question.\n\n--- INPUT ---\nRepo: {{ repo }}\nTitle: {{ title }}\nBody:\n{{ body }}\n\nFirst comments:\n{{ comments }}",
|
||||
"system_prompt": "You rewrite real GitHub issue / PR threads into concise support questions. Preserve technical fidelity: do not invent facts.",
|
||||
"prompt": "Read the GitHub {{ item_type }} below and write ONE realistic user request that could have produced it. Imagine a developer asking a GitHub co-author model to either file this {{ item_type }} or draft a PR that resolves it. Use first-person imperative phrasing (\"Open an issue...\", \"Draft a PR that...\", \"Investigate why...\"). Preserve concrete technical details (model names, flags, file paths, tracebacks) that appear in the thread. Keep it 1-3 sentences. Output ONLY the user request, no preamble.\n\n--- INPUT ---\nRepo: {{ repo }}\nType: {{ item_type }}\nTitle: {{ title }}\nBody:\n{{ body }}\n\nFirst comments:\n{{ comments }}",
|
||||
"system_prompt": "You invert real GitHub threads into the user request that would have produced them. Faithful to the thread, no invented facts, no em-dashes, no emojis.",
|
||||
"with_trace": "none"
|
||||
},
|
||||
{
|
||||
"column_type": "llm-structured",
|
||||
"name": "support_answer",
|
||||
"name": "coauthor_response",
|
||||
"drop": false,
|
||||
"model_alias": "model_1",
|
||||
"prompt": "You are writing one training row for an Unsloth support bot. Produce structured JSON grounded in the GitHub thread.\n\nSource issue / PR:\n- Repo: {{ repo }}\n- Title: {{ title }}\n- URL: {{ url }}\n- State: {{ state }}\n- Labels: {{ labels }}\n- Body: {{ body }}\n- First comments: {{ comments }}\n\nNormalized user question:\n{{ normalized_question }}\n\nRules:\n- `answer`: 80-200 words of Markdown grounded in the thread. Cite the source URL at least once inline as `[source: {{ url }}]`.\n- When the thread is procedural (install, upgrade, fix), include a short ```bash or ```python code block if one appears in the thread.\n- Name at least one concrete symbol (function, class, flag, env var, or file path) from the thread when available.\n- Never recommend `rm -rf`, force push, or other destructive commands without a warning.\n- No em-dashes, no emojis, no AI-disclaimer phrases. Only cite URLs / paths that appear in the thread.\n- `diagnosis_questions`: 1-4 follow-ups when the thread is missing info (versions, GPU, traceback). Empty list if the answer is complete.\n- `cites`: URLs / file paths actually used. Always include `{{ url }}`.\n- `confidence`: `high` / `medium` / `low`. Use `low` when ambiguous or out of scope.",
|
||||
"system_prompt": "You write grounded Unsloth support answers. Faithful to the thread, no invented facts, no em-dashes, no emojis, no AI-disclaimer phrases.",
|
||||
"prompt": "You are generating one training row for an Unsloth GitHub co-author model. Given the real GitHub thread and a synthesized user request, produce a grounded structured response.\n\nSource thread:\n- Repo: {{ repo }}\n- Type: {{ item_type }}\n- Title: {{ title }}\n- URL: {{ url }}\n- State: {{ state }}\n- Labels: {{ labels }}\n- Body: {{ body }}\n- First comments: {{ comments }}\n\nUser request:\n{{ user_request }}\n\nRules:\n- `response`: 100-250 words of Markdown grounded in the thread. If the thread is a closed / resolved issue, follow the `issue_fix_plan` shape: brief diagnosis, numbered fix steps, and a short repro. If the thread is a PR, follow the `explain_pr` shape: what changed, why, and which files or symbols were touched. If the thread is open / unresolved, answer honestly and ask for the missing info.\n- Cite the source URL at least once inline as `[source: {{ url }}]`.\n- Name at least one concrete symbol (function, class, flag, env var, or file path) from the thread when available.\n- Include a short ```bash or ```python code block ONLY if the thread itself contains that code or command.\n- Never recommend `rm -rf`, force push, or other destructive commands without an explicit warning.\n- No em-dashes, no emojis, no AI-disclaimer phrases. Only cite URLs / paths that appear in the thread.\n- `followups`: 0-4 follow-up questions when the thread is missing info (versions, GPU, traceback). Empty list if the response is complete.\n- `cites`: URLs / file paths actually used. Always include `{{ url }}`.\n- `task`: one of `explain_pr`, `issue_fix_plan`, `issue_solution`, `discussion_qa`. Pick the closest match.\n- `confidence`: `high` / `medium` / `low`. Use `low` when ambiguous or out of scope.",
|
||||
"system_prompt": "You write grounded GitHub co-author responses for Unsloth. Faithful to the thread, no invented facts, no em-dashes, no emojis, no AI-disclaimer phrases.",
|
||||
"output_format": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answer": {"type": "string", "minLength": 1},
|
||||
"diagnosis_questions": {"type": "array", "items": {"type": "string"}, "maxItems": 4},
|
||||
"response": {"type": "string", "minLength": 1},
|
||||
"followups": {"type": "array", "items": {"type": "string"}, "maxItems": 4},
|
||||
"cites": {"type": "array", "items": {"type": "string"}, "maxItems": 6},
|
||||
"task": {"type": "string", "enum": ["explain_pr", "issue_fix_plan", "issue_solution", "discussion_qa"]},
|
||||
"confidence": {"type": "string", "enum": ["high", "medium", "low"]}
|
||||
},
|
||||
"required": ["answer", "diagnosis_questions", "cites", "confidence"],
|
||||
"required": ["response", "followups", "cites", "task", "confidence"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
|
|
@ -80,8 +81,8 @@
|
|||
{"id": "Local Model", "x": -1056, "y": 520, "width": 400},
|
||||
{"id": "model_1", "x": -544, "y": 488, "width": 400},
|
||||
{"id": "seed", "x": 0, "y": 140, "width": 400},
|
||||
{"id": "normalized_question", "x": 0, "y": 440, "width": 400},
|
||||
{"id": "support_answer", "x": 0, "y": 740, "width": 400},
|
||||
{"id": "user_request", "x": 0, "y": 440, "width": 400},
|
||||
{"id": "coauthor_response", "x": 0, "y": 740, "width": 400},
|
||||
{
|
||||
"id": "note_1",
|
||||
"x": 480,
|
||||
|
|
@ -89,7 +90,7 @@
|
|||
"width": 400,
|
||||
"node_type": "markdown_note",
|
||||
"name": "note_1",
|
||||
"markdown": "### GitHub Support Bot\nReal GitHub data to synthetic Q&A pairs for support-bot fine-tuning.\n\n**Click `Run` below to generate 10 sample rows.** Defaults already point at `unslothai/unsloth` + `unslothai/unsloth-zoo`, use the server's `GH_TOKEN` env var, and run the bundled local model.\n\n**Upgrade to production**\n- Swap `unsloth/gemma-4-E2B-it-GGUF` for a larger model in the model_config node.\n- Replace the demo prompts on `normalized_question` / `support_answer` with the canonical codex pattern below (see Note 3).\n- Raise the seed `limit` from 100 to `0` (All) for a full backfill.\n- Raise `max_parallel_requests` back to 4 once your inference server can handle it.",
|
||||
"markdown": "### GitHub Scraper\nReal GitHub issues and PRs turned into `{user_request, grounded_response}` training pairs. Mirrors two of the eleven canonical enrichment tasks in the `github_data_gatherer` dataset: `pr_requests_20` / `issue_requests_20` for the input side, and `explain_pr` / `issue_fix_plan` / `issue_solution` for the output side.\n\n**Click `Run` below for 10 sample rows.** Defaults point at `unslothai/unsloth` + `unslothai/unsloth-zoo`, use the server's `GH_TOKEN` env var, and run the bundled local model.\n\n**Upgrade to production**\n- Swap `unsloth/gemma-4-E2B-it-GGUF` for a stronger model (`gpt-5.4-mini` with `reasoning_effort=medium` is what the reference dataset uses).\n- Replace the demo prompts with the task-specific prompts from the reference dataset (see Note 3).\n- Raise the seed `limit` from 100 to `0` (All) for a full backfill.\n- Raise `max_parallel_requests` to 4 once the inference server can handle it.",
|
||||
"note_color": "#E0F2FE",
|
||||
"note_opacity": "35"
|
||||
},
|
||||
|
|
@ -100,7 +101,7 @@
|
|||
"width": 400,
|
||||
"node_type": "markdown_note",
|
||||
"name": "note_2",
|
||||
"markdown": "The **normalize** step turns a raw GitHub thread (which often has title/body/comments scattered) into a single clean support question.\n\nTweak the prompt to:\n- always include the traceback\n- drop off-topic chitchat\n- target a specific user persona",
|
||||
"markdown": "The **user_request** column inverts each GitHub thread into a realistic request a developer would give a co-author model (`\"Draft a PR that...\"`, `\"Investigate why...\"`). Same shape as the `pr_requests_20` and `issue_requests_20` enrichments.\n\nTweak the prompt to:\n- always keep the traceback verbatim\n- vary persona (newcomer, maintainer, ops)\n- split one thread into multiple alternative phrasings for data augmentation.",
|
||||
"note_color": "#E0F2FE",
|
||||
"note_opacity": "35"
|
||||
},
|
||||
|
|
@ -111,17 +112,17 @@
|
|||
"width": 400,
|
||||
"node_type": "markdown_note",
|
||||
"name": "note_3",
|
||||
"markdown": "The **answer** block produces `{answer, diagnosis_questions, cites, confidence}`, ready to feed into the grounded RAG answerer.\n\n**Demo default**: 80-200 word answer, one inline `[source: <url>]` cite, `max_parallel_requests=1` so a small local model stays stable.\n\n**Production prompt (paste in):**\n- Require 150-300 word answers with 2+ citations.\n- Enforce named symbols (function / flag / env var / file path).\n- Code fences for every procedural step.\n- Use the resolved-issue / merged-PR template: `What changed / Fix location / Minimum repro`.\n- Reject rows containing em-dashes, emojis, or AI-disclaimer phrases.\n\nSee the Unsloth SupportBot dataset card for the full codex prompt we used to train `Gemma-4-Unsloth-Bot`.",
|
||||
"markdown": "The **coauthor_response** block emits `{response, followups, cites, task, confidence}` and branches on thread type: closed issues become `issue_fix_plan` rows, PRs become `explain_pr` rows, everything else becomes `issue_solution` or `discussion_qa`.\n\n**Demo default**: 100-250 word response, one inline `[source: <url>]` cite, `max_parallel_requests=1` so a small local model stays stable.\n\n**Production prompt (paste in):**\n- Match the reference dataset's per-task prompts (`explain_pr`, `issue_fix_plan`, `pr_review_critique`, `pr_test_plan`, etc.).\n- Require 300 words for explanations, 6-12 bullets for test plans.\n- Enforce named symbols (function / flag / env var / file path).\n- Code fences only for content already in the thread.\n- Reject rows with em-dashes, emojis, or AI-disclaimer phrases.\n\nSee the `github_data_gatherer` dataset card for the full task catalog and the codex prompts used to train the reference GitHub model.",
|
||||
"note_color": "#E0F2FE",
|
||||
"note_opacity": "35"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{"from": "seed", "to": "normalized_question", "type": "canvas", "source_handle": "data-out-bottom", "target_handle": "data-in-top"},
|
||||
{"from": "normalized_question", "to": "support_answer", "type": "canvas", "source_handle": "data-out-bottom", "target_handle": "data-in-top"},
|
||||
{"from": "seed", "to": "user_request", "type": "canvas", "source_handle": "data-out-bottom", "target_handle": "data-in-top"},
|
||||
{"from": "user_request", "to": "coauthor_response", "type": "canvas", "source_handle": "data-out-bottom", "target_handle": "data-in-top"},
|
||||
{"from": "Local Model", "to": "model_1", "type": "semantic", "source_handle": "semantic-out", "target_handle": "semantic-in"},
|
||||
{"from": "model_1", "to": "normalized_question", "type": "semantic", "source_handle": "semantic-out", "target_handle": "data-in"},
|
||||
{"from": "model_1", "to": "support_answer", "type": "semantic", "source_handle": "semantic-out-bottom", "target_handle": "data-in"}
|
||||
{"from": "model_1", "to": "user_request", "type": "semantic", "source_handle": "semantic-out", "target_handle": "data-in"},
|
||||
{"from": "model_1", "to": "coauthor_response", "type": "semantic", "source_handle": "semantic-out-bottom", "target_handle": "data-in"}
|
||||
],
|
||||
"layout_direction": "LR"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,9 +143,9 @@ export const LEARNING_RECIPES: LearningRecipeDef[] = [
|
|||
},
|
||||
{
|
||||
id: "github-support-bot",
|
||||
title: "GitHub Support Bot",
|
||||
title: "GitHub Scraper",
|
||||
description:
|
||||
"Generate realistic Unsloth support Q&A (question + structured answer with citations) for fine-tuning a support assistant.",
|
||||
"Scrape real GitHub issues and PRs and turn each thread into a {user_request, grounded_response} training pair.",
|
||||
loadPayload: () => loadPayloadFromUrl(githubSupportBotUrl),
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -164,9 +164,9 @@ const TEMPLATE_CARDS: TemplateCard[] = [
|
|||
learningRecipeId: "structured-outputs-jinja",
|
||||
},
|
||||
{
|
||||
title: "GitHub Support Bot",
|
||||
title: "GitHub Scraper",
|
||||
description:
|
||||
"Scrape real GitHub issues / PRs / commits (multi-repo) and turn each into a normalized question + structured answer for fine-tuning a support assistant.",
|
||||
"Scrape real GitHub issues and PRs and invert each thread into a {user_request, grounded_response} training pair.",
|
||||
icon: GithubIcon,
|
||||
difficulty: "Intermediate",
|
||||
learningBadges: ["GitHub", "LLM Text", "Structured LLM"],
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ type RecipeStudioHeaderProps = {
|
|||
savedAtLabel: string;
|
||||
workflowName: string;
|
||||
warnings?: GraphWarning[];
|
||||
supportsEasyMode?: boolean;
|
||||
onWorkflowNameChange: (value: string) => void;
|
||||
onViewChange: (view: RecipeStudioView) => void;
|
||||
onSaveRecipe: () => void;
|
||||
|
|
@ -51,6 +52,7 @@ export function RecipeStudioHeader({
|
|||
savedAtLabel,
|
||||
workflowName,
|
||||
warnings = [],
|
||||
supportsEasyMode = false,
|
||||
onWorkflowNameChange,
|
||||
onViewChange,
|
||||
onSaveRecipe,
|
||||
|
|
@ -58,7 +60,7 @@ export function RecipeStudioHeader({
|
|||
const [editingWorkflowName, setEditingWorkflowName] = useState(false);
|
||||
|
||||
function handleViewValueChange(value: string): void {
|
||||
if (value === "editor" || value === "executions") {
|
||||
if (value === "easy" || value === "editor" || value === "executions") {
|
||||
onViewChange(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +132,12 @@ export function RecipeStudioHeader({
|
|||
<div className="justify-self-center">
|
||||
<Tabs value={activeView} onValueChange={handleViewValueChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="editor">Editor</TabsTrigger>
|
||||
{supportsEasyMode && (
|
||||
<TabsTrigger value="easy">Easy</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="editor">
|
||||
{supportsEasyMode ? "Advanced" : "Editor"}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="executions">Runs</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ type SeedDialogProps = {
|
|||
open: boolean;
|
||||
};
|
||||
|
||||
function GithubRepoSeedForm({
|
||||
export function GithubRepoSeedForm({
|
||||
config,
|
||||
onUpdate,
|
||||
}: {
|
||||
|
|
@ -84,12 +84,13 @@ function GithubRepoSeedForm({
|
|||
}): ReactElement {
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const limitStr = (config.github_limit ?? "100").trim();
|
||||
const allMode = limitStr === "" || limitStr === "0";
|
||||
return (
|
||||
<div className="space-y-3 rounded-xl corner-squircle border border-border/60 p-3">
|
||||
<div className="grid gap-1.5">
|
||||
<FieldLabel
|
||||
label="GitHub repositories"
|
||||
hint="One owner/name per line."
|
||||
hint="One owner/name per line. Defaults to the two Unsloth repos."
|
||||
/>
|
||||
<textarea
|
||||
className="nodrag min-h-20 w-full resize-y rounded-md border border-border/60 bg-background px-2 py-1.5 text-xs font-mono"
|
||||
|
|
@ -101,7 +102,7 @@ function GithubRepoSeedForm({
|
|||
<div className="grid gap-1.5">
|
||||
<FieldLabel
|
||||
label="GitHub token"
|
||||
hint="Use public_repo for public repos or repo for private repos. Leave blank to use the server's GH_TOKEN / GITHUB_TOKEN env var."
|
||||
hint="Personal access token with repo scope. Leave blank to use the server's GH_TOKEN / GITHUB_TOKEN env var."
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
|
|
@ -114,17 +115,29 @@ function GithubRepoSeedForm({
|
|||
<div className="grid gap-1.5">
|
||||
<FieldLabel
|
||||
label="Items per repo"
|
||||
hint="How many issues/PRs/commits to fetch from each repo (1-5000)."
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
className="nodrag"
|
||||
min={1}
|
||||
max={5000}
|
||||
value={limitStr}
|
||||
onChange={(e) => onUpdate({ github_limit: e.target.value })}
|
||||
placeholder="100"
|
||||
hint="How many issues/PRs/commits to fetch from each repo. Toggle All to scrape everything."
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
className="nodrag flex-1"
|
||||
min={1}
|
||||
max={100000}
|
||||
disabled={allMode}
|
||||
value={allMode ? "" : limitStr}
|
||||
onChange={(e) => onUpdate({ github_limit: e.target.value })}
|
||||
placeholder={allMode ? "All" : "100"}
|
||||
/>
|
||||
<label className="flex cursor-pointer items-center gap-1.5 text-xs">
|
||||
<Checkbox
|
||||
checked={allMode}
|
||||
onCheckedChange={(v) =>
|
||||
onUpdate({ github_limit: v === true ? "0" : "100" })
|
||||
}
|
||||
/>
|
||||
<span>All</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { GithubIcon, PlayCircleIcon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { type ReactElement, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FieldLabel } from "../dialogs/shared/field-label";
|
||||
import { GithubRepoSeedForm } from "../dialogs/seed/seed-dialog";
|
||||
import type { ModelConfig, NodeConfig, SeedConfig } from "../types";
|
||||
|
||||
type GithubScraperEasyViewProps = {
|
||||
configs: Record<string, NodeConfig>;
|
||||
previewRows: number;
|
||||
setPreviewRows: (rows: number) => void;
|
||||
updateConfig: (id: string, patch: Partial<NodeConfig>) => void;
|
||||
onRun: () => void;
|
||||
runLoading: boolean;
|
||||
runErrors: string[];
|
||||
onSwitchToAdvanced: () => void;
|
||||
};
|
||||
|
||||
export function GithubScraperEasyView({
|
||||
configs,
|
||||
previewRows,
|
||||
setPreviewRows,
|
||||
updateConfig,
|
||||
onRun,
|
||||
runLoading,
|
||||
runErrors,
|
||||
onSwitchToAdvanced,
|
||||
}: GithubScraperEasyViewProps): ReactElement {
|
||||
const seedConfig = useMemo(
|
||||
() =>
|
||||
Object.values(configs).find(
|
||||
(c): c is SeedConfig => c.kind === "seed",
|
||||
) ?? null,
|
||||
[configs],
|
||||
);
|
||||
const modelConfig = useMemo(
|
||||
() =>
|
||||
Object.values(configs).find(
|
||||
(c): c is ModelConfig => c.kind === "model_config",
|
||||
) ?? null,
|
||||
[configs],
|
||||
);
|
||||
|
||||
const handleSeedUpdate = (patch: Partial<SeedConfig>): void => {
|
||||
if (!seedConfig) return;
|
||||
updateConfig(seedConfig.id, patch);
|
||||
};
|
||||
|
||||
const handleModelChange = (value: string): void => {
|
||||
if (!modelConfig) return;
|
||||
updateConfig(modelConfig.id, { model: value });
|
||||
};
|
||||
|
||||
if (!seedConfig) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center px-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This recipe has no seed node. Switch to{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="underline hover:text-foreground"
|
||||
onClick={onSwitchToAdvanced}
|
||||
>
|
||||
Advanced
|
||||
</button>{" "}
|
||||
to configure it.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full w-full max-w-2xl flex-col gap-4 overflow-y-auto px-6 py-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className="flex size-10 shrink-0 items-center justify-center rounded-lg corner-squircle border border-border/70 bg-muted/20"
|
||||
aria-hidden={true}
|
||||
>
|
||||
<HugeiconsIcon icon={GithubIcon} className="size-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-base font-semibold">GitHub Scraper</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Scrape real GitHub issues and PRs and turn each thread into a{" "}
|
||||
<code>{"{user_request, grounded_response}"}</code> training pair.
|
||||
Defaults use the server's <code>GH_TOKEN</code> env var and the
|
||||
bundled local model.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GithubRepoSeedForm config={seedConfig} onUpdate={handleSeedUpdate} />
|
||||
|
||||
<div className="space-y-3 rounded-xl corner-squircle border border-border/60 p-3">
|
||||
<div className="grid gap-1.5">
|
||||
<FieldLabel
|
||||
label="Rows to generate"
|
||||
hint="How many training pairs the LLM should produce from the scraped threads."
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
className="nodrag"
|
||||
min={1}
|
||||
max={10000}
|
||||
value={previewRows}
|
||||
onChange={(event) => {
|
||||
const next = Number.parseInt(event.target.value, 10);
|
||||
setPreviewRows(Number.isFinite(next) && next > 0 ? next : 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<FieldLabel
|
||||
label="Model"
|
||||
hint="Any OpenAI-compatible model id. Local GGUFs (e.g. unsloth/...-GGUF) run on the bundled llama-server."
|
||||
/>
|
||||
<Input
|
||||
className="nodrag font-mono text-xs"
|
||||
value={modelConfig?.model ?? ""}
|
||||
onChange={(event) => handleModelChange(event.target.value)}
|
||||
placeholder="unsloth/gemma-4-E2B-it-GGUF"
|
||||
disabled={!modelConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{runErrors.length > 0 && (
|
||||
<div className="rounded-lg border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
||||
<p className="font-semibold">Cannot run:</p>
|
||||
<ul className="mt-1 list-disc space-y-0.5 pl-4">
|
||||
{runErrors.slice(0, 4).map((err) => (
|
||||
<li key={err}>{err}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-3 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-muted-foreground underline hover:text-foreground"
|
||||
onClick={onSwitchToAdvanced}
|
||||
>
|
||||
Advanced (drag and drop canvas)
|
||||
</button>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
onClick={onRun}
|
||||
disabled={runLoading}
|
||||
>
|
||||
<HugeiconsIcon icon={PlayCircleIcon} className="size-4" />
|
||||
{runLoading ? "Running..." : "Run"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
export type RecipeStudioView = "editor" | "executions";
|
||||
export type RecipeStudioView = "easy" | "editor" | "executions";
|
||||
|
||||
export type RecipeExecutionKind = "preview" | "full";
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import { ConfigDialog } from "./dialogs/config-dialog";
|
|||
import { ImportDialog } from "./dialogs/import-dialog";
|
||||
import { RunDialog } from "./dialogs/preview-dialog";
|
||||
import { ProcessorsDialog } from "./dialogs/processors-dialog";
|
||||
import { GithubScraperEasyView } from "./easy/github-scraper-easy-view";
|
||||
import type {
|
||||
RecipeExecutionRecord,
|
||||
RecipeStudioView,
|
||||
|
|
@ -201,7 +202,28 @@ export function RecipeStudioPage({
|
|||
null,
|
||||
);
|
||||
const flowContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [activeView, setActiveView] = useState<RecipeStudioView>("editor");
|
||||
const supportsEasyMode = initialPayload?.ui?.seed_source_type === "github_repo";
|
||||
const viewModeStorageKey = `recipe-studio:view-mode:${recipeId}`;
|
||||
const [activeView, setActiveViewState] = useState<RecipeStudioView>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = window.localStorage.getItem(viewModeStorageKey);
|
||||
if (stored === "easy" && supportsEasyMode) return "easy";
|
||||
if (stored === "editor" || stored === "executions") return stored;
|
||||
}
|
||||
return supportsEasyMode ? "easy" : "editor";
|
||||
});
|
||||
const setActiveView = useCallback(
|
||||
(next: RecipeStudioView | ((prev: RecipeStudioView) => RecipeStudioView)) => {
|
||||
setActiveViewState((prev) => {
|
||||
const resolved = typeof next === "function" ? next(prev) : next;
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem(viewModeStorageKey, resolved);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
},
|
||||
[viewModeStorageKey],
|
||||
);
|
||||
const [processorsOpen, setProcessorsOpen] = useState(false);
|
||||
const [interactive, setInteractive] = useState(true);
|
||||
const [runtimeIslandMinimized, setRuntimeIslandMinimized] = useState(false);
|
||||
|
|
@ -326,6 +348,7 @@ export function RecipeStudioPage({
|
|||
validateResult,
|
||||
cancelExecution,
|
||||
loadExecutionDatasetPage,
|
||||
runPreview,
|
||||
copyRecipe,
|
||||
importRecipe,
|
||||
} = useRecipeStudioActions({
|
||||
|
|
@ -739,6 +762,7 @@ export function RecipeStudioPage({
|
|||
savedAtLabel={savedAtLabel}
|
||||
workflowName={workflowName}
|
||||
warnings={getGraphWarnings(configs, edges)}
|
||||
supportsEasyMode={supportsEasyMode}
|
||||
onWorkflowNameChange={setWorkflowName}
|
||||
onViewChange={setActiveView}
|
||||
onSaveRecipe={() => {
|
||||
|
|
@ -749,7 +773,20 @@ export function RecipeStudioPage({
|
|||
className="h-[75vh] w-full rounded-t-none"
|
||||
ref={flowContainerRef}
|
||||
>
|
||||
{activeView === "editor" ? (
|
||||
{activeView === "easy" ? (
|
||||
<GithubScraperEasyView
|
||||
configs={configs}
|
||||
previewRows={previewRows}
|
||||
setPreviewRows={setPreviewRows}
|
||||
updateConfig={updateConfig}
|
||||
onRun={() => {
|
||||
void runPreview();
|
||||
}}
|
||||
runLoading={previewLoading || executionLocked}
|
||||
runErrors={runErrors}
|
||||
onSwitchToAdvanced={() => setActiveView("editor")}
|
||||
/>
|
||||
) : activeView === "editor" ? (
|
||||
editorContent
|
||||
) : (
|
||||
<ExecutionsView
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue