mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-07-15 09:36:10 +00:00
feat(auth,ui): support github sign-in in the UI (#2545)
# What does this PR do? Uses NextAuth to add github sign in support. ## Test Plan Start server with auth configured as in https://github.com/meta-llama/llama-stack/pull/2509 https://github.com/user-attachments/assets/61ff7442-f601-4b39-8686-5d0afb3b45ac
This commit is contained in:
parent
c8bac888af
commit
daf660c4ea
23 changed files with 577 additions and 81 deletions
6
llama_stack/ui/app/api/auth/[...nextauth]/route.ts
Normal file
6
llama_stack/ui/app/api/auth/[...nextauth]/route.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import NextAuth from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
|
||||||
|
const handler = NextAuth(authOptions);
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST };
|
118
llama_stack/ui/app/auth/signin/page.tsx
Normal file
118
llama_stack/ui/app/auth/signin/page.tsx
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { signIn, signOut, useSession } from "next-auth/react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Copy, Check, Home, Github } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export default function SignInPage() {
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleCopyToken = async () => {
|
||||||
|
if (session?.accessToken) {
|
||||||
|
await navigator.clipboard.writeText(session.accessToken);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status === "loading") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<Card className="w-[400px]">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Authentication</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{session
|
||||||
|
? "You are successfully authenticated!"
|
||||||
|
: "Sign in with GitHub to use your access token as an API key"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{!session ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
console.log("Signing in with GitHub...");
|
||||||
|
signIn("github", { callbackUrl: "/auth/signin" }).catch(
|
||||||
|
(error) => {
|
||||||
|
console.error("Sign in error:", error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<Github className="mr-2 h-4 w-4" />
|
||||||
|
Sign in with GitHub
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Signed in as {session.user?.email}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{session.accessToken && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
GitHub Access Token:
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<code className="flex-1 p-2 bg-muted rounded text-xs break-all">
|
||||||
|
{session.accessToken}
|
||||||
|
</code>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCopyToken}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
This GitHub token will be used as your API key for
|
||||||
|
authenticated Llama Stack requests.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={() => router.push("/")} className="flex-1">
|
||||||
|
<Home className="mr-2 h-4 w-4" />
|
||||||
|
Go to Dashboard
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => signOut()}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { ThemeProvider } from "@/components/ui/theme-provider";
|
import { ThemeProvider } from "@/components/ui/theme-provider";
|
||||||
|
import { SessionProvider } from "@/components/providers/session-provider";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { ModeToggle } from "@/components/ui/mode-toggle";
|
import { ModeToggle } from "@/components/ui/mode-toggle";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
@ -21,11 +22,13 @@ export const metadata: Metadata = {
|
||||||
|
|
||||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
import { AppSidebar } from "@/components/layout/app-sidebar";
|
||||||
|
import { SignInButton } from "@/components/ui/sign-in-button";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable} font-sans`}>
|
<body className={`${geistSans.variable} ${geistMono.variable} font-sans`}>
|
||||||
|
<SessionProvider>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
|
@ -41,7 +44,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 text-center"></div>
|
<div className="flex-1 text-center"></div>
|
||||||
<div className="flex-none">
|
<div className="flex-none flex items-center gap-2">
|
||||||
|
<SignInButton />
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,6 +53,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
</main>
|
</main>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</SessionProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,11 +4,12 @@ import { useEffect, useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { ChatCompletion } from "@/lib/types";
|
import { ChatCompletion } from "@/lib/types";
|
||||||
import { ChatCompletionDetailView } from "@/components/chat-completions/chat-completion-detail";
|
import { ChatCompletionDetailView } from "@/components/chat-completions/chat-completion-detail";
|
||||||
import { client } from "@/lib/client";
|
import { useAuthClient } from "@/hooks/use-auth-client";
|
||||||
|
|
||||||
export default function ChatCompletionDetailPage() {
|
export default function ChatCompletionDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id as string;
|
const id = params.id as string;
|
||||||
|
const client = useAuthClient();
|
||||||
|
|
||||||
const [completionDetail, setCompletionDetail] =
|
const [completionDetail, setCompletionDetail] =
|
||||||
useState<ChatCompletion | null>(null);
|
useState<ChatCompletion | null>(null);
|
||||||
|
@ -45,7 +46,7 @@ export default function ChatCompletionDetailPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchCompletionDetail();
|
fetchCompletionDetail();
|
||||||
}, [id]);
|
}, [id, client]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatCompletionDetailView
|
<ChatCompletionDetailView
|
||||||
|
|
|
@ -5,11 +5,12 @@ import { useParams } from "next/navigation";
|
||||||
import type { ResponseObject } from "llama-stack-client/resources/responses/responses";
|
import type { ResponseObject } from "llama-stack-client/resources/responses/responses";
|
||||||
import { OpenAIResponse, InputItemListResponse } from "@/lib/types";
|
import { OpenAIResponse, InputItemListResponse } from "@/lib/types";
|
||||||
import { ResponseDetailView } from "@/components/responses/responses-detail";
|
import { ResponseDetailView } from "@/components/responses/responses-detail";
|
||||||
import { client } from "@/lib/client";
|
import { useAuthClient } from "@/hooks/use-auth-client";
|
||||||
|
|
||||||
export default function ResponseDetailPage() {
|
export default function ResponseDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id as string;
|
const id = params.id as string;
|
||||||
|
const client = useAuthClient();
|
||||||
|
|
||||||
const [responseDetail, setResponseDetail] = useState<OpenAIResponse | null>(
|
const [responseDetail, setResponseDetail] = useState<OpenAIResponse | null>(
|
||||||
null,
|
null,
|
||||||
|
@ -109,7 +110,7 @@ export default function ResponseDetailPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchResponseDetail();
|
fetchResponseDetail();
|
||||||
}, [id]);
|
}, [id, client]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponseDetailView
|
<ResponseDetailView
|
||||||
|
|
|
@ -12,24 +12,34 @@ jest.mock("next/navigation", () => ({
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock next-auth
|
||||||
|
jest.mock("next-auth/react", () => ({
|
||||||
|
useSession: () => ({
|
||||||
|
status: "authenticated",
|
||||||
|
data: { accessToken: "mock-token" },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
// Mock helper functions
|
// Mock helper functions
|
||||||
jest.mock("@/lib/truncate-text");
|
jest.mock("@/lib/truncate-text");
|
||||||
jest.mock("@/lib/format-message-content");
|
jest.mock("@/lib/format-message-content");
|
||||||
|
|
||||||
// Mock the client
|
// Mock the auth client hook
|
||||||
jest.mock("@/lib/client", () => ({
|
const mockClient = {
|
||||||
client: {
|
|
||||||
chat: {
|
chat: {
|
||||||
completions: {
|
completions: {
|
||||||
list: jest.fn(),
|
list: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
|
||||||
|
jest.mock("@/hooks/use-auth-client", () => ({
|
||||||
|
useAuthClient: () => mockClient,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the usePagination hook
|
// Mock the usePagination hook
|
||||||
const mockLoadMore = jest.fn();
|
const mockLoadMore = jest.fn();
|
||||||
jest.mock("@/hooks/usePagination", () => ({
|
jest.mock("@/hooks/use-pagination", () => ({
|
||||||
usePagination: jest.fn(() => ({
|
usePagination: jest.fn(() => ({
|
||||||
data: [],
|
data: [],
|
||||||
status: "idle",
|
status: "idle",
|
||||||
|
@ -47,7 +57,7 @@ import {
|
||||||
} from "@/lib/format-message-content";
|
} from "@/lib/format-message-content";
|
||||||
|
|
||||||
// Import the mocked hook
|
// Import the mocked hook
|
||||||
import { usePagination } from "@/hooks/usePagination";
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
const mockedUsePagination = usePagination as jest.MockedFunction<
|
const mockedUsePagination = usePagination as jest.MockedFunction<
|
||||||
typeof usePagination
|
typeof usePagination
|
||||||
>;
|
>;
|
||||||
|
|
|
@ -10,8 +10,7 @@ import {
|
||||||
extractTextFromContentPart,
|
extractTextFromContentPart,
|
||||||
extractDisplayableText,
|
extractDisplayableText,
|
||||||
} from "@/lib/format-message-content";
|
} from "@/lib/format-message-content";
|
||||||
import { usePagination } from "@/hooks/usePagination";
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
import { client } from "@/lib/client";
|
|
||||||
|
|
||||||
interface ChatCompletionsTableProps {
|
interface ChatCompletionsTableProps {
|
||||||
/** Optional pagination configuration */
|
/** Optional pagination configuration */
|
||||||
|
@ -32,12 +31,15 @@ function formatChatCompletionToRow(completion: ChatCompletion): LogTableRow {
|
||||||
export function ChatCompletionsTable({
|
export function ChatCompletionsTable({
|
||||||
paginationOptions,
|
paginationOptions,
|
||||||
}: ChatCompletionsTableProps) {
|
}: ChatCompletionsTableProps) {
|
||||||
const fetchFunction = async (params: {
|
const fetchFunction = async (
|
||||||
|
client: ReturnType<typeof import("@/hooks/use-auth-client").useAuthClient>,
|
||||||
|
params: {
|
||||||
after?: string;
|
after?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
model?: string;
|
model?: string;
|
||||||
order?: string;
|
order?: string;
|
||||||
}) => {
|
},
|
||||||
|
) => {
|
||||||
const response = await client.chat.completions.list({
|
const response = await client.chat.completions.list({
|
||||||
after: params.after,
|
after: params.after,
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
|
|
|
@ -12,7 +12,7 @@ jest.mock("next/navigation", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the useInfiniteScroll hook
|
// Mock the useInfiniteScroll hook
|
||||||
jest.mock("@/hooks/useInfiniteScroll", () => ({
|
jest.mock("@/hooks/use-infinite-scroll", () => ({
|
||||||
useInfiniteScroll: jest.fn((onLoadMore, options) => {
|
useInfiniteScroll: jest.fn((onLoadMore, options) => {
|
||||||
const ref = React.useRef(null);
|
const ref = React.useRef(null);
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useRouter } from "next/navigation";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { truncateText } from "@/lib/truncate-text";
|
import { truncateText } from "@/lib/truncate-text";
|
||||||
import { PaginationStatus } from "@/lib/types";
|
import { PaginationStatus } from "@/lib/types";
|
||||||
import { useInfiniteScroll } from "@/hooks/useInfiniteScroll";
|
import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
7
llama_stack/ui/components/providers/session-provider.tsx
Normal file
7
llama_stack/ui/components/providers/session-provider.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
|
||||||
|
|
||||||
|
export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
|
||||||
|
}
|
|
@ -12,21 +12,31 @@ jest.mock("next/navigation", () => ({
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock next-auth
|
||||||
|
jest.mock("next-auth/react", () => ({
|
||||||
|
useSession: () => ({
|
||||||
|
status: "authenticated",
|
||||||
|
data: { accessToken: "mock-token" },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
// Mock helper functions
|
// Mock helper functions
|
||||||
jest.mock("@/lib/truncate-text");
|
jest.mock("@/lib/truncate-text");
|
||||||
|
|
||||||
// Mock the client
|
// Mock the auth client hook
|
||||||
jest.mock("@/lib/client", () => ({
|
const mockClient = {
|
||||||
client: {
|
|
||||||
responses: {
|
responses: {
|
||||||
list: jest.fn(),
|
list: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
|
||||||
|
jest.mock("@/hooks/use-auth-client", () => ({
|
||||||
|
useAuthClient: () => mockClient,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the usePagination hook
|
// Mock the usePagination hook
|
||||||
const mockLoadMore = jest.fn();
|
const mockLoadMore = jest.fn();
|
||||||
jest.mock("@/hooks/usePagination", () => ({
|
jest.mock("@/hooks/use-pagination", () => ({
|
||||||
usePagination: jest.fn(() => ({
|
usePagination: jest.fn(() => ({
|
||||||
data: [],
|
data: [],
|
||||||
status: "idle",
|
status: "idle",
|
||||||
|
@ -40,7 +50,7 @@ jest.mock("@/hooks/usePagination", () => ({
|
||||||
import { truncateText as originalTruncateText } from "@/lib/truncate-text";
|
import { truncateText as originalTruncateText } from "@/lib/truncate-text";
|
||||||
|
|
||||||
// Import the mocked hook
|
// Import the mocked hook
|
||||||
import { usePagination } from "@/hooks/usePagination";
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
const mockedUsePagination = usePagination as jest.MockedFunction<
|
const mockedUsePagination = usePagination as jest.MockedFunction<
|
||||||
typeof usePagination
|
typeof usePagination
|
||||||
>;
|
>;
|
||||||
|
|
|
@ -6,8 +6,7 @@ import {
|
||||||
UsePaginationOptions,
|
UsePaginationOptions,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
import { LogsTable, LogTableRow } from "@/components/logs/logs-table";
|
import { LogsTable, LogTableRow } from "@/components/logs/logs-table";
|
||||||
import { usePagination } from "@/hooks/usePagination";
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
import { client } from "@/lib/client";
|
|
||||||
import type { ResponseListResponse } from "llama-stack-client/resources/responses/responses";
|
import type { ResponseListResponse } from "llama-stack-client/resources/responses/responses";
|
||||||
import {
|
import {
|
||||||
isMessageInput,
|
isMessageInput,
|
||||||
|
@ -125,12 +124,15 @@ function formatResponseToRow(response: OpenAIResponse): LogTableRow {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResponsesTable({ paginationOptions }: ResponsesTableProps) {
|
export function ResponsesTable({ paginationOptions }: ResponsesTableProps) {
|
||||||
const fetchFunction = async (params: {
|
const fetchFunction = async (
|
||||||
|
client: ReturnType<typeof import("@/hooks/use-auth-client").useAuthClient>,
|
||||||
|
params: {
|
||||||
after?: string;
|
after?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
model?: string;
|
model?: string;
|
||||||
order?: string;
|
order?: string;
|
||||||
}) => {
|
},
|
||||||
|
) => {
|
||||||
const response = await client.responses.list({
|
const response = await client.responses.list({
|
||||||
after: params.after,
|
after: params.after,
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
|
|
25
llama_stack/ui/components/ui/sign-in-button.tsx
Normal file
25
llama_stack/ui/components/ui/sign-in-button.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { User } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
|
export function SignInButton() {
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" size="sm" asChild>
|
||||||
|
<Link href="/auth/signin" className="flex items-center">
|
||||||
|
<User className="mr-2 h-4 w-4" />
|
||||||
|
<span>
|
||||||
|
{status === "loading"
|
||||||
|
? "Loading..."
|
||||||
|
: session
|
||||||
|
? session.user?.email || "Signed In"
|
||||||
|
: "Sign In"}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
24
llama_stack/ui/hooks/use-auth-client.ts
Normal file
24
llama_stack/ui/hooks/use-auth-client.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import LlamaStackClient from "llama-stack-client";
|
||||||
|
|
||||||
|
export function useAuthClient() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
const client = useMemo(() => {
|
||||||
|
const clientHostname =
|
||||||
|
typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
|
||||||
|
const options: any = {
|
||||||
|
baseURL: `${clientHostname}/api`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (session?.accessToken) {
|
||||||
|
options.apiKey = session.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LlamaStackClient(options);
|
||||||
|
}, [session?.accessToken]);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
import { useState, useCallback, useEffect, useRef } from "react";
|
import { useState, useCallback, useEffect, useRef } from "react";
|
||||||
import { PaginationStatus, UsePaginationOptions } from "@/lib/types";
|
import { PaginationStatus, UsePaginationOptions } from "@/lib/types";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useAuthClient } from "@/hooks/use-auth-client";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface PaginationState<T> {
|
interface PaginationState<T> {
|
||||||
data: T[];
|
data: T[];
|
||||||
|
@ -28,13 +31,18 @@ export interface PaginationReturn<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UsePaginationParams<T> extends UsePaginationOptions {
|
interface UsePaginationParams<T> extends UsePaginationOptions {
|
||||||
fetchFunction: (params: {
|
fetchFunction: (
|
||||||
|
client: ReturnType<typeof useAuthClient>,
|
||||||
|
params: {
|
||||||
after?: string;
|
after?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
model?: string;
|
model?: string;
|
||||||
order?: string;
|
order?: string;
|
||||||
}) => Promise<PaginationResponse<T>>;
|
},
|
||||||
|
) => Promise<PaginationResponse<T>>;
|
||||||
errorMessagePrefix: string;
|
errorMessagePrefix: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
useAuth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePagination<T>({
|
export function usePagination<T>({
|
||||||
|
@ -43,7 +51,12 @@ export function usePagination<T>({
|
||||||
order = "desc",
|
order = "desc",
|
||||||
fetchFunction,
|
fetchFunction,
|
||||||
errorMessagePrefix,
|
errorMessagePrefix,
|
||||||
|
enabled = true,
|
||||||
|
useAuth = true,
|
||||||
}: UsePaginationParams<T>): PaginationReturn<T> {
|
}: UsePaginationParams<T>): PaginationReturn<T> {
|
||||||
|
const { status: sessionStatus } = useSession();
|
||||||
|
const client = useAuthClient();
|
||||||
|
const router = useRouter();
|
||||||
const [state, setState] = useState<PaginationState<T>>({
|
const [state, setState] = useState<PaginationState<T>>({
|
||||||
data: [],
|
data: [],
|
||||||
status: "loading",
|
status: "loading",
|
||||||
|
@ -74,7 +87,7 @@ export function usePagination<T>({
|
||||||
error: null,
|
error: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response = await fetchFunction({
|
const response = await fetchFunction(client, {
|
||||||
after: after || undefined,
|
after: after || undefined,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
...(model && { model }),
|
...(model && { model }),
|
||||||
|
@ -91,6 +104,17 @@ export function usePagination<T>({
|
||||||
status: "idle",
|
status: "idle",
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Check if it's a 401 unauthorized error
|
||||||
|
if (
|
||||||
|
err &&
|
||||||
|
typeof err === "object" &&
|
||||||
|
"status" in err &&
|
||||||
|
err.status === 401
|
||||||
|
) {
|
||||||
|
router.push("/auth/signin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage = isInitialLoad
|
const errorMessage = isInitialLoad
|
||||||
? `Failed to load ${errorMessagePrefix}. Please try refreshing the page.`
|
? `Failed to load ${errorMessagePrefix}. Please try refreshing the page.`
|
||||||
: `Failed to load more ${errorMessagePrefix}. Please try again.`;
|
: `Failed to load more ${errorMessagePrefix}. Please try again.`;
|
||||||
|
@ -107,7 +131,7 @@ export function usePagination<T>({
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[limit, model, order, fetchFunction, errorMessagePrefix],
|
[limit, model, order, fetchFunction, errorMessagePrefix, client, router],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,17 +144,28 @@ export function usePagination<T>({
|
||||||
}
|
}
|
||||||
}, [fetchData]);
|
}, [fetchData]);
|
||||||
|
|
||||||
// Auto-load initial data on mount
|
// Auto-load initial data on mount when enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasFetchedInitialData.current) {
|
// If using auth, wait for session to load
|
||||||
|
const isAuthReady = !useAuth || sessionStatus !== "loading";
|
||||||
|
const shouldFetch = enabled && isAuthReady;
|
||||||
|
|
||||||
|
if (shouldFetch && !hasFetchedInitialData.current) {
|
||||||
hasFetchedInitialData.current = true;
|
hasFetchedInitialData.current = true;
|
||||||
fetchData();
|
fetchData();
|
||||||
|
} else if (!shouldFetch) {
|
||||||
|
// Reset the flag when disabled so it can fetch when re-enabled
|
||||||
|
hasFetchedInitialData.current = false;
|
||||||
}
|
}
|
||||||
}, [fetchData]);
|
}, [fetchData, enabled, useAuth, sessionStatus]);
|
||||||
|
|
||||||
|
// Override status if we're waiting for auth
|
||||||
|
const effectiveStatus =
|
||||||
|
useAuth && sessionStatus === "loading" ? "loading" : state.status;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: state.data,
|
data: state.data,
|
||||||
status: state.status,
|
status: effectiveStatus,
|
||||||
hasMore: state.hasMore,
|
hasMore: state.hasMore,
|
||||||
error: state.error,
|
error: state.error,
|
||||||
loadMore,
|
loadMore,
|
11
llama_stack/ui/instrumentation.ts
Normal file
11
llama_stack/ui/instrumentation.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* Next.js Instrumentation
|
||||||
|
* This file is used for initializing monitoring, tracing, or other observability tools.
|
||||||
|
* It runs once when the server starts, before any application code.
|
||||||
|
*
|
||||||
|
* Learn more: https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function register() {
|
||||||
|
await import("./lib/config-validator");
|
||||||
|
}
|
38
llama_stack/ui/lib/auth.ts
Normal file
38
llama_stack/ui/lib/auth.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { NextAuthOptions } from "next-auth";
|
||||||
|
import GithubProvider from "next-auth/providers/github";
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
providers: [
|
||||||
|
GithubProvider({
|
||||||
|
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||||
|
authorization: {
|
||||||
|
params: {
|
||||||
|
scope: "read:user user:email",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
debug: process.env.NODE_ENV === "development",
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, account }) {
|
||||||
|
// Persist the OAuth access_token to the token right after signin
|
||||||
|
if (account) {
|
||||||
|
token.accessToken = account.access_token;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
// Send properties to the client, like an access_token from a provider.
|
||||||
|
session.accessToken = token.accessToken as string;
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: "/auth/signin",
|
||||||
|
error: "/auth/signin", // Redirect errors to our custom page
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,6 +0,0 @@
|
||||||
import LlamaStackClient from "llama-stack-client";
|
|
||||||
|
|
||||||
export const client = new LlamaStackClient({
|
|
||||||
baseURL:
|
|
||||||
typeof window !== "undefined" ? `${window.location.origin}/api` : "/api",
|
|
||||||
});
|
|
56
llama_stack/ui/lib/config-validator.ts
Normal file
56
llama_stack/ui/lib/config-validator.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* Validates environment configuration for the application
|
||||||
|
* This is called during server initialization
|
||||||
|
*/
|
||||||
|
export function validateServerConfig() {
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
console.log("🚀 Starting Llama Stack UI Server...");
|
||||||
|
|
||||||
|
// Check optional configurations
|
||||||
|
const optionalConfigs = {
|
||||||
|
NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:8322",
|
||||||
|
LLAMA_STACK_BACKEND_URL:
|
||||||
|
process.env.LLAMA_STACK_BACKEND_URL || "http://localhost:8321",
|
||||||
|
LLAMA_STACK_UI_PORT: process.env.LLAMA_STACK_UI_PORT || "8322",
|
||||||
|
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
||||||
|
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("\n📋 Configuration:");
|
||||||
|
console.log(` - NextAuth URL: ${optionalConfigs.NEXTAUTH_URL}`);
|
||||||
|
console.log(` - Backend URL: ${optionalConfigs.LLAMA_STACK_BACKEND_URL}`);
|
||||||
|
console.log(` - UI Port: ${optionalConfigs.LLAMA_STACK_UI_PORT}`);
|
||||||
|
|
||||||
|
// Check GitHub OAuth configuration
|
||||||
|
if (
|
||||||
|
!optionalConfigs.GITHUB_CLIENT_ID ||
|
||||||
|
!optionalConfigs.GITHUB_CLIENT_SECRET
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"\n📝 GitHub OAuth not configured (authentication features disabled)",
|
||||||
|
);
|
||||||
|
console.log(" To enable GitHub OAuth:");
|
||||||
|
console.log(" 1. Go to https://github.com/settings/applications/new");
|
||||||
|
console.log(
|
||||||
|
" 2. Set Application name: Llama Stack UI (or your preferred name)",
|
||||||
|
);
|
||||||
|
console.log(" 3. Set Homepage URL: http://localhost:8322");
|
||||||
|
console.log(
|
||||||
|
" 4. Set Authorization callback URL: http://localhost:8322/api/auth/callback/github",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" 5. Create the app and copy the Client ID and Client Secret",
|
||||||
|
);
|
||||||
|
console.log(" 6. Add them to your .env.local file:");
|
||||||
|
console.log(" GITHUB_CLIENT_ID=your_client_id");
|
||||||
|
console.log(" GITHUB_CLIENT_SECRET=your_client_secret");
|
||||||
|
} else {
|
||||||
|
console.log(" - GitHub OAuth: ✅ Configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this function when the module is imported
|
||||||
|
validateServerConfig();
|
147
llama_stack/ui/package-lock.json
generated
147
llama_stack/ui/package-lock.json
generated
|
@ -18,6 +18,7 @@
|
||||||
"llama-stack-client": "0.2.13",
|
"llama-stack-client": "0.2.13",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.510.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
|
"next-auth": "^4.24.11",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
@ -548,7 +549,6 @@
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
||||||
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
@ -2423,6 +2423,15 @@
|
||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@panva/hkdf": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgr/core": {
|
"node_modules/@pkgr/core": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz",
|
||||||
|
@ -5279,7 +5288,6 @@
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
|
@ -9036,6 +9044,15 @@
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "4.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||||
|
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
@ -9949,6 +9966,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-auth": {
|
||||||
|
"version": "4.24.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz",
|
||||||
|
"integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@panva/hkdf": "^1.0.2",
|
||||||
|
"cookie": "^0.7.0",
|
||||||
|
"jose": "^4.15.5",
|
||||||
|
"oauth": "^0.9.15",
|
||||||
|
"openid-client": "^5.4.0",
|
||||||
|
"preact": "^10.6.3",
|
||||||
|
"preact-render-to-string": "^5.1.19",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@auth/core": "0.34.2",
|
||||||
|
"next": "^12.2.5 || ^13 || ^14 || ^15",
|
||||||
|
"nodemailer": "^6.6.5",
|
||||||
|
"react": "^17.0.2 || ^18 || ^19",
|
||||||
|
"react-dom": "^17.0.2 || ^18 || ^19"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@auth/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-themes": {
|
"node_modules/next-themes": {
|
||||||
"version": "0.4.6",
|
"version": "0.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
||||||
|
@ -10071,6 +10120,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth": {
|
||||||
|
"version": "0.9.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||||
|
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
@ -10081,6 +10136,15 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-hash": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
@ -10194,6 +10258,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oidc-token-hash": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || >=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
@ -10233,6 +10306,39 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openid-client": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^4.15.9",
|
||||||
|
"lru-cache": "^6.0.0",
|
||||||
|
"object-hash": "^2.2.0",
|
||||||
|
"oidc-token-hash": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client/node_modules/lru-cache": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client/node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
@ -10560,6 +10666,34 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.26.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz",
|
||||||
|
"integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/preact-render-to-string": {
|
||||||
|
"version": "5.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
|
||||||
|
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pretty-format": "^3.8.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/preact-render-to-string/node_modules/pretty-format": {
|
||||||
|
"version": "3.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||||
|
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
@ -12409,6 +12543,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"llama-stack-client": "0.2.13",
|
"llama-stack-client": "0.2.13",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.510.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
|
"next-auth": "^4.24.11",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|
7
llama_stack/ui/types/next-auth.d.ts
vendored
Normal file
7
llama_stack/ui/types/next-auth.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import NextAuth from "next-auth";
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
interface Session {
|
||||||
|
accessToken?: string;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue