From 813e2b5bb4eec5351c3f6ef8d587a09b1b769ca6 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Mon, 23 Feb 2026 22:38:06 +0100 Subject: [PATCH] feat: add ShineBorder UI component and learning recipe templates to enhance data recipes page --- .../src/components/ui/shine-border.tsx | 61 +++++++ .../data-recipes/learning-recipes/index.ts | 16 ++ .../data-recipes/pages/data-recipes-page.tsx | 158 ++++++++++++++++-- studio/frontend/src/index.css | 16 +- 4 files changed, 231 insertions(+), 20 deletions(-) create mode 100644 studio/frontend/src/components/ui/shine-border.tsx create mode 100644 studio/frontend/src/features/data-recipes/learning-recipes/index.ts diff --git a/studio/frontend/src/components/ui/shine-border.tsx b/studio/frontend/src/components/ui/shine-border.tsx new file mode 100644 index 0000000000..40b1fe8cf1 --- /dev/null +++ b/studio/frontend/src/components/ui/shine-border.tsx @@ -0,0 +1,61 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +interface ShineBorderProps extends React.HTMLAttributes { + /** + * Width of the border in pixels + * @default 1 + */ + borderWidth?: number + /** + * Duration of the animation in seconds + * @default 14 + */ + duration?: number + /** + * Color of the border, can be a single color or an array of colors + * @default "#000000" + */ + shineColor?: string | string[] +} + +/** + * Shine Border + * + * An animated background border effect component with configurable properties. + */ +export function ShineBorder({ + borderWidth = 1, + duration = 14, + shineColor = "#000000", + className, + style, + ...props +}: ShineBorderProps) { + return ( +
+ ) +} diff --git a/studio/frontend/src/features/data-recipes/learning-recipes/index.ts b/studio/frontend/src/features/data-recipes/learning-recipes/index.ts new file mode 100644 index 0000000000..db827d5d57 --- /dev/null +++ b/studio/frontend/src/features/data-recipes/learning-recipes/index.ts @@ -0,0 +1,16 @@ +export type LearningRecipeDef = { + id: string; + title: string; + description: string; + filePath: string; +}; + +export const LEARNING_RECIPES: LearningRecipeDef[] = [ + { + id: "structured-outputs-jinja", + title: "Structured Outputs + Jinja Expressions", + description: "Minimal schema + Jinja refs + if/else patterns.", + filePath: + "/src/features/data-recipes/learning-recipes/structured-outputs-jinja.json", + }, +]; diff --git a/studio/frontend/src/features/data-recipes/pages/data-recipes-page.tsx b/studio/frontend/src/features/data-recipes/pages/data-recipes-page.tsx index 212e74c212..8bf44cc2ee 100644 --- a/studio/frontend/src/features/data-recipes/pages/data-recipes-page.tsx +++ b/studio/frontend/src/features/data-recipes/pages/data-recipes-page.tsx @@ -7,12 +7,92 @@ import { EmptyMedia, EmptyTitle, } from "@/components/ui/empty"; -import { CookBookIcon, Delete02Icon, PlusSignIcon } from "@hugeicons/core-free-icons"; +import { ShineBorder } from "@/components/ui/shine-border"; +import { + AiChat02Icon, + CodeIcon, + CookBookIcon, + Database02Icon, + Delete02Icon, + FunctionIcon, + Plant01Icon, + PlusSignIcon, + Shield02Icon, +} from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { useNavigate } from "@tanstack/react-router"; import type { ReactElement } from "react"; import { useState } from "react"; -import { createRecipeDraft, deleteRecipe, useRecipes } from "../data/recipes-db"; +import { + createRecipeDraft, + deleteRecipe, + useRecipes, +} from "../data/recipes-db"; + +type TemplateCard = { + title: string; + description: string; + icon: typeof CookBookIcon; + surfaceClassName: string; + shineColor: string[]; +}; + +const TEMPLATE_CARDS: TemplateCard[] = [ + { + title: "Structured Outputs + Jinja Expressions", + description: + "Support ticket triage dataset with structured JSON outputs and Jinja if/else refs.", + icon: FunctionIcon, + surfaceClassName: + "from-cyan-500/15 via-sky-500/5 to-transparent border-cyan-500/30", + shineColor: ["#06b6d4", "#38bdf8", "#22d3ee"], + }, + { + title: "Basic MCP Tool Use", + description: + "Agent workflow starter showing tool-call patterns and grounded tool result usage.", + icon: Shield02Icon, + surfaceClassName: + "from-violet-500/15 via-fuchsia-500/5 to-transparent border-violet-500/30", + shineColor: ["#8b5cf6", "#d946ef", "#a855f7"], + }, + { + title: "Seed Dataset", + description: + "Start from real rows, then expand with synthetic fields while preserving source context.", + icon: Plant01Icon, + surfaceClassName: + "from-emerald-500/15 via-green-500/5 to-transparent border-emerald-500/30", + shineColor: ["#10b981", "#22c55e", "#34d399"], + }, + { + title: "Text to Python", + description: + "Instruction-to-code pairs for training models that generate clean Python implementations.", + icon: CodeIcon, + surfaceClassName: + "from-amber-500/15 via-orange-500/5 to-transparent border-amber-500/30", + shineColor: ["#f59e0b", "#f97316", "#fb923c"], + }, + { + title: "Text to SQL", + description: + "Natural language to SQL pairs, including schema-aware query construction patterns.", + icon: Database02Icon, + surfaceClassName: + "from-blue-500/15 via-indigo-500/5 to-transparent border-blue-500/30", + shineColor: ["#3b82f6", "#6366f1", "#60a5fa"], + }, + { + title: "Multi-Turn Chat", + description: + "Role-based multi-turn conversations for assistant behavior, memory, and response quality.", + icon: AiChat02Icon, + surfaceClassName: + "from-rose-500/15 via-pink-500/5 to-transparent border-rose-500/30", + shineColor: ["#f43f5e", "#ec4899", "#fb7185"], + }, +]; function formatRelativeTime(value: number): string { const now = Date.now(); @@ -63,10 +143,10 @@ export function DataRecipesPage(): ReactElement { } function openRecipe(recipeId: string): void { - void navigate({ + navigate({ to: "/data-recipes/$recipeId", params: { recipeId }, - }); + }).catch(() => undefined); } async function handleDeleteRecipe(recipeId: string): Promise { @@ -78,7 +158,9 @@ export function DataRecipesPage(): ReactElement {
-

Data Recipes

+

+ Data Recipes +

Create and manage local recipe workflows.

@@ -86,7 +168,7 @@ export function DataRecipesPage(): ReactElement { +
+ {TEMPLATE_CARDS.map((template) => ( +
+ +
+
+ +
+
+

+ {template.title} +

+

+ {template.description} +

+
+ + Template + +
+
+ ))} +
) : ( @@ -132,13 +247,18 @@ export function DataRecipesPage(): ReactElement { onClick={() => openRecipe(recipe.id)} >
- +
-

{recipe.name}

+

+ {recipe.name} +

- Last updated {formatRelativeTime(recipe.updatedAt)} | Created{" "} - {formatRelativeTime(recipe.createdAt)} + Last updated {formatRelativeTime(recipe.updatedAt)} | + Created {formatRelativeTime(recipe.createdAt)}

@@ -147,7 +267,9 @@ export function DataRecipesPage(): ReactElement { variant="ghost" size="icon" className="size-8" - onClick={() => void handleDeleteRecipe(recipe.id)} + onClick={() => { + handleDeleteRecipe(recipe.id).catch(() => undefined); + }} aria-label={`Delete ${recipe.name}`} > diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css index df14df684a..d96cd8ab0a 100644 --- a/studio/frontend/src/index.css +++ b/studio/frontend/src/index.css @@ -242,7 +242,19 @@ background-position: calc(100% + var(--shiny-width)) 0; } } -} + --animate-shine: shine var(--duration) infinite linear +; + @keyframes shine { + 0% { + background-position: 0% 0%; + } + 50% { + background-position: 100% 100%; + } + to { + background-position: 0% 0%; + } + }} @layer base { * { @@ -328,4 +340,4 @@ body { @apply bg-background text-foreground; } -} +} \ No newline at end of file