[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:
Ishaan Jaff
2025-11-19 16:26:11 -08:00
committed by GitHub
parent ffe00f4034
commit cd6256f64a
18 changed files with 1162 additions and 1 deletions

View File

@@ -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=())

View File

@@ -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

View File

@@ -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";

View File

@@ -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";

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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();
};

View 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;

View File

@@ -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;

File diff suppressed because one or more lines are too long