From f609d15b33a2bb11cc3ef31ff03164bd69724633 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Tue, 12 Aug 2025 19:54:55 -0400 Subject: [PATCH 1/3] feat: Adding support for selecting a vector store in chat playground Signed-off-by: Francisco Javier Arceo --- llama_stack/ui/app/chat-playground/page.tsx | 89 ++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/llama_stack/ui/app/chat-playground/page.tsx b/llama_stack/ui/app/chat-playground/page.tsx index c31248b78..c900c57ab 100644 --- a/llama_stack/ui/app/chat-playground/page.tsx +++ b/llama_stack/ui/app/chat-playground/page.tsx @@ -15,6 +15,8 @@ import { type Message } from "@/components/chat-playground/chat-message"; 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"; +import type { VectorDBListResponse } from "llama-stack-client/resources/vector-dbs"; +import { VectorDbManager } from "@/components/vector-db/vector-db-manager"; export default function ChatPlaygroundPage() { const [messages, setMessages] = useState([]); @@ -25,6 +27,10 @@ export default function ChatPlaygroundPage() { const [selectedModel, setSelectedModel] = useState(""); const [modelsLoading, setModelsLoading] = useState(true); const [modelsError, setModelsError] = useState(null); + const [vectorDbs, setVectorDbs] = useState([]); + const [selectedVectorDb, setSelectedVectorDb] = useState(""); + const [vectorDbsLoading, setVectorDbsLoading] = useState(true); + const [vectorDbsError, setVectorDbsError] = useState(null); const client = useAuthClient(); const isModelsLoading = modelsLoading ?? true; @@ -49,7 +55,22 @@ export default function ChatPlaygroundPage() { } }; + const fetchVectorDbs = async () => { + try { + setVectorDbsLoading(true); + setVectorDbsError(null); + const vectorDbList = await client.vectorDBs.list(); + setVectorDbs(vectorDbList); + } catch (err) { + console.error("Error fetching vector DBs:", err); + setVectorDbsError("Failed to fetch available vector databases"); + } finally { + setVectorDbsLoading(false); + } + }; + fetchModels(); + fetchVectorDbs(); }, [client]); const extractTextContent = (content: unknown): string => { @@ -96,6 +117,35 @@ const handleSubmitWithContent = async (content: string) => { setError(null); try { + let enhancedContent = content; + + // If a vector DB is selected, query for relevant context + if (selectedVectorDb && selectedVectorDb !== "none") { + try { + const vectorResponse = await client.vectorIo.query({ + query: content, + vector_db_id: selectedVectorDb, + }); + + if (vectorResponse.chunks && vectorResponse.chunks.length > 0) { + const context = vectorResponse.chunks + .map(chunk => { + // Extract text content from the chunk + const chunkContent = typeof chunk.content === 'string' + ? chunk.content + : extractTextContent(chunk.content); + return chunkContent; + }) + .join('\n\n'); + + enhancedContent = `Please answer the following query using the context below.\n\nCONTEXT:\n${context}\n\nQUERY:\n${content}`; + } + } catch (vectorErr) { + console.error("Error querying vector DB:", vectorErr); + // Continue with original content if vector query fails + } + } + const messageParams: CompletionCreateParams["messages"] = [ ...messages.map(msg => { const msgContent = typeof msg.content === 'string' ? msg.content : extractTextContent(msg.content); @@ -107,7 +157,7 @@ const handleSubmitWithContent = async (content: string) => { return { role: "system" as const, content: msgContent }; } }), - { role: "user" as const, content } + { role: "user" as const, content: enhancedContent } ]; const response = await client.chat.completions.create({ @@ -172,6 +222,20 @@ const handleSubmitWithContent = async (content: string) => { setError(null); }; + const refreshVectorDbs = async () => { + try { + setVectorDbsLoading(true); + setVectorDbsError(null); + const vectorDbList = await client.vectorDBs.list(); + setVectorDbs(vectorDbList); + } catch (err) { + console.error("Error refreshing vector DBs:", err); + setVectorDbsError("Failed to refresh vector databases"); + } finally { + setVectorDbsLoading(false); + } + }; + return (
@@ -189,6 +253,23 @@ const handleSubmitWithContent = async (content: string) => { ))} + + @@ -201,6 +282,12 @@ const handleSubmitWithContent = async (content: string) => {
)} + {vectorDbsError && ( +
+

{vectorDbsError}

+
+ )} + {error && (

{error}

From 82de94a11bb2719836bf270538db4edc920c816d Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Tue, 12 Aug 2025 23:48:56 -0400 Subject: [PATCH 2/3] fixed width behavior and made session list cleaner Signed-off-by: Francisco Javier Arceo --- llama_stack/ui/app/api/fetch-url/route.ts | 58 +++ llama_stack/ui/app/chat-playground/page.tsx | 209 +++++++--- .../chat-playground/chat-message.tsx | 10 +- .../ui/components/chat-playground/chat.tsx | 14 +- .../chat-playground/session-manager.tsx | 256 ++++++++++++ .../vector-db/vector-db-manager.tsx | 380 ++++++++++++++++++ 6 files changed, 859 insertions(+), 68 deletions(-) create mode 100644 llama_stack/ui/app/api/fetch-url/route.ts create mode 100644 llama_stack/ui/components/chat-playground/session-manager.tsx create mode 100644 llama_stack/ui/components/vector-db/vector-db-manager.tsx diff --git a/llama_stack/ui/app/api/fetch-url/route.ts b/llama_stack/ui/app/api/fetch-url/route.ts new file mode 100644 index 000000000..dac6fecac --- /dev/null +++ b/llama_stack/ui/app/api/fetch-url/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const { url } = await request.json(); + + if (!url || typeof url !== 'string') { + return NextResponse.json( + { error: 'URL is required' }, + { status: 400 } + ); + } + + // Fetch the URL content + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const contentType = response.headers.get('content-type') || ''; + let content: string; + + if (contentType.includes('application/json')) { + const json = await response.json(); + content = JSON.stringify(json, null, 2); + } else if (contentType.includes('text/html')) { + const html = await response.text(); + // Basic HTML to text conversion - remove tags and decode entities + content = html + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/)<[^<]*)*<\/style>/gi, '') + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, ' ') + .trim(); + } else { + content = await response.text(); + } + + return NextResponse.json({ content }); + } catch (error) { + console.error('Error fetching URL:', error); + return NextResponse.json( + { error: 'Failed to fetch URL content' }, + { status: 500 } + ); + } +} diff --git a/llama_stack/ui/app/chat-playground/page.tsx b/llama_stack/ui/app/chat-playground/page.tsx index c900c57ab..86efe3d5c 100644 --- a/llama_stack/ui/app/chat-playground/page.tsx +++ b/llama_stack/ui/app/chat-playground/page.tsx @@ -17,24 +17,52 @@ import type { CompletionCreateParams } from "llama-stack-client/resources/chat/c import type { Model } from "llama-stack-client/resources/models"; import type { VectorDBListResponse } from "llama-stack-client/resources/vector-dbs"; import { VectorDbManager } from "@/components/vector-db/vector-db-manager"; +import { SessionManager, SessionUtils } from "@/components/chat-playground/session-manager"; + +interface ChatSession { + id: string; + name: string; + messages: Message[]; + selectedModel: string; + selectedVectorDb: string; + createdAt: number; + updatedAt: number; +} export default function ChatPlaygroundPage() { - const [messages, setMessages] = useState([]); + const [currentSession, setCurrentSession] = useState(null); const [input, setInput] = useState(""); const [isGenerating, setIsGenerating] = 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 [vectorDbs, setVectorDbs] = useState([]); - const [selectedVectorDb, setSelectedVectorDb] = useState(""); const [vectorDbsLoading, setVectorDbsLoading] = useState(true); const [vectorDbsError, setVectorDbsError] = useState(null); const client = useAuthClient(); const isModelsLoading = modelsLoading ?? true; + // Load current session on mount + useEffect(() => { + const savedSession = SessionUtils.loadCurrentSession(); + if (savedSession) { + setCurrentSession(savedSession); + } else { + // Create default session if none exists - will be updated with model when models load + const defaultSession = SessionUtils.createDefaultSession(); + setCurrentSession(defaultSession); + SessionUtils.saveCurrentSession(defaultSession); + } + }, []); + + // Save session when it changes + useEffect(() => { + if (currentSession) { + SessionUtils.saveCurrentSession(currentSession); + } + }, [currentSession]); useEffect(() => { const fetchModels = async () => { @@ -44,8 +72,8 @@ export default function ChatPlaygroundPage() { 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); + if (llmModels.length > 0 && currentSession && !currentSession.selectedModel) { + setCurrentSession(prev => prev ? { ...prev, selectedModel: llmModels[0].identifier } : null); } } catch (err) { console.error("Error fetching models:", err); @@ -95,7 +123,7 @@ export default function ChatPlaygroundPage() { const handleSubmit = async (event?: { preventDefault?: () => void }) => { event?.preventDefault?.(); - if (!input.trim()) return; + if (!input.trim() || !currentSession || !currentSession.selectedModel) return; // Add user message to chat const userMessage: Message = { @@ -105,7 +133,11 @@ const handleSubmit = async (event?: { preventDefault?: () => void }) => { createdAt: new Date(), }; - setMessages(prev => [...prev, userMessage]); + setCurrentSession(prev => prev ? { + ...prev, + messages: [...prev.messages, userMessage], + updatedAt: Date.now() + } : null); setInput(""); // Use the helper function with the content @@ -118,26 +150,26 @@ const handleSubmitWithContent = async (content: string) => { try { let enhancedContent = content; - + // If a vector DB is selected, query for relevant context - if (selectedVectorDb && selectedVectorDb !== "none") { + if (currentSession?.selectedVectorDb && currentSession.selectedVectorDb !== "none") { try { const vectorResponse = await client.vectorIo.query({ query: content, - vector_db_id: selectedVectorDb, + vector_db_id: currentSession.selectedVectorDb, }); - + if (vectorResponse.chunks && vectorResponse.chunks.length > 0) { const context = vectorResponse.chunks .map(chunk => { // Extract text content from the chunk - const chunkContent = typeof chunk.content === 'string' - ? chunk.content + const chunkContent = typeof chunk.content === 'string' + ? chunk.content : extractTextContent(chunk.content); return chunkContent; }) .join('\n\n'); - + enhancedContent = `Please answer the following query using the context below.\n\nCONTEXT:\n${context}\n\nQUERY:\n${content}`; } } catch (vectorErr) { @@ -147,7 +179,7 @@ const handleSubmitWithContent = async (content: string) => { } const messageParams: CompletionCreateParams["messages"] = [ - ...messages.map(msg => { + ...(currentSession?.messages || []).map(msg => { const msgContent = typeof msg.content === 'string' ? msg.content : extractTextContent(msg.content); if (msg.role === "user") { return { role: "user" as const, content: msgContent }; @@ -161,7 +193,7 @@ const handleSubmitWithContent = async (content: string) => { ]; const response = await client.chat.completions.create({ - model: selectedModel, + model: currentSession?.selectedModel || "", messages: messageParams, stream: true, }); @@ -173,7 +205,12 @@ const handleSubmitWithContent = async (content: string) => { createdAt: new Date(), }; - setMessages(prev => [...prev, assistantMessage]); + setCurrentSession(prev => prev ? { + ...prev, + messages: [...prev.messages, assistantMessage], + updatedAt: Date.now() + } : null); + let fullContent = ""; for await (const chunk of response) { if (chunk.choices && chunk.choices[0]?.delta?.content) { @@ -181,13 +218,14 @@ const handleSubmitWithContent = async (content: string) => { fullContent += deltaContent; flushSync(() => { - setMessages(prev => { - const newMessages = [...prev]; + setCurrentSession(prev => { + if (!prev) return null; + const newMessages = [...prev.messages]; const lastMessage = newMessages[newMessages.length - 1]; if (lastMessage.role === "assistant") { lastMessage.content = fullContent; } - return newMessages; + return { ...prev, messages: newMessages, updatedAt: Date.now() }; }); }); } @@ -195,7 +233,11 @@ const handleSubmitWithContent = async (content: string) => { } catch (err) { console.error("Error sending message:", err); setError("Failed to send message. Please try again."); - setMessages(prev => prev.slice(0, -1)); + setCurrentSession(prev => prev ? { + ...prev, + messages: prev.messages.slice(0, -1), + updatedAt: Date.now() + } : null); } finally { setIsGenerating(false); } @@ -213,15 +255,41 @@ const handleSubmitWithContent = async (content: string) => { content: message.content, createdAt: new Date(), }; - setMessages(prev => [...prev, newMessage]) + setCurrentSession(prev => prev ? { + ...prev, + messages: [...prev.messages, newMessage], + updatedAt: Date.now() + } : null); handleSubmitWithContent(newMessage.content); }; const clearChat = () => { - setMessages([]); + setCurrentSession(prev => prev ? { + ...prev, + messages: [], + updatedAt: Date.now() + } : null); setError(null); }; + const handleSessionChange = (session: ChatSession) => { + setCurrentSession(session); + setError(null); + }; + + const handleNewSession = () => { + const defaultModel = currentSession?.selectedModel || (models.length > 0 ? models[0].identifier : ""); + const defaultVectorDb = currentSession?.selectedVectorDb || ""; + + const newSession = { + ...SessionUtils.createDefaultSession(), + selectedModel: defaultModel, + selectedVectorDb: defaultVectorDb, + }; + setCurrentSession(newSession); + SessionUtils.saveCurrentSession(newSession); + }; + const refreshVectorDbs = async () => { try { setVectorDbsLoading(true); @@ -237,43 +305,70 @@ const handleSubmitWithContent = async (content: string) => { }; return ( -
-
-

Chat Playground

-
- - - +
+
+

Chat Playground

+
+ +
+
+ +
+
+ Model: + +
+ +
+ Vector DB: + +
+ + +
{modelsError && ( @@ -296,14 +391,14 @@ const handleSubmitWithContent = async (content: string) => { setCurrentSession(prev => prev ? { ...prev, messages, updatedAt: Date.now() } : null)} />
); diff --git a/llama_stack/ui/components/chat-playground/chat-message.tsx b/llama_stack/ui/components/chat-playground/chat-message.tsx index e5d621c81..3e3211dd7 100644 --- a/llama_stack/ui/components/chat-playground/chat-message.tsx +++ b/llama_stack/ui/components/chat-playground/chat-message.tsx @@ -161,10 +161,10 @@ export const ChatMessage: React.FC = ({ const isUser = role === "user" - const formattedTime = createdAt?.toLocaleTimeString("en-US", { + const formattedTime = createdAt ? new Date(createdAt).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", - }) + }) : undefined if (isUser) { return ( @@ -185,7 +185,7 @@ export const ChatMessage: React.FC = ({ {showTimeStamp && createdAt ? (