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 9c5458f28..90780ab23 100644 --- a/llama_stack/ui/app/chat-playground/page.tsx +++ b/llama_stack/ui/app/chat-playground/page.tsx @@ -17,9 +17,20 @@ 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); @@ -35,6 +46,26 @@ export default function ChatPlaygroundPage() { 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 () => { try { @@ -43,8 +74,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); @@ -108,9 +139,9 @@ export default function ChatPlaygroundPage() { setInput(e.target.value); }; - const handleSubmit = async (event?: { preventDefault?: () => void }) => { - event?.preventDefault?.(); - if (!input.trim()) return; +const handleSubmit = async (event?: { preventDefault?: () => void }) => { + event?.preventDefault?.(); + if (!input.trim() || !currentSession || !currentSession.selectedModel) return; // Add user message to chat const userMessage: Message = { @@ -120,8 +151,12 @@ export default function ChatPlaygroundPage() { createdAt: new Date(), }; - setMessages(prev => [...prev, userMessage]); - setInput(""); + setCurrentSession(prev => prev ? { + ...prev, + messages: [...prev.messages, userMessage], + updatedAt: Date.now() + } : null); + setInput(""); // Use the helper function with the content await handleSubmitWithContent(userMessage.content); @@ -135,11 +170,11 @@ export default function ChatPlaygroundPage() { 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) { @@ -162,7 +197,7 @@ export default function ChatPlaygroundPage() { } 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 }; @@ -175,11 +210,11 @@ export default function ChatPlaygroundPage() { { role: "user" as const, content: enhancedContent } ]; - const response = await client.chat.completions.create({ - model: selectedModel, - messages: messageParams, - stream: true, - }); + const response = await client.chat.completions.create({ + model: currentSession?.selectedModel || "", + messages: messageParams, + stream: true, + }); const assistantMessage: Message = { id: (Date.now() + 1).toString(), @@ -188,33 +223,43 @@ export default function ChatPlaygroundPage() { createdAt: new Date(), }; - setMessages(prev => [...prev, assistantMessage]); - let fullContent = ""; - for await (const chunk of response) { - if (chunk.choices && chunk.choices[0]?.delta?.content) { - const deltaContent = chunk.choices[0].delta.content; - fullContent += deltaContent; + setCurrentSession(prev => prev ? { + ...prev, + messages: [...prev.messages, assistantMessage], + updatedAt: Date.now() + } : null); - flushSync(() => { - setMessages(prev => { - const newMessages = [...prev]; - const lastMessage = newMessages[newMessages.length - 1]; - if (lastMessage.role === "assistant") { - lastMessage.content = fullContent; - } - return newMessages; - }); + let fullContent = ""; + for await (const chunk of response) { + if (chunk.choices && chunk.choices[0]?.delta?.content) { + const deltaContent = chunk.choices[0].delta.content; + fullContent += deltaContent; + + flushSync(() => { + setCurrentSession(prev => { + if (!prev) return null; + const newMessages = [...prev.messages]; + const lastMessage = newMessages[newMessages.length - 1]; + if (lastMessage.role === "assistant") { + lastMessage.content = fullContent; + } + return { ...prev, messages: newMessages, updatedAt: Date.now() }; }); - } + }); } - } catch (err) { - console.error("Error sending message:", err); - setError("Failed to send message. Please try again."); - setMessages(prev => prev.slice(0, -1)); - } finally { - setIsGenerating(false); } - }; + } catch (err) { + console.error("Error sending message:", err); + setError("Failed to send message. Please try again."); + setCurrentSession(prev => prev ? { + ...prev, + messages: prev.messages.slice(0, -1), + updatedAt: Date.now() + } : null); + } finally { + setIsGenerating(false); + } +}; const suggestions = [ "Write a Python function that prints 'Hello, World!'", "Explain step-by-step how to solve this math problem: If x² + 6x + 9 = 25, what is x?", @@ -228,15 +273,41 @@ export default function ChatPlaygroundPage() { 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); @@ -252,51 +323,70 @@ export default function ChatPlaygroundPage() { }; return ( -
+
-

Chat Playground (Completions)

-
- - - +

Chat Playground

+
+ +
+
+ +
+
+ Model: + +
+ +
+ Vector DB: + +
+ + +
{modelsError && ( @@ -319,14 +409,14 @@ export default function ChatPlaygroundPage() { 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 84c798e29..2287bb66d 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 ( @@ -220,7 +220,7 @@ export const ChatMessage: React.FC = ({ {showTimeStamp && createdAt ? (