mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
Prompt Management - Add table + prompt info page to UI (#13232)
* fix(create_key_button.tsx): add prompts on UI * feat(key_management_endpoints.py): support adding prompt to key via `/key/update` * fix(key_info_view.tsx): show existing prompts on key in key_info_view.tsx * fix(key_edit_view.tsx): UX - disable premium feature for non-premium users prevent accidental clicking * fix(create_key_button.tsx): disable premium features behind flag, prevent errors * feat(prompts.tsx): add new ui component to view created prompts enables viewing prompts created on config * feat(prompt_info.tsx): add component for viewing the prompt information
This commit is contained in:
parent
b79f55eec0
commit
e47b30a76d
14 changed files with 662 additions and 11 deletions
|
|
@ -514,6 +514,7 @@ class LiteLLMRoutes(enum.Enum):
|
|||
"/user/daily/activity",
|
||||
"/model/{model_id}/update",
|
||||
"/prompt/list",
|
||||
"/prompt/info",
|
||||
] # routes that manage their own allowed/disallowed logic
|
||||
|
||||
## Org Admin Routes ##
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Any, Optional, Union
|
||||
|
||||
from litellm.proxy._types import (
|
||||
GenerateKeyRequest,
|
||||
KeyRequestBase,
|
||||
LiteLLM_ManagementEndpoint_MetadataFields_Premium,
|
||||
LiteLLM_TeamTable,
|
||||
|
|
|
|||
|
|
@ -855,6 +855,15 @@ async def prepare_key_update_data(
|
|||
data: Union[UpdateKeyRequest, RegenerateKeyRequest],
|
||||
existing_key_row: LiteLLM_VerificationToken,
|
||||
):
|
||||
for field in LiteLLM_ManagementEndpoint_MetadataFields_Premium:
|
||||
if getattr(data, field, None) is not None:
|
||||
_set_object_metadata_field(
|
||||
object_data=data,
|
||||
field_name=field,
|
||||
value=getattr(data, field),
|
||||
)
|
||||
delattr(data, field)
|
||||
|
||||
data_json: dict = data.model_dump(exclude_unset=True)
|
||||
data_json.pop("key", None)
|
||||
data_json.pop("new_key", None)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ CRUD ENDPOINTS FOR PROMPTS
|
|||
|
||||
from typing import List, Optional, cast
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.types.prompts.init_prompts import ListPromptsResponse
|
||||
from litellm.types.prompts.init_prompts import ListPromptsResponse, PromptSpec
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -50,3 +50,43 @@ async def list_prompts(
|
|||
)
|
||||
else:
|
||||
return ListPromptsResponse(prompts=[])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/prompt/info",
|
||||
tags=["Prompt Management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PromptSpec,
|
||||
)
|
||||
async def get_prompt(
|
||||
prompt_id: str,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Get info about a prompt
|
||||
"""
|
||||
from litellm.proxy.prompts.prompt_registry import IN_MEMORY_PROMPT_REGISTRY
|
||||
|
||||
## CHECK IF USER HAS ACCESS TO PROMPT
|
||||
prompts: Optional[List[str]] = None
|
||||
if user_api_key_dict.metadata is not None:
|
||||
prompts = cast(
|
||||
Optional[List[str]], user_api_key_dict.metadata.get("prompts", None)
|
||||
)
|
||||
if prompts is not None and prompt_id not in prompts:
|
||||
raise HTTPException(status_code=400, detail=f"Prompt {prompt_id} not found")
|
||||
if user_api_key_dict.user_role is not None and (
|
||||
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
||||
or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
|
||||
):
|
||||
pass
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"You are not authorized to access this prompt. Your role - {user_api_key_dict.user_role}, Your key's prompts - {prompts}",
|
||||
)
|
||||
|
||||
prompt_spec = IN_MEMORY_PROMPT_REGISTRY.get_prompt_by_id(prompt_id)
|
||||
if prompt_spec is None:
|
||||
raise HTTPException(status_code=400, detail=f"Prompt {prompt_id} not found")
|
||||
return prompt_spec
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ from typing import Callable, Dict, Optional
|
|||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_prompt_management import CustomPromptManagement
|
||||
from litellm.types.prompts.init_prompts import PromptLiteLLMParams, PromptSpec
|
||||
from litellm.types.prompts.init_prompts import (
|
||||
PromptInfo,
|
||||
PromptLiteLLMParams,
|
||||
PromptSpec,
|
||||
)
|
||||
|
||||
prompt_initializer_registry = {}
|
||||
|
||||
|
|
@ -147,6 +151,7 @@ class InMemoryPromptRegistry:
|
|||
parsed_prompt = PromptSpec(
|
||||
prompt_id=prompt_id,
|
||||
litellm_params=litellm_params,
|
||||
prompt_info=PromptInfo(prompt_type="config"),
|
||||
)
|
||||
|
||||
# store references to the prompt in memory
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing_extensions import Required, TypedDict
|
||||
|
|
@ -12,6 +12,12 @@ class SupportedPromptIntegrations(str, Enum):
|
|||
CUSTOM = "custom"
|
||||
|
||||
|
||||
class PromptInfo(BaseModel):
|
||||
prompt_type: Literal["config", "db"]
|
||||
|
||||
model_config = ConfigDict(extra="allow", protected_namespaces=())
|
||||
|
||||
|
||||
class PromptLiteLLMParams(BaseModel):
|
||||
prompt_id: str
|
||||
prompt_integration: str
|
||||
|
|
@ -22,7 +28,7 @@ class PromptLiteLLMParams(BaseModel):
|
|||
class PromptSpec(TypedDict, total=False):
|
||||
prompt_id: Required[str]
|
||||
litellm_params: Required[PromptLiteLLMParams]
|
||||
prompt_info: Optional[Dict]
|
||||
prompt_info: Optional[PromptInfo]
|
||||
created_at: Optional[datetime]
|
||||
updated_at: Optional[datetime]
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from "@/components/networking";
|
||||
import { Organization } from "@/components/networking";
|
||||
import GuardrailsPanel from "@/components/guardrails";
|
||||
import PromptsPanel from "@/components/prompts";
|
||||
import TransformRequestPanel from "@/components/transform_request";
|
||||
import { fetchUserModels } from "@/components/create_key_button";
|
||||
import { fetchTeams } from "@/components/common_components/fetch_teams";
|
||||
|
|
@ -361,6 +362,11 @@ export default function CreateKeyPage() {
|
|||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
/>
|
||||
) : page == "prompts" ? (
|
||||
<PromptsPanel
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
/>
|
||||
) : page == "transform-request" ? (
|
||||
<TransformRequestPanel accessToken={accessToken} />
|
||||
) : page == "general-settings" ? (
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||
icon: <ExperimentOutlined />,
|
||||
children: [
|
||||
{ key: "9", page: "caching", label: "Caching", icon: <DatabaseOutlined />, roles: all_admin_roles },
|
||||
{ key: "25", page: "prompts", label: "Prompts", icon: <FileTextOutlined />, roles: all_admin_roles },
|
||||
{ key: "10", page: "budgets", label: "Budgets", icon: <BankOutlined />, roles: all_admin_roles },
|
||||
{ key: "20", page: "transform-request", label: "API Playground", icon: <ApiOutlined />, roles: [...all_admin_roles, ...internalUserRoles] },
|
||||
{ key: "19", page: "tag-management", label: "Tag Management", icon: <TagsOutlined />, roles: all_admin_roles },
|
||||
|
|
|
|||
|
|
@ -75,13 +75,19 @@ export interface Model {
|
|||
model_info: Object | null;
|
||||
}
|
||||
|
||||
interface PromptSpec {
|
||||
prompt_id: string;
|
||||
litellm_params: Object;
|
||||
prompt_info: Object;
|
||||
interface PromptInfo {
|
||||
prompt_type: string;
|
||||
}
|
||||
|
||||
interface ListPromptsResponse {
|
||||
export interface PromptSpec {
|
||||
prompt_id: string;
|
||||
litellm_params: Object;
|
||||
prompt_info: PromptInfo;
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export interface ListPromptsResponse {
|
||||
prompts: PromptSpec[];
|
||||
}
|
||||
|
||||
|
|
@ -4917,6 +4923,31 @@ export const getPromptsList = async (accessToken: String) : Promise<ListPromptsR
|
|||
}
|
||||
};
|
||||
|
||||
export const getPromptInfo = async (accessToken: String, promptId: string): Promise<PromptSpec> => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/prompt/info?prompt_id=${promptId}` : `/prompt/info?prompt_id=${promptId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get prompt info:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createGuardrailCall = async (
|
||||
accessToken: string,
|
||||
guardrailData: any
|
||||
|
|
|
|||
71
ui/litellm-dashboard/src/components/prompts.tsx
Normal file
71
ui/litellm-dashboard/src/components/prompts.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useState, useEffect } from "react"
|
||||
import { Card, Text } from "@tremor/react"
|
||||
import { getPromptsList, PromptSpec, ListPromptsResponse } from "./networking"
|
||||
import PromptTable from "./prompts/prompt_table"
|
||||
import PromptInfoView from "./prompts/prompt_info"
|
||||
import { isAdminRole } from "@/utils/roles"
|
||||
|
||||
interface PromptsProps {
|
||||
accessToken: string | null
|
||||
userRole?: string
|
||||
}
|
||||
|
||||
const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
const [promptsList, setPromptsList] = useState<PromptSpec[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [selectedPromptId, setSelectedPromptId] = useState<string | null>(null)
|
||||
|
||||
const isAdmin = userRole ? isAdminRole(userRole) : false
|
||||
|
||||
const fetchPrompts = async () => {
|
||||
if (!accessToken) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response: ListPromptsResponse = await getPromptsList(accessToken)
|
||||
console.log(`prompts: ${JSON.stringify(response)}`)
|
||||
setPromptsList(response.prompts)
|
||||
} catch (error) {
|
||||
console.error("Error fetching prompts:", error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrompts()
|
||||
}, [accessToken])
|
||||
|
||||
const handlePromptClick = (promptId: string) => {
|
||||
setSelectedPromptId(promptId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
{selectedPromptId ? (
|
||||
<PromptInfoView
|
||||
promptId={selectedPromptId}
|
||||
onClose={() => setSelectedPromptId(null)}
|
||||
accessToken={accessToken}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Text className="text-lg font-semibold">Prompts</Text>
|
||||
</div>
|
||||
|
||||
<PromptTable
|
||||
promptsList={promptsList}
|
||||
isLoading={isLoading}
|
||||
onPromptClick={handlePromptClick}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptsPanel
|
||||
73
ui/litellm-dashboard/src/components/prompts/README.md
Normal file
73
ui/litellm-dashboard/src/components/prompts/README.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Prompts Component
|
||||
|
||||
This component provides a view-only interface for viewing prompts in the LiteLLM dashboard, similar to the guardrails component.
|
||||
|
||||
## Components
|
||||
|
||||
### PromptsPanel (`prompts.tsx`)
|
||||
- Main component that displays the prompts list
|
||||
- Fetches prompts using the `getPromptsList` API call
|
||||
- Handles loading states and error handling
|
||||
|
||||
### PromptTable (`prompt_table.tsx`)
|
||||
- Table component that displays prompts data
|
||||
- Uses Tanstack Table for sorting and filtering
|
||||
- Shows: Prompt ID, Created At, Updated At, Type
|
||||
- Supports clicking on prompt IDs to open detailed view
|
||||
|
||||
### PromptInfoView (`prompt_info.tsx`)
|
||||
- Detail view component for individual prompts
|
||||
- Shows comprehensive prompt information including metadata and parameters
|
||||
- Three-tab interface: **Overview**, **Details** (admin-only), and **Raw JSON**
|
||||
- **Overview**: Shows formatted prompt information with key details
|
||||
- **Details**: Shows structured breakdown of all prompt data (admin users only)
|
||||
- **Raw JSON**: Shows exactly what the API returns with copy-to-clipboard functionality
|
||||
- Includes copy-to-clipboard functionality for prompt ID and raw JSON
|
||||
- Similar structure to GuardrailInfoView component
|
||||
|
||||
## Usage
|
||||
|
||||
The component is integrated into the main application at:
|
||||
- **Navigation**: Available in the left sidebar under "Experimental" > "Prompts" (admin role required)
|
||||
- **Routing**: Accessible via `?page=prompts` URL parameter
|
||||
- **API**: Uses `getPromptsList` and `getPromptInfo` functions from `networking.tsx`
|
||||
- **Detail View**: Click any prompt ID to view detailed information
|
||||
|
||||
## Props
|
||||
|
||||
```typescript
|
||||
interface PromptsProps {
|
||||
accessToken: string | null
|
||||
userRole?: string
|
||||
}
|
||||
```
|
||||
|
||||
## Data Structure
|
||||
|
||||
The component expects prompts with the following structure:
|
||||
|
||||
```typescript
|
||||
interface PromptItem {
|
||||
prompt_id?: string
|
||||
prompt_name: string | null
|
||||
prompt_info: Record<string, any>
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
The component is fully integrated into the main application:
|
||||
|
||||
1. **Left Navigation**: Added to `leftnav.tsx` with `FileTextOutlined` icon
|
||||
2. **Main Routing**: Added to `page.tsx` with proper routing logic
|
||||
3. **Permissions**: Restricted to admin roles (same as guardrails)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- ✅ Add prompt detail view (similar to GuardrailInfoView) - **COMPLETED**
|
||||
- Add create/edit/delete functionality
|
||||
- Add bulk operations
|
||||
- Add search and filtering capabilities
|
||||
- Add export functionality
|
||||
2
ui/litellm-dashboard/src/components/prompts/index.ts
Normal file
2
ui/litellm-dashboard/src/components/prompts/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as PromptTable } from "./prompt_table"
|
||||
export { default as PromptInfoView } from "./prompt_info"
|
||||
231
ui/litellm-dashboard/src/components/prompts/prompt_info.tsx
Normal file
231
ui/litellm-dashboard/src/components/prompts/prompt_info.tsx
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import React, { useState, useEffect } from "react"
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
Grid,
|
||||
Badge,
|
||||
Button as TremorButton,
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from "@tremor/react"
|
||||
import { Button, message, Tooltip } from "antd"
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline"
|
||||
import { getPromptInfo, PromptSpec } from "@/components/networking"
|
||||
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"
|
||||
import { CheckIcon, CopyIcon } from "lucide-react"
|
||||
|
||||
export interface PromptInfoProps {
|
||||
promptId: string
|
||||
onClose: () => void
|
||||
accessToken: string | null
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessToken, isAdmin }) => {
|
||||
const [promptData, setPromptData] = useState<PromptSpec | null>(null)
|
||||
const [rawApiResponse, setRawApiResponse] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({})
|
||||
|
||||
const fetchPromptInfo = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!accessToken) return
|
||||
const response = await getPromptInfo(accessToken, promptId)
|
||||
setPromptData(response)
|
||||
setRawApiResponse(response) // Store the raw response for the Raw JSON tab
|
||||
} catch (error) {
|
||||
message.error("Failed to load prompt information")
|
||||
console.error("Error fetching prompt info:", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchPromptInfo()
|
||||
}, [promptId, accessToken])
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-4">Loading...</div>
|
||||
}
|
||||
|
||||
if (!promptData) {
|
||||
return <div className="p-4">Prompt not found</div>
|
||||
}
|
||||
|
||||
// Format date helper function
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "-"
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text: string | null | undefined, key: string) => {
|
||||
const success = await utilCopyToClipboard(text)
|
||||
if (success) {
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: true }))
|
||||
setTimeout(() => {
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: false }))
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
Back to Prompts
|
||||
</TremorButton>
|
||||
<Title>Prompt Details</Title>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Text className="text-gray-500 font-mono">{promptData.prompt_id}</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["prompt-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
onClick={() => copyToClipboard(promptData.prompt_id, "prompt-id")}
|
||||
className={`left-2 z-10 transition-all duration-200 ${
|
||||
copiedStates["prompt-id"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab key="overview">Overview</Tab>
|
||||
{isAdmin ? <Tab key="details">Details</Tab> : <></>}
|
||||
<Tab key="raw-json">Raw JSON</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* Overview Panel */}
|
||||
<TabPanel>
|
||||
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
|
||||
<Card>
|
||||
<Text>Prompt ID</Text>
|
||||
<div className="mt-2">
|
||||
<Title className="font-mono text-sm">{promptData.prompt_id}</Title>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Prompt Type</Text>
|
||||
<div className="mt-2">
|
||||
<Title>{promptData.prompt_info?.prompt_type || "-"}</Title>
|
||||
<Badge color="blue" className="mt-1">
|
||||
{promptData.prompt_info?.prompt_type || "Unknown"}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Created At</Text>
|
||||
<div className="mt-2">
|
||||
<Title>{formatDate(promptData.created_at)}</Title>
|
||||
<Text>Last Updated: {formatDate(promptData.updated_at)}</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{promptData.litellm_params && Object.keys(promptData.litellm_params).length > 0 && (
|
||||
<Card className="mt-6">
|
||||
<Text className="font-medium">LiteLLM Parameters</Text>
|
||||
<div className="mt-2 p-3 bg-gray-50 rounded-md">
|
||||
<pre className="text-xs text-gray-800 whitespace-pre-wrap">
|
||||
{JSON.stringify(promptData.litellm_params, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Details Panel (only for admins) */}
|
||||
{isAdmin && (
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<Title className="mb-4">Prompt Details</Title>
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Prompt Type</Text>
|
||||
<div>{promptData.prompt_info?.prompt_type || "-"}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Created At</Text>
|
||||
<div>{formatDate(promptData.created_at)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Last Updated</Text>
|
||||
<div>{formatDate(promptData.updated_at)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">LiteLLM Parameters</Text>
|
||||
<div className="mt-2 p-3 bg-gray-50 rounded-md border">
|
||||
<pre className="text-xs text-gray-800 whitespace-pre-wrap overflow-auto max-h-96">
|
||||
{JSON.stringify(promptData.litellm_params, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Prompt Info</Text>
|
||||
<div className="mt-2 p-3 bg-gray-50 rounded-md border">
|
||||
<pre className="text-xs text-gray-800 whitespace-pre-wrap">
|
||||
{JSON.stringify(promptData.prompt_info, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
)}
|
||||
|
||||
{/* Raw JSON Panel */}
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Raw API Response</Title>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["raw-json"] ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
|
||||
onClick={() => copyToClipboard(JSON.stringify(rawApiResponse, null, 2), "raw-json")}
|
||||
className={`transition-all duration-200 ${
|
||||
copiedStates["raw-json"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{copiedStates["raw-json"] ? "Copied!" : "Copy JSON"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-gray-50 rounded-md border overflow-auto">
|
||||
<pre className="text-xs text-gray-800 whitespace-pre-wrap">
|
||||
{JSON.stringify(rawApiResponse, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptInfoView
|
||||
174
ui/litellm-dashboard/src/components/prompts/prompt_table.tsx
Normal file
174
ui/litellm-dashboard/src/components/prompts/prompt_table.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import React, { useState } from "react"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Button } from "@tremor/react"
|
||||
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline"
|
||||
import { Tooltip } from "antd"
|
||||
import {PromptSpec} from "@/components/networking"
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
|
||||
interface PromptTableProps {
|
||||
promptsList: PromptSpec[]
|
||||
isLoading: boolean
|
||||
onPromptClick?: (id: string) => void
|
||||
}
|
||||
|
||||
const PromptTable: React.FC<PromptTableProps> = ({
|
||||
promptsList,
|
||||
isLoading,
|
||||
onPromptClick,
|
||||
}) => {
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }])
|
||||
|
||||
// Format date helper function
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "-"
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const columns: ColumnDef<PromptSpec>[] = [
|
||||
{
|
||||
header: "Prompt ID",
|
||||
accessorKey: "prompt_id",
|
||||
cell: (info: any) => (
|
||||
<Tooltip title={String(info.getValue() || "")}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
onClick={() => info.getValue() && onPromptClick?.(info.getValue())}
|
||||
>
|
||||
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) => {
|
||||
const prompt = row.original
|
||||
return (
|
||||
<Tooltip title={prompt.created_at}>
|
||||
<span className="text-xs">{formatDate(prompt.created_at)}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Updated At",
|
||||
accessorKey: "updated_at",
|
||||
cell: ({ row }) => {
|
||||
const prompt = row.original
|
||||
return (
|
||||
<Tooltip title={prompt.updated_at}>
|
||||
<span className="text-xs">{formatDate(prompt.updated_at)}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Type",
|
||||
accessorKey: "prompt_info.prompt_type",
|
||||
cell: ({ row }) => {
|
||||
const prompt = row.original
|
||||
return (
|
||||
<Tooltip title={prompt.prompt_info.prompt_type}>
|
||||
<span className="text-xs">{prompt.prompt_info.prompt_type}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: promptsList,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
enableSorting: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
className="py-1 h-8"
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
<div className="w-4">
|
||||
{header.column.getIsSorted() ? (
|
||||
{
|
||||
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
|
||||
}[header.column.getIsSorted() as string]
|
||||
) : (
|
||||
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : promptsList.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="h-8">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No prompts found</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptTable
|
||||
Loading…
Add table
Reference in a new issue