mirror of
https://github.com/BerriAI/litellm.git
synced 2025-12-06 11:33:26 +08:00
[Feat] Prompt Management - Add UI for editing the prompts (#16853)
* v0 for prompt management * v0 * clean up view of prompt editor * commit editor view * refactor prompt editor view * ui - refactor prompt editor * add move message * add prompt editor view * fix allow viewing dotprompt file * add dotprompt_content * handleSave for Prompt * ui fix build fail * ui fix build
This commit is contained in:
@@ -23,6 +23,11 @@ class PromptLiteLLMParams(BaseModel):
|
||||
prompt_id: str
|
||||
prompt_integration: str
|
||||
|
||||
dotprompt_content: Optional[str] = None
|
||||
"""
|
||||
allows saving the dotprompt file content
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="allow", protected_namespaces=())
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getPromptsList, PromptSpec, ListPromptsResponse, deletePromptCall } fro
|
||||
import PromptTable from "./prompts/prompt_table";
|
||||
import PromptInfoView from "./prompts/prompt_info";
|
||||
import AddPromptForm from "./prompts/add_prompt_form";
|
||||
import PromptEditorView from "./prompts/prompt_editor_view";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
|
||||
@@ -19,6 +20,7 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedPromptId, setSelectedPromptId] = useState<string | null>(null);
|
||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||
const [showEditorView, setShowEditorView] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string } | null>(null);
|
||||
|
||||
@@ -50,6 +52,13 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
};
|
||||
|
||||
const handleAddPrompt = () => {
|
||||
if (selectedPromptId) {
|
||||
setSelectedPromptId(null);
|
||||
}
|
||||
setShowEditorView(true);
|
||||
};
|
||||
|
||||
const handleAddPromptFromFile = () => {
|
||||
if (selectedPromptId) {
|
||||
setSelectedPromptId(null);
|
||||
}
|
||||
@@ -60,6 +69,10 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
setIsAddModalVisible(false);
|
||||
};
|
||||
|
||||
const handleCloseEditor = () => {
|
||||
setShowEditorView(false);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
fetchPrompts();
|
||||
};
|
||||
@@ -91,7 +104,13 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
{selectedPromptId ? (
|
||||
{showEditorView ? (
|
||||
<PromptEditorView
|
||||
onClose={handleCloseEditor}
|
||||
onSuccess={handleSuccess}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
) : selectedPromptId ? (
|
||||
<PromptInfoView
|
||||
promptId={selectedPromptId}
|
||||
onClose={() => setSelectedPromptId(null)}
|
||||
@@ -102,9 +121,14 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
|
||||
) : (
|
||||
<>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleAddPrompt} disabled={!accessToken}>
|
||||
+ Add New Prompt
|
||||
</Button>
|
||||
<Button onClick={handleAddPromptFromFile} disabled={!accessToken} variant="secondary">
|
||||
Upload .prompt File
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PromptTable
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export { default as PromptTable } from "./prompt_table";
|
||||
export { default as PromptInfoView } from "./prompt_info";
|
||||
export { default as PromptEditorView } from "./prompt_editor_view";
|
||||
export { default as ToolModal } from "./tool_modal";
|
||||
export { default as VariableTextArea } from "./variable_textarea";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Re-export from the new modular structure
|
||||
export { default } from "./prompt_editor_view/index";
|
||||
export type { PromptEditorViewProps } from "./prompt_editor_view/types";
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { MessageSquareIcon } from "lucide-react";
|
||||
|
||||
const ConversationPanel: React.FC = () => {
|
||||
return (
|
||||
<div className="flex-1 bg-white flex flex-col">
|
||||
<div className="flex-1 flex items-center justify-center text-gray-400">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 mx-auto mb-3 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<MessageSquareIcon size={24} className="text-gray-400" />
|
||||
</div>
|
||||
<p className="text-sm">Your conversation will appear here</p>
|
||||
<p className="text-xs text-gray-500 mt-2">Save the prompt to test it</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationPanel;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import { Card, Text } from "@tremor/react";
|
||||
import VariableTextArea from "../variable_textarea";
|
||||
|
||||
interface DeveloperMessageCardProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const DeveloperMessageCard: React.FC<DeveloperMessageCardProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<Card className="p-3">
|
||||
<Text className="block mb-2 text-sm font-medium">Developer message</Text>
|
||||
<Text className="text-gray-500 text-xs mb-2">
|
||||
Optional system instructions for the model
|
||||
</Text>
|
||||
<VariableTextArea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
rows={3}
|
||||
placeholder="e.g., You are a helpful assistant..."
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeveloperMessageCard;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { PromptType } from "./types";
|
||||
import { convertToDotPrompt } from "./utils";
|
||||
|
||||
interface DotpromptViewTabProps {
|
||||
prompt: PromptType;
|
||||
}
|
||||
|
||||
const DotpromptViewTab: React.FC<DotpromptViewTabProps> = ({ prompt }) => {
|
||||
const dotpromptContent = convertToDotPrompt(prompt);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-2">
|
||||
Generated .prompt file
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
This is the dotprompt format that will be saved to the database
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 overflow-auto">
|
||||
<pre className="text-sm text-gray-900 font-mono whitespace-pre-wrap">
|
||||
{dotpromptContent}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotpromptViewTab;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import React, { useState } from "react";
|
||||
import { Text } from "@tremor/react";
|
||||
import { Input } from "antd";
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
import ModelSelector from "../../common_components/ModelSelector";
|
||||
|
||||
interface ModelConfigCardProps {
|
||||
model: string;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
accessToken: string | null;
|
||||
onModelChange: (model: string) => void;
|
||||
onTemperatureChange: (temp: number) => void;
|
||||
onMaxTokensChange: (tokens: number) => void;
|
||||
}
|
||||
|
||||
const ModelConfigCard: React.FC<ModelConfigCardProps> = ({
|
||||
model,
|
||||
temperature = 1,
|
||||
maxTokens = 1000,
|
||||
accessToken,
|
||||
onModelChange,
|
||||
onTemperatureChange,
|
||||
onMaxTokensChange,
|
||||
}) => {
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-[300px]">
|
||||
<ModelSelector
|
||||
accessToken={accessToken || ""}
|
||||
value={model}
|
||||
onChange={onModelChange}
|
||||
showLabel={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowConfig(!showConfig)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<SettingsIcon size={16} />
|
||||
<span>Parameters</span>
|
||||
</button>
|
||||
|
||||
{showConfig && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-30">
|
||||
<div className="bg-white rounded-lg shadow-xl p-6 w-96">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">Model Parameters</h3>
|
||||
<button
|
||||
onClick={() => setShowConfig(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="text-sm text-gray-700">Temperature</Text>
|
||||
<Input
|
||||
type="number"
|
||||
size="small"
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
value={temperature}
|
||||
onChange={(e) => onTemperatureChange(parseFloat(e.target.value) || 0)}
|
||||
className="w-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="text-sm text-gray-700">Max Tokens</Text>
|
||||
<Input
|
||||
type="number"
|
||||
size="small"
|
||||
min={1}
|
||||
max={32768}
|
||||
value={maxTokens}
|
||||
onChange={(e) => onMaxTokensChange(parseInt(e.target.value) || 1000)}
|
||||
className="w-24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelConfigCard;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import { Button as TremorButton } from "@tremor/react";
|
||||
import { Input } from "antd";
|
||||
import { ArrowLeftIcon, SaveIcon } from "lucide-react";
|
||||
|
||||
interface PromptEditorHeaderProps {
|
||||
promptName: string;
|
||||
onNameChange: (name: string) => void;
|
||||
onBack: () => void;
|
||||
onSave: () => void;
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
const PromptEditorHeader: React.FC<PromptEditorHeaderProps> = ({
|
||||
promptName,
|
||||
onNameChange,
|
||||
onBack,
|
||||
onSave,
|
||||
isSaving,
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onBack} size="xs">
|
||||
Back
|
||||
</TremorButton>
|
||||
<Input
|
||||
value={promptName}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
className="text-base font-medium border-none shadow-none"
|
||||
style={{ width: "200px" }}
|
||||
/>
|
||||
<span className="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">Draft</span>
|
||||
<span className="text-xs text-gray-400">Unsaved changes</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<TremorButton
|
||||
icon={SaveIcon}
|
||||
onClick={onSave}
|
||||
loading={isSaving}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Save
|
||||
</TremorButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptEditorHeader;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { useState } from "react";
|
||||
import { Card, Text } from "@tremor/react";
|
||||
import { Select } from "antd";
|
||||
import { PlusIcon, TrashIcon, GripVerticalIcon } from "lucide-react";
|
||||
import VariableTextArea from "../variable_textarea";
|
||||
import { Message } from "./types";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface PromptMessagesCardProps {
|
||||
messages: Message[];
|
||||
onAddMessage: () => void;
|
||||
onUpdateMessage: (index: number, field: "role" | "content", value: string) => void;
|
||||
onRemoveMessage: (index: number) => void;
|
||||
onMoveMessage: (fromIndex: number, toIndex: number) => void;
|
||||
}
|
||||
|
||||
const PromptMessagesCard: React.FC<PromptMessagesCardProps> = ({
|
||||
messages,
|
||||
onAddMessage,
|
||||
onUpdateMessage,
|
||||
onRemoveMessage,
|
||||
onMoveMessage,
|
||||
}) => {
|
||||
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
|
||||
const handleDragStart = (index: number) => {
|
||||
setDraggedIndex(index);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, index: number) => {
|
||||
e.preventDefault();
|
||||
setDragOverIndex(index);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
|
||||
e.preventDefault();
|
||||
if (draggedIndex !== null && draggedIndex !== dropIndex) {
|
||||
onMoveMessage(draggedIndex, dropIndex);
|
||||
}
|
||||
setDraggedIndex(null);
|
||||
setDragOverIndex(null);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDraggedIndex(null);
|
||||
setDragOverIndex(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-3">
|
||||
<div className="mb-2">
|
||||
<Text className="text-sm font-medium">Prompt messages</Text>
|
||||
<Text className="text-gray-500 text-xs mt-1">
|
||||
Use <code className="bg-gray-100 px-1 rounded text-xs">{'{{variable}}'}</code> syntax for template variables
|
||||
</Text>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
draggable
|
||||
onDragStart={() => handleDragStart(index)}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className={`border border-gray-300 rounded overflow-hidden bg-white transition-all ${
|
||||
draggedIndex === index ? "opacity-50" : ""
|
||||
} ${dragOverIndex === index && draggedIndex !== index ? "border-blue-500 border-2" : ""}`}
|
||||
>
|
||||
<div className="bg-gray-50 px-2 py-1.5 border-b border-gray-300 flex items-center justify-between">
|
||||
<Select
|
||||
value={message.role}
|
||||
onChange={(value) => onUpdateMessage(index, "role", value)}
|
||||
style={{ width: 100 }}
|
||||
size="small"
|
||||
bordered={false}
|
||||
>
|
||||
<Option value="user">User</Option>
|
||||
<Option value="assistant">Assistant</Option>
|
||||
<Option value="system">System</Option>
|
||||
</Select>
|
||||
<div className="flex items-center gap-1">
|
||||
{messages.length > 1 && (
|
||||
<button
|
||||
onClick={() => onRemoveMessage(index)}
|
||||
className="text-gray-400 hover:text-red-500"
|
||||
>
|
||||
<TrashIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
<div className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600">
|
||||
<GripVerticalIcon size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<VariableTextArea
|
||||
value={message.content}
|
||||
onChange={(value) => onUpdateMessage(index, "content", value)}
|
||||
rows={3}
|
||||
placeholder="Enter prompt content..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={onAddMessage}
|
||||
className="mt-2 text-xs text-blue-600 hover:text-blue-700 flex items-center"
|
||||
>
|
||||
<PlusIcon size={14} className="mr-1" />
|
||||
Add message
|
||||
</button>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptMessagesCard;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import { Button as TremorButton, Text } from "@tremor/react";
|
||||
import { Input, Modal } from "antd";
|
||||
|
||||
interface PublishModalProps {
|
||||
visible: boolean;
|
||||
promptName: string;
|
||||
isSaving: boolean;
|
||||
onNameChange: (name: string) => void;
|
||||
onPublish: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const PublishModal: React.FC<PublishModalProps> = ({
|
||||
visible,
|
||||
promptName,
|
||||
isSaving,
|
||||
onNameChange,
|
||||
onPublish,
|
||||
onCancel,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title="Publish Prompt"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<div key="footer" className="flex justify-end gap-2">
|
||||
<TremorButton variant="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</TremorButton>
|
||||
<TremorButton onClick={onPublish} loading={isSaving}>
|
||||
Publish
|
||||
</TremorButton>
|
||||
</div>
|
||||
]}
|
||||
>
|
||||
<div className="py-4">
|
||||
<Text className="mb-2">Name</Text>
|
||||
<Input
|
||||
value={promptName}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
placeholder="Enter prompt name"
|
||||
onPressEnter={onPublish}
|
||||
autoFocus
|
||||
/>
|
||||
<Text className="text-gray-500 text-xs mt-2">
|
||||
Published prompts can be used in API calls and are versioned for easy tracking.
|
||||
</Text>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublishModal;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { Card, Text } from "@tremor/react";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { Tool } from "./types";
|
||||
|
||||
interface ToolsCardProps {
|
||||
tools: Tool[];
|
||||
onAddTool: () => void;
|
||||
onEditTool: (index: number) => void;
|
||||
onRemoveTool: (index: number) => void;
|
||||
}
|
||||
|
||||
const ToolsCard: React.FC<ToolsCardProps> = ({
|
||||
tools,
|
||||
onAddTool,
|
||||
onEditTool,
|
||||
onRemoveTool,
|
||||
}) => {
|
||||
return (
|
||||
<Card className="p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="text-sm font-medium">Tools</Text>
|
||||
<button
|
||||
onClick={onAddTool}
|
||||
className="text-xs text-blue-600 hover:text-blue-700 flex items-center"
|
||||
>
|
||||
<PlusIcon size={14} className="mr-1" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{tools.length === 0 ? (
|
||||
<Text className="text-gray-500 text-xs">No tools added</Text>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{tools.map((tool, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-2 bg-gray-50 border border-gray-200 rounded"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-xs truncate">{tool.name}</div>
|
||||
<div className="text-xs text-gray-500 truncate">{tool.description}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1 ml-2">
|
||||
<button
|
||||
onClick={() => onEditTool(index)}
|
||||
className="text-xs text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onRemoveTool(index)}
|
||||
className="text-gray-400 hover:text-red-500"
|
||||
>
|
||||
<TrashIcon size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolsCard;
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import React, { useState } from "react";
|
||||
import ToolModal from "../tool_modal";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
import { createPromptCall } from "../../networking";
|
||||
import { PromptType, PromptEditorViewProps, Tool } from "./types";
|
||||
import { convertToDotPrompt } from "./utils";
|
||||
import PromptEditorHeader from "./PromptEditorHeader";
|
||||
import ModelConfigCard from "./ModelConfigCard";
|
||||
import ToolsCard from "./ToolsCard";
|
||||
import DeveloperMessageCard from "./DeveloperMessageCard";
|
||||
import PromptMessagesCard from "./PromptMessagesCard";
|
||||
import ConversationPanel from "./ConversationPanel";
|
||||
import PublishModal from "./PublishModal";
|
||||
import DotpromptViewTab from "./DotpromptViewTab";
|
||||
|
||||
const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess, accessToken }) => {
|
||||
const [prompt, setPrompt] = useState<PromptType>({
|
||||
name: "New prompt",
|
||||
model: "gpt-4o",
|
||||
config: {
|
||||
temperature: 1,
|
||||
max_tokens: 1000,
|
||||
},
|
||||
tools: [],
|
||||
developerMessage: "",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Enter task specifics. Use {{template_variables}} for dynamic inputs",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [showToolModal, setShowToolModal] = useState(false);
|
||||
const [showNameModal, setShowNameModal] = useState(false);
|
||||
const [editingToolIndex, setEditingToolIndex] = useState<number | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<"pretty" | "dotprompt">("pretty");
|
||||
|
||||
const addMessage = () => {
|
||||
setPrompt({
|
||||
...prompt,
|
||||
messages: [
|
||||
...prompt.messages,
|
||||
{
|
||||
role: "user",
|
||||
content: "",
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const updateMessage = (index: number, field: "role" | "content", value: string) => {
|
||||
const newMessages = [...prompt.messages];
|
||||
newMessages[index][field] = value;
|
||||
setPrompt({
|
||||
...prompt,
|
||||
messages: newMessages,
|
||||
});
|
||||
};
|
||||
|
||||
const removeMessage = (index: number) => {
|
||||
if (prompt.messages.length > 1) {
|
||||
setPrompt({
|
||||
...prompt,
|
||||
messages: prompt.messages.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const moveMessage = (fromIndex: number, toIndex: number) => {
|
||||
const newMessages = [...prompt.messages];
|
||||
const [movedMessage] = newMessages.splice(fromIndex, 1);
|
||||
newMessages.splice(toIndex, 0, movedMessage);
|
||||
setPrompt({
|
||||
...prompt,
|
||||
messages: newMessages,
|
||||
});
|
||||
};
|
||||
|
||||
const addTool = (json: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(json);
|
||||
const tool: Tool = {
|
||||
name: parsed.function?.name || "Unnamed Tool",
|
||||
description: parsed.function?.description || "",
|
||||
json: json,
|
||||
};
|
||||
|
||||
if (editingToolIndex !== null) {
|
||||
const newTools = [...prompt.tools];
|
||||
newTools[editingToolIndex] = tool;
|
||||
setPrompt({
|
||||
...prompt,
|
||||
tools: newTools,
|
||||
});
|
||||
} else {
|
||||
setPrompt({
|
||||
...prompt,
|
||||
tools: [...prompt.tools, tool],
|
||||
});
|
||||
}
|
||||
|
||||
setShowToolModal(false);
|
||||
setEditingToolIndex(null);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Invalid JSON format");
|
||||
}
|
||||
};
|
||||
|
||||
const removeTool = (index: number) => {
|
||||
setPrompt({
|
||||
...prompt,
|
||||
tools: prompt.tools.filter((_, i) => i !== index),
|
||||
});
|
||||
};
|
||||
|
||||
const openToolModal = (index?: number) => {
|
||||
if (index !== undefined) {
|
||||
setEditingToolIndex(index);
|
||||
} else {
|
||||
setEditingToolIndex(null);
|
||||
}
|
||||
setShowToolModal(true);
|
||||
};
|
||||
|
||||
const handleSaveClick = () => {
|
||||
if (!prompt.name || prompt.name.trim() === "" || prompt.name === "New prompt") {
|
||||
setShowNameModal(true);
|
||||
} else {
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("Access token is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prompt.name || prompt.name.trim() === "") {
|
||||
NotificationsManager.fromBackend("Please enter a valid prompt name");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const promptId = prompt.name.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
|
||||
const dotpromptContent = convertToDotPrompt(prompt);
|
||||
|
||||
const promptData = {
|
||||
prompt_id: promptId,
|
||||
litellm_params: {
|
||||
prompt_integration: "dotprompt",
|
||||
prompt_id: promptId,
|
||||
dotprompt_content: dotpromptContent,
|
||||
},
|
||||
prompt_info: {
|
||||
prompt_type: "db",
|
||||
},
|
||||
};
|
||||
|
||||
await createPromptCall(accessToken, promptData);
|
||||
NotificationsManager.success("Prompt created successfully!");
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Error saving prompt:", error);
|
||||
NotificationsManager.fromBackend("Failed to save prompt");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
setShowNameModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full bg-white">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<PromptEditorHeader
|
||||
promptName={prompt.name}
|
||||
onNameChange={(name) => setPrompt({ ...prompt, name })}
|
||||
onBack={onClose}
|
||||
onSave={handleSaveClick}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<div className="w-1/2 overflow-y-auto bg-white border-r border-gray-200">
|
||||
<div className="border-b border-gray-200 bg-white px-6 py-4 flex items-center gap-3">
|
||||
<ModelConfigCard
|
||||
model={prompt.model}
|
||||
temperature={prompt.config.temperature}
|
||||
maxTokens={prompt.config.max_tokens}
|
||||
accessToken={accessToken}
|
||||
onModelChange={(model) => setPrompt({ ...prompt, model })}
|
||||
onTemperatureChange={(temperature) =>
|
||||
setPrompt({
|
||||
...prompt,
|
||||
config: { ...prompt.config, temperature },
|
||||
})
|
||||
}
|
||||
onMaxTokensChange={(max_tokens) =>
|
||||
setPrompt({
|
||||
...prompt,
|
||||
config: { ...prompt.config, max_tokens },
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="ml-auto inline-flex items-center bg-gray-200 rounded-full p-0.5">
|
||||
<button
|
||||
className={`px-3 py-1 text-xs font-medium rounded-full transition-colors ${
|
||||
viewMode === "pretty"
|
||||
? "bg-white text-gray-900 shadow-sm"
|
||||
: "text-gray-600"
|
||||
}`}
|
||||
onClick={() => setViewMode("pretty")}
|
||||
>
|
||||
PRETTY
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-1 text-xs font-medium rounded-full transition-colors ${
|
||||
viewMode === "dotprompt"
|
||||
? "bg-white text-gray-900 shadow-sm"
|
||||
: "text-gray-600"
|
||||
}`}
|
||||
onClick={() => setViewMode("dotprompt")}
|
||||
>
|
||||
DOTPROMPT
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewMode === "pretty" ? (
|
||||
<div className="p-6 space-y-4 pb-20">
|
||||
<ToolsCard
|
||||
tools={prompt.tools}
|
||||
onAddTool={() => openToolModal()}
|
||||
onEditTool={openToolModal}
|
||||
onRemoveTool={removeTool}
|
||||
/>
|
||||
|
||||
<DeveloperMessageCard
|
||||
value={prompt.developerMessage}
|
||||
onChange={(developerMessage) => setPrompt({ ...prompt, developerMessage })}
|
||||
/>
|
||||
|
||||
<PromptMessagesCard
|
||||
messages={prompt.messages}
|
||||
onAddMessage={addMessage}
|
||||
onUpdateMessage={updateMessage}
|
||||
onRemoveMessage={removeMessage}
|
||||
onMoveMessage={moveMessage}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<DotpromptViewTab prompt={prompt} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConversationPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PublishModal
|
||||
visible={showNameModal}
|
||||
promptName={prompt.name}
|
||||
isSaving={isSaving}
|
||||
onNameChange={(name) => setPrompt({ ...prompt, name })}
|
||||
onPublish={handleSave}
|
||||
onCancel={() => setShowNameModal(false)}
|
||||
/>
|
||||
|
||||
{showToolModal && (
|
||||
<ToolModal
|
||||
visible={showToolModal}
|
||||
initialJson={editingToolIndex !== null ? prompt.tools[editingToolIndex].json : ""}
|
||||
onSave={addTool}
|
||||
onClose={() => {
|
||||
setShowToolModal(false);
|
||||
setEditingToolIndex(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptEditorView;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export interface Message {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
json: string;
|
||||
}
|
||||
|
||||
export interface PromptType {
|
||||
name: string;
|
||||
model: string;
|
||||
config: {
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
top_p?: number;
|
||||
};
|
||||
tools: Tool[];
|
||||
developerMessage: string;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface PromptEditorViewProps {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
accessToken: string | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { PromptType } from "./types";
|
||||
|
||||
export const extractVariables = (prompt: PromptType): string[] => {
|
||||
const variableSet = new Set<string>();
|
||||
const variableRegex = /\{\{(\w+)\}\}/g;
|
||||
|
||||
prompt.messages.forEach((message) => {
|
||||
let match;
|
||||
while ((match = variableRegex.exec(message.content)) !== null) {
|
||||
variableSet.add(match[1]);
|
||||
}
|
||||
});
|
||||
|
||||
if (prompt.developerMessage) {
|
||||
let match;
|
||||
while ((match = variableRegex.exec(prompt.developerMessage)) !== null) {
|
||||
variableSet.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(variableSet);
|
||||
};
|
||||
|
||||
export const convertToDotPrompt = (prompt: PromptType): string => {
|
||||
const variables = extractVariables(prompt);
|
||||
let result = `---\nmodel: ${prompt.model}\n`;
|
||||
|
||||
// Add temperature if set
|
||||
if (prompt.config.temperature !== undefined) {
|
||||
result += `temperature: ${prompt.config.temperature}\n`;
|
||||
}
|
||||
|
||||
// Add max_tokens if set
|
||||
if (prompt.config.max_tokens !== undefined) {
|
||||
result += `max_tokens: ${prompt.config.max_tokens}\n`;
|
||||
}
|
||||
|
||||
// Add top_p if set
|
||||
if (prompt.config.top_p !== undefined) {
|
||||
result += `top_p: ${prompt.config.top_p}\n`;
|
||||
}
|
||||
|
||||
// Add input schema
|
||||
result += `input:\n schema:\n`;
|
||||
variables.forEach((variable) => {
|
||||
result += ` ${variable}: string\n`;
|
||||
});
|
||||
|
||||
// Add output format
|
||||
result += `output:\n format: text\n`;
|
||||
|
||||
// Add tools if present
|
||||
if (prompt.tools && prompt.tools.length > 0) {
|
||||
result += `tools:\n`;
|
||||
prompt.tools.forEach((tool) => {
|
||||
const toolObj = JSON.parse(tool.json);
|
||||
result += ` - ${JSON.stringify(toolObj)}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
result += `---\n\n`;
|
||||
|
||||
// Add developer message if present
|
||||
if (prompt.developerMessage && prompt.developerMessage.trim() !== "") {
|
||||
result += `Developer: ${prompt.developerMessage.trim()}\n\n`;
|
||||
}
|
||||
|
||||
// Add messages with role prefixes
|
||||
prompt.messages.forEach((message) => {
|
||||
const role = message.role.charAt(0).toUpperCase() + message.role.slice(1);
|
||||
result += `${role}: ${message.content}\n\n`;
|
||||
});
|
||||
|
||||
return result.trim();
|
||||
};
|
||||
|
||||
89
ui/litellm-dashboard/src/components/prompts/tool_modal.tsx
Normal file
89
ui/litellm-dashboard/src/components/prompts/tool_modal.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from "react";
|
||||
import { Modal, Button } from "antd";
|
||||
|
||||
interface ToolModalProps {
|
||||
visible: boolean;
|
||||
initialJson: string;
|
||||
onSave: (json: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const defaultToolJson = `{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_weather",
|
||||
"description": "Get the current weather in a given location",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state, e.g. San Francisco, CA"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["celsius", "fahrenheit"]
|
||||
}
|
||||
},
|
||||
"required": ["location"]
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const ToolModal: React.FC<ToolModalProps> = ({ visible, initialJson, onSave, onClose }) => {
|
||||
const [json, setJson] = useState(initialJson || defaultToolJson);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSave = () => {
|
||||
try {
|
||||
JSON.parse(json);
|
||||
setError(null);
|
||||
onSave(json);
|
||||
} catch (e) {
|
||||
setError("Invalid JSON format. Please check your syntax.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setError(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-medium">Add Tool</span>
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button key="save" type="primary" onClick={handleSave}>
|
||||
Add
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded text-red-600 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
value={json}
|
||||
onChange={(e) => setJson(e.target.value)}
|
||||
className="w-full min-h-[400px] px-4 py-3 border border-gray-300 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
placeholder="Paste your tool JSON here..."
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolModal;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import React, { useState } from "react";
|
||||
import { Input, Popover, Tag } from "antd";
|
||||
import { EditOutlined } from "@ant-design/icons";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface VariableTextAreaProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const VariableTextArea: React.FC<VariableTextAreaProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
rows = 4,
|
||||
className,
|
||||
}) => {
|
||||
const [editingVariable, setEditingVariable] = useState<{
|
||||
oldName: string;
|
||||
start: number;
|
||||
end: number;
|
||||
} | null>(null);
|
||||
const [newVariableName, setNewVariableName] = useState("");
|
||||
|
||||
// Extract all variables from the text
|
||||
const extractVariables = (): Array<{ name: string; start: number; end: number }> => {
|
||||
const variableRegex = /\{\{(\w+)\}\}/g;
|
||||
const variables: Array<{ name: string; start: number; end: number }> = [];
|
||||
let match;
|
||||
|
||||
while ((match = variableRegex.exec(value)) !== null) {
|
||||
variables.push({
|
||||
name: match[1],
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
});
|
||||
}
|
||||
|
||||
return variables;
|
||||
};
|
||||
|
||||
const handleVariableEdit = () => {
|
||||
if (!newVariableName.trim() || !editingVariable) return;
|
||||
|
||||
const newValue =
|
||||
value.substring(0, editingVariable.start) +
|
||||
`{{${newVariableName}}}` +
|
||||
value.substring(editingVariable.end);
|
||||
|
||||
onChange(newValue);
|
||||
setEditingVariable(null);
|
||||
setNewVariableName("");
|
||||
};
|
||||
|
||||
const variables = extractVariables();
|
||||
|
||||
// New approach: Use ContentEditable div for true inline styling
|
||||
// This is much harder to get right with React, so for now, let's stick to the reliable
|
||||
// "Tags Below" approach which is robust and functional.
|
||||
// If user insists on inline coloring, we can revisit the overlay approach but it's very fragile.
|
||||
|
||||
// BUT, to satisfy "variables in text box", we can try a simple trick:
|
||||
// Render the text as HTML with colored spans inside a contentEditable div
|
||||
// and sync it back. This is the "wysiwyg" approach.
|
||||
|
||||
return (
|
||||
<div className={`variable-textarea-container ${className}`}>
|
||||
<style>
|
||||
{`
|
||||
.variable-highlight-text {
|
||||
color: #f97316;
|
||||
background-color: #fff7ed;
|
||||
border-radius: 4px;
|
||||
padding: 0 2px;
|
||||
border: 1px solid #fed7aa;
|
||||
font-family: monospace;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
{/* Using standard TextArea for reliability */}
|
||||
<TextArea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
className="font-sans"
|
||||
/>
|
||||
|
||||
{/* Variable Management - Clear and Functional */}
|
||||
{variables.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2 items-center">
|
||||
<span className="text-xs text-gray-500 mr-1">Detected variables:</span>
|
||||
{variables.map((variable, index) => (
|
||||
<Popover
|
||||
key={`${variable.start}-${index}`}
|
||||
content={
|
||||
<div className="p-2" style={{ minWidth: "200px" }}>
|
||||
<div className="text-xs text-gray-500 mb-2">Edit variable name</div>
|
||||
<Input
|
||||
size="small"
|
||||
value={newVariableName}
|
||||
onChange={(e) => setNewVariableName(e.target.value)}
|
||||
onPressEnter={handleVariableEdit}
|
||||
placeholder="Variable name"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
onClick={handleVariableEdit}
|
||||
className="text-xs px-2 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingVariable(null);
|
||||
setNewVariableName("");
|
||||
}}
|
||||
className="text-xs px-2 py-1 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
open={editingVariable?.start === variable.start}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setEditingVariable(null);
|
||||
setNewVariableName("");
|
||||
}
|
||||
}}
|
||||
trigger="click"
|
||||
>
|
||||
<Tag
|
||||
color="orange"
|
||||
className="cursor-pointer hover:opacity-80 transition-all m-0"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
setEditingVariable({
|
||||
oldName: variable.name,
|
||||
start: variable.start,
|
||||
end: variable.end,
|
||||
});
|
||||
setNewVariableName(variable.name);
|
||||
}}
|
||||
>
|
||||
{variable.name}
|
||||
</Tag>
|
||||
</Popover>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariableTextArea;
|
||||
1
ui/litellm-dashboard/tsconfig.tsbuildinfo
Normal file
1
ui/litellm-dashboard/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user