[Feat] UI - Show "get code" section for prompt management + minor polish of showing version history (#16941)

* add _get_prompt_data_from_dotprompt_content

* fix pre call hook for prompt template

* fix: get_latest_version_prompt_id

* fix get_latest_version_prompt_id

* test_get_latest_version_prompt_id

* fx info and delete lookup for prompts

* refactor prompt table

* - rename to prompt studio

* fix get_prompt_info

* fix endpoints

* add PromptCodeSnippets

* prompt info view

* add prompt info view

* show correct version for prompts

* fix version selector

* fix endpoints and version

* fix get_prompt_info

* fix version display
This commit is contained in:
Ishaan Jaff 2025-11-21 14:00:33 -08:00 committed by GitHub
parent c9ac1949ee
commit fb38763eb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 555 additions and 31 deletions

View file

@ -38,7 +38,7 @@ def get_base_prompt_id(prompt_id: str) -> str:
Extract the base prompt ID by stripping the version suffix if present.
Args:
prompt_id: Prompt ID that may include version suffix (e.g., "jack_success.v1")
prompt_id: Prompt ID that may include version suffix (e.g., "jack_success.v1" or "jack_success_v1")
Returns:
Base prompt ID without version suffix (e.g., "jack_success")
@ -46,10 +46,18 @@ def get_base_prompt_id(prompt_id: str) -> str:
Examples:
>>> get_base_prompt_id("jack_success.v1")
"jack_success"
>>> get_base_prompt_id("jack_success_v1")
"jack_success"
>>> get_base_prompt_id("jack_success")
"jack_success"
"""
return prompt_id.split(".v")[0] if ".v" in prompt_id else prompt_id
# Try dot separator first (.v)
if ".v" in prompt_id:
return prompt_id.split(".v")[0]
# Try underscore separator (_v)
if "_v" in prompt_id:
return prompt_id.split("_v")[0]
return prompt_id
def get_version_number(prompt_id: str) -> int:
@ -57,7 +65,7 @@ def get_version_number(prompt_id: str) -> int:
Extract the version number from a versioned prompt ID.
Args:
prompt_id: Prompt ID that may include version suffix (e.g., "jack_success.v2")
prompt_id: Prompt ID that may include version suffix (e.g., "jack_success.v2" or "jack_success_v2")
Returns:
Version number (defaults to 1 if no version suffix or invalid format)
@ -65,15 +73,27 @@ def get_version_number(prompt_id: str) -> int:
Examples:
>>> get_version_number("jack_success.v2")
2
>>> get_version_number("jack_success_v2")
2
>>> get_version_number("jack_success")
1
"""
# Try dot separator first (.v)
if ".v" in prompt_id:
version_str = prompt_id.split(".v")[1]
try:
return int(version_str)
except ValueError:
return 1
pass
# Try underscore separator (_v)
if "_v" in prompt_id:
version_str = prompt_id.split("_v")[1]
try:
return int(version_str)
except ValueError:
pass
return 1
@ -403,10 +423,31 @@ async def get_prompt_versions(
status_code=404, detail=f"No versions found for prompt ID {base_prompt_id}"
)
# Sort by version number (descending - newest first)
prompt_versions.sort(key=lambda p: get_version_number(prompt_id=p.prompt_id), reverse=True)
# Create response with explicit version field for each prompt
versioned_prompts = []
for prompt in prompt_versions:
# Extract version number from the root prompt_id which has version suffix
# (e.g., "jack-sparrow.v3" -> 3)
version_number = get_version_number(prompt_id=prompt.prompt_id)
# Strip version from prompt_id for clean display
base_prompt_id = get_base_prompt_id(prompt_id=prompt.prompt_id)
# Create a copy with explicit version field and clean prompt_id
versioned_prompt = PromptSpec(
prompt_id=base_prompt_id, # Clean ID without version (e.g., "jack-sparrow")
litellm_params=prompt.litellm_params,
prompt_info=prompt.prompt_info,
created_at=prompt.created_at,
updated_at=prompt.updated_at,
version=version_number, # Explicit version field (e.g., 3)
)
versioned_prompts.append(versioned_prompt)
return ListPromptsResponse(prompts=prompt_versions)
# Sort by version number (descending - newest first)
versioned_prompts.sort(key=lambda p: p.version or 1, reverse=True)
return ListPromptsResponse(prompts=versioned_prompts)
@router.get(
@ -489,6 +530,20 @@ async def get_prompt_info(
if prompt_spec is None:
raise HTTPException(status_code=400, detail=f"Prompt {prompt_id} not found")
# Extract version number from the prompt_id
version_number = get_version_number(prompt_id=prompt_spec.prompt_id)
# Create a copy of the prompt spec with the base prompt ID (stripped of version)
# and explicit version field for consistency with list_prompts and versions endpoints
prompt_spec_response = PromptSpec(
prompt_id=get_base_prompt_id(prompt_id=prompt_spec.prompt_id),
litellm_params=prompt_spec.litellm_params, # This preserves the versioned ID
prompt_info=prompt_spec.prompt_info,
created_at=prompt_spec.created_at,
updated_at=prompt_spec.updated_at,
version=version_number, # Explicit version field
)
# Get prompt content from the callback
prompt_template: Optional[PromptTemplateBase] = None
try:
@ -519,7 +574,7 @@ async def get_prompt_info(
# Create response with content
return PromptInfoResponse(
prompt_spec=prompt_spec,
prompt_spec=prompt_spec_response,
raw_prompt_template=prompt_template,
)

View file

@ -37,6 +37,7 @@ class PromptSpec(BaseModel):
prompt_info: PromptInfo
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
version: Optional[int] = None # Version number for version history
def __init__(self, **data):
if "prompt_info" not in data:

View file

@ -124,6 +124,7 @@ export interface PromptSpec {
prompt_info: PromptInfo;
created_at?: string;
updated_at?: string;
version?: number; // Explicit version number for version history
}
export interface PromptTemplateBase {

View file

@ -0,0 +1,284 @@
import React, { useState } from "react";
import { Modal, Select, Button as AntdButton, Tabs } from "antd";
import { CodeOutlined } from "@ant-design/icons";
import { Button as TremorButton, Text } from "@tremor/react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
import NotificationsManager from "../../molecules/notifications_manager";
interface PromptCodeSnippetsProps {
promptId: string;
model: string;
promptVariables?: Record<string, string>;
accessToken: string | null;
version?: string;
proxySettings?: {
PROXY_BASE_URL?: string;
LITELLM_UI_API_DOC_BASE_URL?: string | null;
};
}
const PromptCodeSnippets: React.FC<PromptCodeSnippetsProps> = ({
promptId,
model,
promptVariables = {},
accessToken,
version = "1",
proxySettings,
}) => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedLanguage, setSelectedLanguage] = useState<"curl" | "python" | "javascript">("curl");
const [selectedTab, setSelectedTab] = useState("basic");
const [generatedCode, setGeneratedCode] = useState("");
const showModal = () => {
setIsModalVisible(true);
};
const handleCancel = () => {
setIsModalVisible(false);
};
// Determine base URL with priority: LITELLM_UI_API_DOC_BASE_URL > PROXY_BASE_URL > window.location.origin
let apiBase = window.location.origin;
const customDocBaseUrl = proxySettings?.LITELLM_UI_API_DOC_BASE_URL;
if (customDocBaseUrl && customDocBaseUrl.trim()) {
apiBase = customDocBaseUrl;
} else if (proxySettings?.PROXY_BASE_URL) {
apiBase = proxySettings.PROXY_BASE_URL;
}
const effectiveApiKey = accessToken || "sk-1234";
// Generate code based on selected language and tab
const generateCode = () => {
const hasVariables = Object.keys(promptVariables).length > 0;
if (selectedLanguage === "curl") {
if (selectedTab === "basic") {
return `curl -X POST '${apiBase}/chat/completions' \\
-H 'Content-Type: application/json' \\
-H 'Authorization: Bearer ${effectiveApiKey}' \\
-d '{
"model": "${model}",
"prompt_id": "${promptId}"${hasVariables ? `,
"prompt_variables": ${JSON.stringify(promptVariables, null, 6).replace(/\n/g, '\n ')}` : ''}
}' | jq`;
} else if (selectedTab === "messages") {
return `curl -X POST '${apiBase}/chat/completions' \\
-H 'Content-Type: application/json' \\
-H 'Authorization: Bearer ${effectiveApiKey}' \\
-d '{
"model": "${model}",
"prompt_id": "${promptId}"${hasVariables ? `,
"prompt_variables": ${JSON.stringify(promptVariables, null, 6).replace(/\n/g, '\n ')}` : ''},
"messages": [
{
"role": "user",
"content": "hi"
}
]
}' | jq`;
} else {
return `curl -X POST '${apiBase}/chat/completions' \\
-H 'Content-Type: application/json' \\
-H 'Authorization: Bearer ${effectiveApiKey}' \\
-d '{
"model": "${model}",
"prompt_id": "${promptId}",
"prompt_version": ${version},
"messages": [
{
"role": "user",
"content": "Who are u"
}
]
}' | jq`;
}
} else if (selectedLanguage === "python") {
const importCode = `import openai
client = openai.OpenAI(
api_key="${effectiveApiKey}",
base_url="${apiBase}"
)
`;
if (selectedTab === "basic") {
return `${importCode}
response = client.chat.completions.create(
model="${model}",
extra_body={
"prompt_id": "${promptId}"${hasVariables ? `,
"prompt_variables": ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, '\n ')}` : ''}
}
)
print(response)`;
} else if (selectedTab === "messages") {
return `${importCode}
response = client.chat.completions.create(
model="${model}",
messages=[
{"role": "user", "content": "hi"}
],
extra_body={
"prompt_id": "${promptId}"${hasVariables ? `,
"prompt_variables": ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, '\n ')}` : ''}
}
)
print(response)`;
} else {
return `${importCode}
response = client.chat.completions.create(
model="${model}",
messages=[
{"role": "user", "content": "Who are u"}
],
extra_body={
"prompt_id": "${promptId}",
"prompt_version": ${version}
}
)
print(response)`;
}
} else {
// JavaScript/Node.js
const importCode = `import OpenAI from 'openai';
const client = new OpenAI({
apiKey: "${effectiveApiKey}",
baseURL: "${apiBase}"
});
`;
if (selectedTab === "basic") {
return `${importCode}
async function main() {
const response = await client.chat.completions.create({
model: "${model}",
${hasVariables ? `prompt_id: "${promptId}",
prompt_variables: ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, '\n ')}` : `prompt_id: "${promptId}"`}
});
console.log(response);
}
main();`;
} else if (selectedTab === "messages") {
return `${importCode}
async function main() {
const response = await client.chat.completions.create({
model: "${model}",
messages: [
{ role: "user", content: "hi" }
],
${hasVariables ? `prompt_id: "${promptId}",
prompt_variables: ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, '\n ')}` : `prompt_id: "${promptId}"`}
});
console.log(response);
}
main();`;
} else {
return `${importCode}
async function main() {
const response = await client.chat.completions.create({
model: "${model}",
messages: [
{ role: "user", content: "Who are u" }
],
prompt_id: "${promptId}",
prompt_version: ${version}
});
console.log(response);
}
main();`;
}
}
};
// Update generated code when language, tab or props change
React.useEffect(() => {
if (isModalVisible) {
setGeneratedCode(generateCode());
}
}, [isModalVisible, selectedLanguage, selectedTab, promptId, model, promptVariables]);
return (
<>
<TremorButton
variant="secondary"
icon={CodeOutlined}
onClick={showModal}
>
Get Code
</TremorButton>
<Modal
title="Generated Code"
open={isModalVisible}
onCancel={handleCancel}
footer={null}
width={800}
>
<div className="flex justify-between items-center mb-4">
<div>
<Text className="font-medium block mb-1 text-gray-700">Language</Text>
<Select
value={selectedLanguage}
onChange={(value) => setSelectedLanguage(value as "curl" | "python" | "javascript")}
style={{ width: 180 }}
options={[
{ value: "curl", label: "cURL" },
{ value: "python", label: "Python (OpenAI SDK)" },
{ value: "javascript", label: "JavaScript (OpenAI SDK)" },
]}
/>
</div>
<AntdButton
onClick={() => {
navigator.clipboard.writeText(generatedCode);
NotificationsManager.success("Copied to clipboard!");
}}
>
Copy to Clipboard
</AntdButton>
</div>
<Tabs
activeKey={selectedTab}
onChange={setSelectedTab}
items={[
{ label: "Basic", key: "basic" },
{ label: "With Messages", key: "messages" },
{ label: "With Version", key: "version" },
]}
/>
<SyntaxHighlighter
language={selectedLanguage === "curl" ? "bash" : selectedLanguage === "python" ? "python" : "javascript"}
style={coy as any}
wrapLines={true}
wrapLongLines={true}
className="rounded-md mt-0"
customStyle={{
maxHeight: "60vh",
overflowY: "auto",
marginTop: 0,
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
}}
>
{generatedCode}
</SyntaxHighlighter>
</Modal>
</>
);
};
export default PromptCodeSnippets;

View file

@ -2,6 +2,7 @@ import React from "react";
import { Button as TremorButton } from "@tremor/react";
import { Input } from "antd";
import { ArrowLeftIcon, SaveIcon, ClockIcon } from "lucide-react";
import PromptCodeSnippets from "./PromptCodeSnippets";
interface PromptEditorHeaderProps {
promptName: string;
@ -12,6 +13,13 @@ interface PromptEditorHeaderProps {
editMode?: boolean;
onShowHistory?: () => void;
version?: string | null;
promptModel?: string;
promptVariables?: Record<string, string>;
accessToken: string | null;
proxySettings?: {
PROXY_BASE_URL?: string;
LITELLM_UI_API_DOC_BASE_URL?: string | null;
};
}
const PromptEditorHeader: React.FC<PromptEditorHeaderProps> = ({
@ -23,6 +31,10 @@ const PromptEditorHeader: React.FC<PromptEditorHeaderProps> = ({
editMode = false,
onShowHistory,
version,
promptModel = "gpt-4o",
promptVariables = {},
accessToken,
proxySettings,
}) => {
return (
<div className="bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between">
@ -45,6 +57,14 @@ const PromptEditorHeader: React.FC<PromptEditorHeaderProps> = ({
<span className="text-xs text-gray-400">Unsaved changes</span>
</div>
<div className="flex items-center space-x-2">
<PromptCodeSnippets
promptId={promptName}
model={promptModel}
promptVariables={promptVariables}
accessToken={accessToken}
version={version?.replace('v', '') || "1"}
proxySettings={proxySettings}
/>
{editMode && onShowHistory && (
<TremorButton
icon={ClockIcon}

View file

@ -44,9 +44,19 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
}
};
const getVersionNumber = (pid: string) => {
if (pid.includes(".v")) {
return `v${pid.split(".v")[1]}`;
const getVersionNumber = (prompt: PromptSpec) => {
// Use explicit version field if available, otherwise try to extract from litellm_params.prompt_id
if (prompt.version) {
return `v${prompt.version}`;
}
// Fallback: try to extract from litellm_params.prompt_id
const versionedId = (prompt.litellm_params as any)?.prompt_id || prompt.prompt_id;
if (versionedId.includes(".v")) {
return `v${versionedId.split(".v")[1]}`;
}
if (versionedId.includes("_v")) {
return `v${versionedId.split("_v")[1]}`;
}
return "v1";
};
@ -74,10 +84,25 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
<List
dataSource={versions}
renderItem={(item, index) => {
const isSelected = item.prompt_id === (activeVersionId || promptId);
// Use version field for comparison since all items have the same prompt_id
const itemVersionNum = item.version || parseInt(getVersionNumber(item).replace('v', ''));
// Extract version number from activeVersionId (may have .vX suffix)
let activeVersionNum: number | null = null;
if (activeVersionId) {
if (activeVersionId.includes('.v')) {
activeVersionNum = parseInt(activeVersionId.split('.v')[1]);
} else if (activeVersionId.includes('_v')) {
activeVersionNum = parseInt(activeVersionId.split('_v')[1]);
}
}
// Default to latest (first item) if no activeVersionId
const isSelected = activeVersionNum ? itemVersionNum === activeVersionNum : index === 0;
return (
<div
key={item.prompt_id}
key={`${item.prompt_id}-v${item.version || itemVersionNum}`}
className={`mb-4 p-4 rounded-lg border cursor-pointer transition-all hover:shadow-md ${
isSelected ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white hover:border-blue-300"
}`}
@ -85,12 +110,10 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
>
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
<Tag className="m-0">{getVersionNumber(item.prompt_id)}</Tag>
{index === 0 && (
<Tag color="blue" className="m-0">
Latest
</Tag>
)}
<Tag className="m-0">
{getVersionNumber(item)}
</Tag>
{index === 0 && <Tag color="blue" className="m-0">Latest</Tag>}
</div>
{isSelected && (
<Tag color="green" className="m-0">

View file

@ -45,7 +45,28 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
const [prompt, setPrompt] = useState<PromptType>(getInitialPrompt());
const [editMode, setEditMode] = useState<boolean>(!!initialPromptData);
const [showHistoryModal, setShowHistoryModal] = useState(false);
const [activeVersionId, setActiveVersionId] = useState<string | undefined>(initialPromptData?.prompt_spec?.prompt_id);
// Construct versioned ID from prompt_id and version field
const getInitialVersionId = () => {
if (!initialPromptData?.prompt_spec) return undefined;
const baseId = initialPromptData.prompt_spec.prompt_id;
const version = initialPromptData.prompt_spec.version ||
(initialPromptData.prompt_spec.litellm_params as any)?.prompt_id;
// If version is a number, construct versioned ID
if (typeof version === 'number') {
return `${baseId}.v${version}`;
}
// If version is a string with version suffix, use it
if (typeof version === 'string' && (version.includes('.v') || version.includes('_v'))) {
return version;
}
return baseId;
};
const [activeVersionId, setActiveVersionId] = useState<string | undefined>(getInitialVersionId());
const [showToolModal, setShowToolModal] = useState(false);
const [showNameModal, setShowNameModal] = useState(false);
@ -144,8 +165,10 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
try {
const loadedPrompt = parseExistingPrompt({ prompt_spec: versionData });
setPrompt(loadedPrompt);
setActiveVersionId(versionData.prompt_id);
// NotificationsManager.success(`Loaded version ${versionData.prompt_id}`);
// Store the version number or construct versioned ID for tracking
const versionNum = versionData.version || 1;
setActiveVersionId(`${versionData.prompt_id}.v${versionNum}`);
// NotificationsManager.success(`Loaded version v${versionNum}`);
} catch (error) {
console.error("Error loading version:", error);
NotificationsManager.fromBackend("Failed to load prompt version");
@ -216,6 +239,25 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
const currentVersion = getVersionNumber(activeVersionId);
// Extract template variables from prompt content for code examples
const extractTemplateVariables = (): Record<string, string> => {
const variables: Record<string, string> = {};
const allContent = [
prompt.developerMessage,
...prompt.messages.map(m => m.content)
].join(' ');
const variableRegex = /\{\{(\w+)\}\}/g;
let match;
while ((match = variableRegex.exec(allContent)) !== null) {
const varName = match[1];
if (!variables[varName]) {
variables[varName] = `example_${varName}`;
}
}
return variables;
};
return (
<div className="flex h-full bg-white">
<div className="flex-1 flex flex-col">
@ -228,6 +270,9 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
editMode={editMode}
onShowHistory={() => setShowHistoryModal(true)}
version={currentVersion}
promptModel={prompt.model}
promptVariables={extractTemplateVariables()}
accessToken={accessToken}
/>
<div className="flex-1 flex overflow-hidden">

View file

@ -146,8 +146,12 @@ export const parseExistingPrompt = (apiResponse: any): PromptType => {
const tools: Tool[] = [];
// TODO: Add tool parsing if needed
// Strip version suffix from prompt name for display
const promptId = apiResponse?.prompt_spec?.prompt_id || "Unnamed Prompt";
const baseName = stripVersionFromPromptId(promptId) || promptId;
return {
name: apiResponse?.prompt_spec?.prompt_id || "Unnamed Prompt",
name: baseName,
model: metadata.model || "gpt-4o",
config: {
temperature: metadata.temperature,
@ -159,3 +163,16 @@ export const parseExistingPrompt = (apiResponse: any): PromptType => {
messages: messages.length > 0 ? messages : [{ role: "user", content: "Enter task specifics. Use {{template_variables}} for dynamic inputs" }],
};
};
export const getVersionNumber = (promptId?: string): string => {
if (!promptId) return "1";
// Match version with dot (.v), underscore (_v), or hyphen (-v) separator
const match = promptId.match(/[._-]v(\d+)$/);
return match ? match[1] : "1";
};
export const stripVersionFromPromptId = (promptId?: string): string => {
if (!promptId) return "";
// Remove version suffix with dot (.v), underscore (_v), or hyphen (-v) separator
return promptId.replace(/[._-]v\d+$/, "");
};

View file

@ -18,6 +18,13 @@ import { getPromptInfo, PromptSpec, PromptTemplateBase, deletePromptCall } from
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { CheckIcon, CopyIcon } from "lucide-react";
import NotificationsManager from "../molecules/notifications_manager";
import PromptCodeSnippets from "./prompt_editor_view/PromptCodeSnippets";
import {
extractModel,
extractTemplateVariables,
getBasePromptId,
getCurrentVersion
} from "./prompt_utils";
export interface PromptInfoProps {
promptId: string;
@ -91,8 +98,8 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
setIsDeleting(true);
try {
await deletePromptCall(accessToken, promptData.prompt_id);
NotificationsManager.success(`Prompt "${promptData.prompt_id}" deleted successfully`);
await deletePromptCall(accessToken, basePromptId);
NotificationsManager.success(`Prompt "${basePromptId}" deleted successfully`);
onDelete?.(); // Call the callback to refresh the parent component
onClose(); // Close the info view
} catch (error) {
@ -108,6 +115,11 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
setShowDeleteConfirm(false);
};
// Use utility functions to extract prompt data
const promptModel = promptData ? extractModel(promptData) || "gpt-4o" : "gpt-4o";
const basePromptId = getBasePromptId(promptData);
const currentVersion = getCurrentVersion(promptData);
return (
<div className="p-4">
<div>
@ -118,12 +130,12 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
<div>
<Title>Prompt Details</Title>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">{promptData.prompt_id}</Text>
<Text className="text-gray-500 font-mono">{basePromptId}</Text>
<Button
type="text"
size="small"
icon={copiedStates["prompt-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(promptData.prompt_id, "prompt-id")}
onClick={() => copyToClipboard(basePromptId, "prompt-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["prompt-id"]
? "text-green-600 bg-green-50 border-green-200"
@ -133,13 +145,20 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
</div>
</div>
<div className="flex gap-2">
<PromptCodeSnippets
promptId={basePromptId}
model={promptModel}
promptVariables={extractTemplateVariables(promptTemplate?.content)}
accessToken={accessToken}
version={currentVersion}
/>
<TremorButton
icon={PencilIcon}
variant="primary"
onClick={() => onEdit?.(rawApiResponse)}
className="flex items-center"
>
Edit Prompt
Prompt Studio
</TremorButton>
{isAdmin && (
<TremorButton
@ -170,7 +189,17 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
<Card>
<Text>Prompt ID</Text>
<div className="mt-2">
<Title className="font-mono text-sm">{promptData.prompt_id}</Title>
<Title className="font-mono text-sm">{basePromptId}</Title>
</div>
</Card>
<Card>
<Text>Version</Text>
<div className="mt-2">
<Title>{currentVersion}</Title>
<Badge color="blue" className="mt-1">
v{currentVersion}
</Badge>
</div>
</Card>
@ -262,7 +291,7 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
<div className="space-y-4">
<div>
<Text className="font-medium">Prompt ID</Text>
<div className="font-mono text-sm bg-gray-50 p-2 rounded">{promptData.prompt_id}</div>
<div className="font-mono text-sm bg-gray-50 p-2 rounded">{basePromptId}</div>
</div>
<div>
@ -343,7 +372,7 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
okButtonProps={{ danger: true }}
>
<p>
Are you sure you want to delete prompt: <strong>{promptData?.prompt_id}</strong>?
Are you sure you want to delete prompt: <strong>{basePromptId}</strong>?
</p>
<p>This action cannot be undone.</p>
</Modal>

View file

@ -1,4 +1,5 @@
import { PromptSpec } from "@/components/networking";
import { getVersionNumber } from "./prompt_editor_view/utils";
interface ModelGroupInfo {
model_group: string;
@ -6,6 +7,54 @@ interface ModelGroupInfo {
[key: string]: any;
}
/**
* Extract template variables from prompt content
*/
export const extractTemplateVariables = (content?: string): Record<string, string> => {
if (!content) return {};
const variables: Record<string, string> = {};
const variableRegex = /\{\{(\w+)\}\}/g;
let match;
while ((match = variableRegex.exec(content)) !== null) {
const varName = match[1];
if (!variables[varName]) {
variables[varName] = `example_${varName}`;
}
}
return variables;
};
/**
* Get base prompt ID (stripped of version) from PromptSpec
*/
export const getBasePromptId = (promptData?: PromptSpec): string => {
return promptData?.prompt_id || "";
};
/**
* Get versioned prompt ID from litellm_params (preserves version)
*/
export const getVersionedPromptId = (promptData?: PromptSpec): string => {
const baseId = getBasePromptId(promptData);
const versionedId = (promptData?.litellm_params as any)?.prompt_id || baseId;
return versionedId;
};
/**
* Get current version number from prompt data
*/
export const getCurrentVersion = (promptData?: PromptSpec): string => {
// Use explicit version field if available (from API response)
if (promptData?.version) {
return String(promptData.version);
}
// Fallback: extract from versioned ID in litellm_params
const versionedId = getVersionedPromptId(promptData);
return getVersionNumber(versionedId);
};
/**
* Extract model from prompt litellm_params
*/