From 228af61155e581a2b12909850940e98ec0dbec2f Mon Sep 17 00:00:00 2001 From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:07:18 +0100 Subject: [PATCH] feat: frontend implementation for Access Endpoint (#4914) Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com> --- .../frontend/src/features/chat/chat-page.tsx | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index cf1ba11d7b..5d1c31d9e6 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -8,6 +8,14 @@ import { } from "@/components/assistant-ui/model-selector"; import { Thread } from "@/components/assistant-ui/thread"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; import { Sheet, SheetContent, @@ -33,6 +41,7 @@ import { Settings04Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; +import { code as codePlugin } from "@streamdown/code"; import { type CSSProperties, type ReactElement, @@ -44,6 +53,7 @@ import { useRef, useState, } from "react"; +import { Streamdown } from "streamdown"; import { toast } from "sonner"; import { listLocalModels } from "./api/chat-api"; import { ChatSettingsPanel } from "./chat-settings-sheet"; @@ -73,6 +83,42 @@ type LoraCandidate = { updatedAt?: number; }; +const ENDPOINT_BASE_URL_FALLBACK = "http://127.0.0.1:8001/v1"; +const SHIKI_THEME = ["github-light", "github-dark"] as ["github-light", "github-dark"]; + +function buildDefaultEndpointBaseUrl(): string { + if (typeof window === "undefined") { + return ENDPOINT_BASE_URL_FALLBACK; + } + return `${window.location.origin}/v1`; +} + +function HighlightedSnippet({ + language, + source, +}: { + language: "python" | "bash"; + source: string; +}) { + const markdown = useMemo( + () => `\`\`\`${language}\n${source}\n\`\`\``, + [language, source], + ); + + return ( +
+ + {markdown} + +
+ ); +} + function normalizeModelRef(value: string | null | undefined): string { return value?.trim().toLowerCase() ?? ""; } @@ -497,6 +543,11 @@ export function ChatPage(): ReactElement { // explicitly sets a nonce in handleNewThread. const [view, setView] = useState(getInitialSingleChatView); const [settingsOpen, setSettingsOpen] = useState(false); + const [endpointDialogOpen, setEndpointDialogOpen] = useState(false); + const [endpointBaseUrl, setEndpointBaseUrl] = useState( + buildDefaultEndpointBaseUrl, + ); + const [endpointApiKey, setEndpointApiKey] = useState("sk-no-key-required"); const [modelSelectorOpen, setModelSelectorOpen] = useState(false); const [modelSelectorLocked, setModelSelectorLocked] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(true); @@ -537,6 +588,47 @@ export function ChatPage(): ReactElement { const canCompare = useMemo(() => { return Boolean(inferenceParams.checkpoint); }, [inferenceParams.checkpoint]); + const modelAlias = inferenceParams.checkpoint || "unsloth/your-model-alias"; + const endpointPythonSnippet = useMemo( + () => `from openai import OpenAI + +client = OpenAI( + base_url="${endpointBaseUrl}", + api_key="${endpointApiKey}", +) + +completion = client.chat.completions.create( + model="${modelAlias}", + messages=[{"role": "user", "content": "What is 2+2?"}], +) + +print(completion.choices[0].message.content)`, + [endpointApiKey, endpointBaseUrl, modelAlias], + ); + const endpointCurlSnippet = useMemo( + () => `curl ${endpointBaseUrl}/chat/completions \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer ${endpointApiKey}" \\ + -d '{ + "model": "${modelAlias}", + "messages": [{"role": "user", "content": "What is 2+2?"}] + }'`, + [endpointApiKey, endpointBaseUrl, modelAlias], + ); + + useEffect(() => { + if (typeof window === "undefined") return; + setEndpointBaseUrl((prev) => + prev === ENDPOINT_BASE_URL_FALLBACK ? buildDefaultEndpointBaseUrl() : prev, + ); + }, []); + + useEffect(() => { + if (!endpointDialogOpen) return; + setEndpointBaseUrl((prev) => + prev.trim().length === 0 ? buildDefaultEndpointBaseUrl() : prev, + ); + }, [endpointDialogOpen]); const handleCheckpointChange = useCallback( ( @@ -641,6 +733,13 @@ export function ChatPage(): ReactElement { const openSettings = useCallback(() => setSettingsOpen(true), []); const closeSettings = useCallback(() => setSettingsOpen(false), []); const openSidebar = useCallback(() => setSidebarOpen(true), []); + const handleOpenEndpointDialog = useCallback(() => { + if (!inferenceParams.checkpoint) { + toast.message("Load a model first to access endpoint details."); + return; + } + setEndpointDialogOpen(true); + }, [inferenceParams.checkpoint]); const enterCompare = useCallback(() => { setViewBeforeCompare((prev) => prev ?? view); @@ -932,6 +1031,17 @@ export function ChatPage(): ReactElement { completionTokens={contextUsage.completionTokens} /> ) : null} + {inferenceParams.checkpoint ? ( + + ) : null}