From fc3286be7e7c746f6f5812a08807ffaea65b2c78 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Sat, 19 Jul 2025 23:47:10 -0400 Subject: [PATCH] feat(UI): adding MVP playground ui Signed-off-by: Francisco Javier Arceo --- llama_stack/ui/app/chat-playground/page.tsx | 242 ++++++++++++++++++ .../ui/components/layout/app-sidebar.tsx | 6 + llama_stack/ui/package-lock.json | 8 +- 3 files changed, 252 insertions(+), 4 deletions(-) create mode 100644 llama_stack/ui/app/chat-playground/page.tsx diff --git a/llama_stack/ui/app/chat-playground/page.tsx b/llama_stack/ui/app/chat-playground/page.tsx new file mode 100644 index 000000000..9f50a1bdb --- /dev/null +++ b/llama_stack/ui/app/chat-playground/page.tsx @@ -0,0 +1,242 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { ChatMessage } from "@/lib/types"; +import { ChatMessageItem } from "@/components/chat-completions/chat-messasge-item"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Send, Loader2, ChevronDown } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useAuthClient } from "@/hooks/use-auth-client"; +import type { CompletionCreateParams } from "llama-stack-client/resources/chat/completions"; +import type { Model } from "llama-stack-client/resources/models"; + +export default function ChatPlaygroundPage() { + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [models, setModels] = useState([]); + const [selectedModel, setSelectedModel] = useState(""); + const [modelsLoading, setModelsLoading] = useState(true); + const [modelsError, setModelsError] = useState(null); + const messagesEndRef = useRef(null); + const client = useAuthClient(); + const isModelsLoading = modelsLoading ?? true; + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + useEffect(() => { + const fetchModels = async () => { + try { + setModelsLoading(true); + setModelsError(null); + const modelList = await client.models.list(); + const llmModels = modelList.filter(model => model.model_type === 'llm'); + setModels(llmModels); + if (llmModels.length > 0) { + setSelectedModel(llmModels[0].identifier); + } + } catch (err) { + console.error("Error fetching models:", err); + setModelsError("Failed to fetch available models"); + } finally { + setModelsLoading(false); + } + }; + + fetchModels(); + }, [client]); + + const extractTextContent = (content: any): string => { + if (typeof content === 'string') { + return content; + } + if (Array.isArray(content)) { + return content + .filter(item => item.type === 'text') + .map(item => item.text) + .join(''); + } + if (content && content.type === 'text') { + return content.text || ''; + } + return ''; + }; + + const handleSendMessage = async () => { + if (!inputMessage.trim() || isLoading || !selectedModel) return; + + const userMessage: ChatMessage = { + role: "user", + content: inputMessage.trim(), + }; + + setMessages(prev => [...prev, userMessage]); + setInputMessage(""); + setIsLoading(true); + setError(null); + + try { + const messageParams: CompletionCreateParams["messages"] = [...messages, userMessage].map(msg => { + const content = typeof msg.content === 'string' ? msg.content : extractTextContent(msg.content); + if (msg.role === "user") { + return { role: "user" as const, content }; + } else if (msg.role === "assistant") { + return { role: "assistant" as const, content }; + } else { + return { role: "system" as const, content }; + } + }); + + const response = await client.chat.completions.create({ + model: selectedModel, + messages: messageParams, + stream: false, + }); + + if ('choices' in response && response.choices && response.choices.length > 0) { + const choice = response.choices[0]; + if ('message' in choice && choice.message) { + const assistantMessage: ChatMessage = { + role: "assistant", + content: extractTextContent(choice.message.content), + }; + setMessages(prev => [...prev, assistantMessage]); + } + } + } catch (err) { + console.error("Error sending message:", err); + setError("Failed to send message. Please try again."); + } finally { + setIsLoading(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const clearChat = () => { + setMessages([]); + setError(null); + }; + + return ( +
+
+

Chat Playground

+
+ + + + + + {models.map((model) => ( + setSelectedModel(model.identifier)} + > + {model.identifier} + + ))} + + + +
+
+ + + + Chat Messages + + +
+ {messages.length === 0 ? ( +
+

Start a conversation by typing a message below.

+
+ ) : ( + messages.map((message, index) => ( + + )) + )} + {isLoading && ( +
+ + Thinking... +
+ )} +
+
+ + {modelsError && ( +
+

{modelsError}

+
+ )} + + {error && ( +
+

{error}

+
+ )} + +
+ setInputMessage(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Type your message here..." + disabled={isLoading} + className="flex-1" + /> + +
+ + +
+ ); +} diff --git a/llama_stack/ui/components/layout/app-sidebar.tsx b/llama_stack/ui/components/layout/app-sidebar.tsx index 532e43dbd..007f88537 100644 --- a/llama_stack/ui/components/layout/app-sidebar.tsx +++ b/llama_stack/ui/components/layout/app-sidebar.tsx @@ -5,6 +5,7 @@ import { MessagesSquare, MoveUpRight, Database, + MessageCircle, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -23,6 +24,11 @@ import { } from "@/components/ui/sidebar"; const logItems = [ + { + title: "Chat Playground", + url: "/chat-playground", + icon: MessageCircle, + }, { title: "Chat Completions", url: "/logs/chat-completions", diff --git a/llama_stack/ui/package-lock.json b/llama_stack/ui/package-lock.json index 158569241..d77d434eb 100644 --- a/llama_stack/ui/package-lock.json +++ b/llama_stack/ui/package-lock.json @@ -15,7 +15,7 @@ "@radix-ui/react-tooltip": "^1.2.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "llama-stack-client": "^0.2.14", + "llama-stack-client": "^0.2.15", "lucide-react": "^0.510.0", "next": "15.3.3", "next-auth": "^4.24.11", @@ -9099,9 +9099,9 @@ "license": "MIT" }, "node_modules/llama-stack-client": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/llama-stack-client/-/llama-stack-client-0.2.14.tgz", - "integrity": "sha512-bVU3JHp+EPEKR0Vb9vcd9ZyQj/72jSDuptKLwOXET9WrkphIQ8xuW5ueecMTgq8UEls3lwB3HiZM2cDOR9eDsQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/llama-stack-client/-/llama-stack-client-0.2.15.tgz", + "integrity": "sha512-onfYzgPWAxve4uP7BuiK/ZdEC7w6X1PIXXXpQY57qZC7C4xUAM5kwfT3JWIe/jE22Lwc2vTN1ScfYlAYcoYAsg==", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18",