Skip to content
79 changes: 28 additions & 51 deletions app/(routes)/configurations/prompt-editor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,21 @@
import { invalidateConfigCache } from "@/app/lib/utils";
import { configState } from "@/app/lib/store/configStore";
import { apiFetch } from "@/app/lib/apiClient";
import { isGpt5Model } from "@/app/lib/models";
import { DEFAULT_CONFIG } from "@/app/lib/constants";

function PromptEditorContent() {
const toast = useToast();
const searchParams = useSearchParams();
const { sidebarCollapsed, setSidebarCollapsed } = useApp();
const { activeKey } = useAuth();

// URL query params for cross-navigation
const urlConfigId = searchParams.get("config");
const urlVersion = searchParams.get("version");
const showHistory = searchParams.get("history") === "true";
const isNewConfig = searchParams.get("new") === "true";

// Evaluation context to preserve (when coming from evaluations page)
const urlDatasetId = searchParams.get("dataset");
const urlExperimentName = searchParams.get("experiment");
const fromEvaluations = searchParams.get("from") === "evaluations";

// Default config for new versions
const defaultConfig: ConfigBlob = {
completion: {
provider: "openai",
type: "text",
params: {
model: "gpt-4o-mini",
instructions: "",
temperature: 0.7,
tools: [],
},
},
};

// Use shared configs hook with caching — pageSize:0 means only 1 API call on mount
const {
configs: savedConfigs,
isLoading,
Expand All @@ -76,50 +58,41 @@
const initialLoadComplete = !isLoading;
const editorInitialized = React.useRef(false);
const [editorReady, setEditorReady] = useState<boolean>(!urlConfigId);

const [stableVersionItemsMap, setStableVersionItemsMap] = useState<
Record<string, import("@/app/lib/types/configs").ConfigVersionItems[]>
>({});

useEffect(() => {
if (Object.keys(hookVersionItemsMap).length > 0) {
setStableVersionItemsMap((prev) => ({ ...prev, ...hookVersionItemsMap }));
}
}, [hookVersionItemsMap]);

const versionItemsMap = stableVersionItemsMap;

// Current working state
const [currentContent, setCurrentContent] = useState<string>(
"You are a helpful AI assistant.\nYou provide clear and concise answers.\nYou are polite and professional.",
);
const [currentConfigBlob, setCurrentConfigBlob] =
useState<ConfigBlob>(defaultConfig);
useState<ConfigBlob>(DEFAULT_CONFIG);
const [currentConfigName, setCurrentConfigName] = useState<string>("");
const [selectedConfigId, setSelectedConfigId] = useState<string>(""); // Selected version ID
const [selectedConfigId, setSelectedConfigId] = useState<string>("");
const [currentConfigParentId, setCurrentConfigParentId] =
useState<string>(""); // Parent config ID for evaluation
const [currentConfigVersion, setCurrentConfigVersion] = useState<number>(0); // Version number for evaluation
useState<string>("");
const [currentConfigVersion, setCurrentConfigVersion] = useState<number>(0);
const [provider, setProvider] = useState<string>("openai");
const [temperature, setTemperature] = useState<number>(0.7);
const [tools, setTools] = useState<Tool[]>([]);
const [expandedConfigs, setExpandedConfigs] = useState<Set<string>>(
new Set(),
);

// UI state
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
const [commitMessage, setCommitMessage] = useState<string>("");
const [showHistorySidebar, setShowHistorySidebar] = useState<boolean>(true); // Default open, or from URL param
const [showConfigPane, setShowConfigPane] = useState<boolean>(true); // Config pane collapse state

// History viewing state
const [showHistorySidebar, setShowHistorySidebar] = useState<boolean>(true);
const [showConfigPane, setShowConfigPane] = useState<boolean>(true);
const [selectedVersion, setSelectedVersion] = useState<SavedConfig | null>(
null,
);
const [compareWith, setCompareWith] = useState<SavedConfig | null>(null);

const getApiKey = (): string | null => activeKey?.key ?? null;
useEffect(() => {
if (Object.keys(hookVersionItemsMap).length > 0) {
setStableVersionItemsMap((prev) => ({ ...prev, ...hookVersionItemsMap }));
}
}, [hookVersionItemsMap]);

const versionItemsMap = stableVersionItemsMap;

// Populate the editor from a fully-loaded SavedConfig
const applyConfig = React.useCallback(
Expand Down Expand Up @@ -152,7 +125,7 @@
);
if (selectInHistory) setSelectedVersion(config);
},
[],

Check warning on line 128 in app/(routes)/configurations/prompt-editor/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build

React Hook React.useCallback has a missing dependency: 'currentConfigParentId'. Either include it or remove the dependency array. You can also replace multiple useState variables with useReducer if 'setExpandedConfigs' needs the current value of 'currentConfigParentId'
);

// Load a config directly from a SavedConfig object (no savedConfigs lookup needed)
Expand All @@ -161,7 +134,7 @@
if (!config) {
// Reset to new config
setCurrentContent("");
setCurrentConfigBlob(defaultConfig);
setCurrentConfigBlob(DEFAULT_CONFIG);
setProvider("openai");
setTemperature(0.7);
setSelectedConfigId("");
Expand All @@ -187,7 +160,7 @@
// If new config is requested, reset to defaults
if (isNewConfig) {
setCurrentContent("");
setCurrentConfigBlob(defaultConfig);
setCurrentConfigBlob(DEFAULT_CONFIG);
setProvider("openai");
setTemperature(0.7);
setSelectedConfigId("");
Expand Down Expand Up @@ -288,7 +261,7 @@
return;
}

const apiKey = getApiKey();
const apiKey = activeKey?.key;
if (!apiKey) {
toast.error("No API key found. Please add an API key in the Keystore.");
return;
Expand All @@ -314,18 +287,22 @@
}
});

const model = currentConfigBlob.completion.params.model;
const gpt5 = isGpt5Model(model);

const configBlob: ConfigBlob = {
completion: {
provider: currentConfigBlob.completion.provider,
type: currentConfigBlob.completion.type || "text", // Default to 'text'
type: currentConfigBlob.completion.type || "text",
params: {
model: currentConfigBlob.completion.params.model,
instructions: currentContent, // Store prompt as instructions
temperature: currentConfigBlob.completion.params.temperature,
// Flatten tools array to direct fields for backend - support multiple knowledge bases
model,
instructions: currentContent,
...(!gpt5 && {
temperature: currentConfigBlob.completion.params.temperature,
}),
...(allKnowledgeBaseIds.length > 0 && {
knowledge_base_ids: allKnowledgeBaseIds,
max_num_results: maxNumResults,
...(!gpt5 && { max_num_results: maxNumResults }),
}),
},
},
Expand Down
2 changes: 1 addition & 1 deletion app/(routes)/configurations/prompt-editor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface ConfigBlob {
params: {
model: string;
instructions: string;
temperature: number;
temperature?: number;
// Frontend uses tools array for UI
tools?: Tool[];
// Backend expects these as direct fields (flattened from tools array)
Expand Down
102 changes: 24 additions & 78 deletions app/(routes)/evaluations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import TabNavigation from "@/app/components/TabNavigation";
import { useToast } from "@/app/components/Toast";
import { useAuth } from "@/app/lib/context/AuthContext";
import { useApp } from "@/app/lib/context/AppContext";
import { apiFetch } from "@/app/lib/apiClient";
import Loader from "@/app/components/Loader";
import DatasetsTab from "@/app/components/evaluations/DatasetsTab";
import EvaluationsTab from "@/app/components/evaluations/EvaluationsTab";
Expand All @@ -36,14 +37,8 @@ function SimplifiedEvalContent() {
});

const { sidebarCollapsed, setSidebarCollapsed } = useApp();
const { apiKeys } = useAuth();
const [selectedKeyId, setSelectedKeyId] = useState<string>("");
const { activeKey } = useAuth();
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

// Dataset creation state
const [datasetName, setDatasetName] = useState("");
const [datasetDescription, setDatasetDescription] = useState("");
Expand Down Expand Up @@ -73,41 +68,33 @@ function SimplifiedEvalContent() {
);
const [isEvaluating, setIsEvaluating] = useState(false);

// Set initial selected key from context
useEffect(() => {
if (apiKeys.length > 0 && !selectedKeyId) {
setSelectedKeyId(apiKeys[0].id);
}
}, [apiKeys, selectedKeyId]);
setMounted(true);
}, []);

// Fetch datasets from backend
const loadStoredDatasets = useCallback(async () => {
const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
if (!selectedKey) {
if (!activeKey?.key) {
console.error("No selected API key found for loading datasets");
return;
}
setIsDatasetsLoading(true);
try {
const response = await fetch("/api/evaluations/datasets", {
method: "GET",
headers: { "X-API-KEY": selectedKey.key },
});
if (!response.ok) return;
const data = await response.json();
const data = await apiFetch<Dataset[] | { data: Dataset[] }>(
"/api/evaluations/datasets",
activeKey.key,
);
setStoredDatasets(Array.isArray(data) ? data : data.data || []);
} catch (e) {
console.error("Failed to load datasets:", e);
} finally {
setIsDatasetsLoading(false);
}
}, [apiKeys, selectedKeyId]);
}, [activeKey]);

useEffect(() => {
if (apiKeys.length > 0 && selectedKeyId) loadStoredDatasets();
}, [apiKeys, selectedKeyId, loadStoredDatasets]);
if (activeKey?.key) loadStoredDatasets();
}, [activeKey, loadStoredDatasets]);

// File selection handler
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
Expand Down Expand Up @@ -162,7 +149,6 @@ function SimplifiedEvalContent() {
reader.readAsText(file);
};

// Create dataset
const handleCreateDataset = async () => {
if (!uploadedFile) {
toast.error("Please select a CSV file");
Expand All @@ -172,8 +158,7 @@ function SimplifiedEvalContent() {
toast.error("Please enter a dataset name");
return;
}
const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
if (!selectedKey) {
if (!activeKey?.key) {
toast.error("No API key selected. Please select one in the Keystore.");
return;
}
Expand All @@ -190,29 +175,17 @@ function SimplifiedEvalContent() {
formData.append("duplication_factor", duplicationFactor);
}

const response = await fetch("/api/evaluations/datasets", {
method: "POST",
body: formData,
headers: { "X-API-KEY": selectedKey.key },
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.error ||
errorData.message ||
`Upload failed with status ${response.status}`,
);
}

const data = await response.json();
const data = await apiFetch<{ dataset_id?: number }>(
"/api/evaluations/datasets",
activeKey.key,
{ method: "POST", body: formData },
);
await loadStoredDatasets();

if (data.dataset_id) {
setSelectedDatasetId(data.dataset_id.toString());
}

// Reset form
setUploadedFile(null);
setDatasetName("");
setDatasetDescription("");
Expand All @@ -228,9 +201,8 @@ function SimplifiedEvalContent() {
}
};

// Run evaluation
const handleRunEvaluation = async () => {
if (!selectedKeyId) {
if (!activeKey?.key) {
toast.error("Please select an API key first");
return;
}
Expand All @@ -247,12 +219,6 @@ function SimplifiedEvalContent() {
return;
}

const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
if (!selectedKey) {
toast.error("Selected API key not found");
return;
}

setIsEvaluating(true);
try {
const payload = {
Expand All @@ -262,31 +228,13 @@ function SimplifiedEvalContent() {
config_version: selectedConfigVersion,
};

const response = await fetch("/api/evaluations", {
await apiFetch("/api/evaluations", activeKey.key, {
method: "POST",
headers: {
"X-API-KEY": selectedKey.key,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.error ||
errorData.message ||
`Evaluation failed with status ${response.status}`,
);
}

const data = await response.json();
const evalId = data.id || data.data?.id || data.eval_id || "unknown";

setIsEvaluating(false);
toast.success(
`Evaluation created! ${evalId !== "unknown" ? `Job ID: ${evalId}` : ""}`,
);
toast.success(`Evaluation created!`);
return true;
} catch (error: unknown) {
toast.error(
Expand Down Expand Up @@ -364,7 +312,7 @@ function SimplifiedEvalContent() {
/>

{/* Tab Content */}
{!mounted || apiKeys.length === 0 ? (
{!mounted || !activeKey ? (
<div
className="flex-1 flex items-center justify-center"
style={{ backgroundColor: colors.bg.secondary }}
Expand Down Expand Up @@ -431,16 +379,14 @@ function SimplifiedEvalContent() {
}}
storedDatasets={storedDatasets}
isDatasetsLoading={isDatasetsLoading}
apiKeys={apiKeys}
selectedKeyId={selectedKeyId}
activeKey={activeKey}
loadStoredDatasets={loadStoredDatasets}
toast={toast}
/>
) : (
<EvaluationsTab
leftPanelWidth={leftPanelWidth}
apiKeys={apiKeys}
selectedKeyId={selectedKeyId}
activeKey={activeKey}
storedDatasets={storedDatasets}
selectedDatasetId={selectedDatasetId}
setSelectedDatasetId={setSelectedDatasetId}
Expand Down
Loading
Loading