chore: move src/llama_stack/ui to src/llama_stack_ui (#4068)

# What does this PR do?
This better separates UI from backend code, which was a point of
confusion often for our beloved AI friends.


## Test Plan
CI
This commit is contained in:
ehhuang 2025-11-04 15:21:49 -08:00 committed by GitHub
parent 5850e3473f
commit 95b0493fae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 20 additions and 20 deletions

View file

@ -0,0 +1,170 @@
"use client";
import {
MessageSquareText,
MessagesSquare,
MoveUpRight,
Database,
MessageCircle,
Settings2,
Compass,
FileText,
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import Image from "next/image";
import { cn } from "@/lib/utils";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarHeader,
} from "@/components/ui/sidebar";
const createItems = [
{
title: "Chat Playground",
url: "/chat-playground",
icon: MessageCircle,
},
];
const manageItems = [
{
title: "Chat Completions",
url: "/logs/chat-completions",
icon: MessageSquareText,
},
{
title: "Responses",
url: "/logs/responses",
icon: MessagesSquare,
},
{
title: "Vector Stores",
url: "/logs/vector-stores",
icon: Database,
},
{
title: "Prompts",
url: "/prompts",
icon: FileText,
},
{
title: "Documentation",
url: "https://llama-stack.readthedocs.io/en/latest/references/api_reference/index.html",
icon: MoveUpRight,
},
];
const optimizeItems: { title: string; url: string; icon: React.ElementType }[] =
[
{
title: "Evaluations",
url: "",
icon: Compass,
},
{
title: "Fine-tuning",
url: "",
icon: Settings2,
},
];
interface SidebarItem {
title: string;
url: string;
icon: React.ElementType;
}
export function AppSidebar() {
const pathname = usePathname();
const renderSidebarItems = (items: SidebarItem[]) => {
return items.map(item => {
const isActive = pathname.startsWith(item.url);
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
className={cn(
"justify-start",
isActive &&
"bg-gray-200 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
)}
>
<Link href={item.url}>
<item.icon
className={cn(
isActive && "text-gray-900 dark:text-gray-100",
"mr-2 h-4 w-4"
)}
/>
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
});
};
return (
<Sidebar>
<SidebarHeader>
<Link href="/" className="flex items-center gap-2 p-2">
<Image
src="/logo.webp"
alt="Llama Stack"
width={32}
height={32}
className="h-8 w-8"
/>
<span className="font-semibold text-lg">Llama Stack</span>
</Link>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Create</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>{renderSidebarItems(createItems)}</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Manage</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>{renderSidebarItems(manageItems)}</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Optimize</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{optimizeItems.map(item => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
disabled
className="justify-start opacity-60 cursor-not-allowed"
>
<item.icon className="mr-2 h-4 w-4" />
<span>{item.title}</span>
<span className="ml-2 text-xs text-gray-500">
(Coming Soon)
</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
);
}

View file

@ -0,0 +1,145 @@
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
export function DetailLoadingView() {
return (
<>
<Skeleton className="h-8 w-3/4 mb-6" /> {/* Title Skeleton */}
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-grow md:w-2/3 space-y-6">
{[...Array(2)].map((_, i) => (
<Card key={`main-skeleton-card-${i}`}>
<CardHeader>
<CardTitle>
<Skeleton className="h-6 w-1/2" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</CardContent>
</Card>
))}
</div>
<div className="md:w-1/3">
<div className="p-4 border rounded-lg shadow-sm bg-white space-y-3">
<Skeleton className="h-6 w-1/3 mb-3" />{" "}
{/* Properties Title Skeleton */}
{[...Array(5)].map((_, i) => (
<div key={`prop-skeleton-${i}`} className="space-y-1">
<Skeleton className="h-4 w-1/4" />
<Skeleton className="h-4 w-1/2" />
</div>
))}
</div>
</div>
</div>
</>
);
}
export function DetailErrorView({
title,
id,
error,
}: {
title: string;
id: string;
error: Error;
}) {
return (
<>
<h1 className="text-2xl font-bold mb-6">{title}</h1>
<p>
Error loading details for ID {id}: {error.message}
</p>
</>
);
}
export function DetailNotFoundView({
title,
id,
}: {
title: string;
id: string;
}) {
return (
<>
<h1 className="text-2xl font-bold mb-6">{title}</h1>
<p>No details found for ID: {id}.</p>
</>
);
}
export interface PropertyItemProps {
label: string;
value: React.ReactNode;
className?: string;
hasBorder?: boolean;
}
export function PropertyItem({
label,
value,
className = "",
hasBorder = false,
}: PropertyItemProps) {
return (
<li
className={`${hasBorder ? "pt-1 mt-1 border-t border-gray-200" : ""} ${className}`}
>
<strong>{label}:</strong>{" "}
{typeof value === "string" || typeof value === "number" ? (
<span className="text-gray-900 dark:text-gray-100 font-medium">
{value}
</span>
) : (
value
)}
</li>
);
}
export interface PropertiesCardProps {
children: React.ReactNode;
}
export function PropertiesCard({ children }: PropertiesCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>Properties</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
{children}
</ul>
</CardContent>
</Card>
);
}
export interface DetailLayoutProps {
title: string;
mainContent: React.ReactNode;
sidebar: React.ReactNode;
}
export function DetailLayout({
title,
mainContent,
sidebar,
}: DetailLayoutProps) {
return (
<>
<h1 className="text-2xl font-bold mb-6">{title}</h1>
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-grow md:w-2/3 space-y-6">{mainContent}</div>
<div className="md:w-1/3">{sidebar}</div>
</div>
</>
);
}

View file

@ -0,0 +1,47 @@
"use client";
import React from "react";
import { usePathname, useParams } from "next/navigation";
import {
PageBreadcrumb,
BreadcrumbSegment,
} from "@/components/layout/page-breadcrumb";
import { truncateText } from "@/lib/truncate-text";
interface LogsLayoutProps {
children: React.ReactNode;
sectionLabel: string;
basePath: string;
}
export default function LogsLayout({
children,
sectionLabel,
basePath,
}: LogsLayoutProps) {
const pathname = usePathname();
const params = useParams();
let segments: BreadcrumbSegment[] = [];
if (pathname === basePath) {
segments = [{ label: sectionLabel }];
}
const idParam = params?.id;
if (idParam && typeof idParam === "string") {
segments = [
{ label: sectionLabel, href: basePath },
{ label: `Details (${truncateText(idParam, 20)})` },
];
}
return (
<div className="container mx-auto p-4 h-[calc(100vh-64px)] flex flex-col">
{segments.length > 0 && (
<PageBreadcrumb segments={segments} className="mb-4" />
)}
<div className="flex-1 min-h-0 flex flex-col">{children}</div>
</div>
);
}

View file

@ -0,0 +1,49 @@
"use client";
import Link from "next/link";
import React from "react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
export interface BreadcrumbSegment {
label: string;
href?: string;
}
interface PageBreadcrumbProps {
segments: BreadcrumbSegment[];
className?: string;
}
export function PageBreadcrumb({ segments, className }: PageBreadcrumbProps) {
if (!segments || segments.length === 0) {
return null;
}
return (
<Breadcrumb className={className}>
<BreadcrumbList>
{segments.map((segment, index) => (
<React.Fragment key={segment.label + index}>
<BreadcrumbItem>
{segment.href ? (
<BreadcrumbLink asChild>
<Link href={segment.href}>{segment.label}</Link>
</BreadcrumbLink>
) : (
<BreadcrumbPage>{segment.label}</BreadcrumbPage>
)}
</BreadcrumbItem>
{index < segments.length - 1 && <BreadcrumbSeparator />}
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
);
}