diff --git a/app/(main)/configurations/prompt-editor/page.tsx b/app/(main)/configurations/prompt-editor/page.tsx index 897c0ec..55a65db 100644 --- a/app/(main)/configurations/prompt-editor/page.tsx +++ b/app/(main)/configurations/prompt-editor/page.tsx @@ -29,19 +29,8 @@ import { import { invalidateConfigCache } from "@/app/lib/utils"; import { configState } from "@/app/lib/store/configStore"; import { apiFetch } from "@/app/lib/apiClient"; - -const DEFAULT_CONFIG: ConfigBlob = { - completion: { - provider: "openai", - type: "text", - params: { - model: "gpt-4o-mini", - instructions: "", - temperature: 0.7, - tools: [], - }, - }, -}; +import { isGpt5Model } from "@/app/lib/models"; +import { DEFAULT_CONFIG } from "@/app/lib/constants"; function PromptEditorContent() { const toast = useToast(); @@ -55,7 +44,6 @@ function PromptEditorContent() { const urlDatasetId = searchParams.get("dataset"); const urlExperimentName = searchParams.get("experiment"); const fromEvaluations = searchParams.get("from") === "evaluations"; - const { configs: savedConfigs, isLoading, @@ -78,10 +66,10 @@ function PromptEditorContent() { const [currentConfigBlob, setCurrentConfigBlob] = useState(DEFAULT_CONFIG); const [currentConfigName, setCurrentConfigName] = useState(""); - const [selectedConfigId, setSelectedConfigId] = useState(""); // Selected version ID + const [selectedConfigId, setSelectedConfigId] = useState(""); const [currentConfigParentId, setCurrentConfigParentId] = useState(""); - const [currentConfigVersion, setCurrentConfigVersion] = useState(0); // Version number for evaluation + const [currentConfigVersion, setCurrentConfigVersion] = useState(0); const [provider, setProvider] = useState("openai"); const [temperature, setTemperature] = useState(0.7); const [tools, setTools] = useState([]); @@ -287,17 +275,22 @@ function PromptEditorContent() { } }); + const model = currentConfigBlob.completion.params.model; + const gpt5 = isGpt5Model(model); + const configBlob: ConfigBlob = { completion: { provider: currentConfigBlob.completion.provider, type: currentConfigBlob.completion.type || "text", params: { - model: currentConfigBlob.completion.params.model, + model, instructions: currentContent, - temperature: currentConfigBlob.completion.params.temperature, + ...(!gpt5 && { + temperature: currentConfigBlob.completion.params.temperature, + }), ...(allKnowledgeBaseIds.length > 0 && { knowledge_base_ids: allKnowledgeBaseIds, - max_num_results: maxNumResults, + ...(!gpt5 && { max_num_results: maxNumResults }), }), }, }, diff --git a/app/(main)/evaluations/[id]/page.tsx b/app/(main)/evaluations/[id]/page.tsx index a9a5843..aa1ec57 100644 --- a/app/(main)/evaluations/[id]/page.tsx +++ b/app/(main)/evaluations/[id]/page.tsx @@ -20,7 +20,6 @@ import { GroupedTraceItem, isGroupedFormat, } from "@/app/components/types"; -import { getStatusColor } from "@/app/components/utils"; import ConfigModal from "@/app/components/ConfigModal"; import Sidebar from "@/app/components/Sidebar"; import DetailedResultsTable from "@/app/components/DetailedResultsTable"; @@ -35,6 +34,7 @@ import { GroupIcon, RefreshIcon, } from "@/app/components/icons"; +import { sanitizeCSVCell } from "@/app/lib/utils"; export default function EvaluationReport() { const router = useRouter(); @@ -56,23 +56,6 @@ export default function EvaluationReport() { const [isResyncing, setIsResyncing] = useState(false); const [showNoTracesModal, setShowNoTracesModal] = useState(false); - // CSV helper functions - const escapeCSVValue = (value: string): string => { - return value.replace(/"/g, '""').replace(/\n/g, " "); - }; - - const sanitizeCSVCell = ( - value: string, - preventFormulaInjection = false, - ): string => { - let sanitized = escapeCSVValue(value); - // Prevent CSV formula injection by prepending space to values starting with =, +, -, @ - if (preventFormulaInjection && /^[=+\-@]/.test(sanitized)) { - sanitized = " " + sanitized; - } - return `"${sanitized}"`; - }; - useEffect(() => { if (apiKeys.length > 0 && !selectedKeyId) { setSelectedKeyId(apiKeys[0].id); @@ -394,12 +377,14 @@ export default function EvaluationReport() { const scoreObject = getScoreObject(job); const hasScore = !!scoreObject; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const statusColor = getStatusColor(job.status); const isNewFormat = hasSummaryScores(scoreObject); const summaryScores = isNewFormat && scoreObject ? scoreObject.summary_scores || [] : []; + const isJobInProgress = + job.status.toLowerCase() !== "completed" && + job.status.toLowerCase() !== "failed"; + return (
- {/* Content */}
- {/* Metrics */} {hasScore && isNewFormat ? (
{summaryScores.some( (s) => job.total_items && s.total_pairs < job.total_items, - ) && ( -
- - Some traces are still being scored. Scores shown are - partial and may change — click{" "} - Resync to - get the latest. -
- )} + ) && + isJobInProgress && ( +
+ + Some traces are still being scored. Scores shown are + partial and may change - click{" "} + Resync to get + the latest. +
+ )}

{summary.std !== undefined && `±${summary.std.toFixed(3)} · `} - {job.total_items && - summary.total_pairs < job.total_items ? ( - - {summary.total_pairs}/{job.total_items} pairs - - - ) : ( - {summary.total_pairs} pairs - )} + + {summary.total_pairs} + {job.total_items && + summary.total_pairs < job.total_items && + `/${job.total_items}`}{" "} + pairs +

))} @@ -686,18 +665,13 @@ export default function EvaluationReport() { )}
- {job.total_items && - summary.total_pairs < job.total_items ? ( - - {summary.total_pairs}/{job.total_items} pairs - - - ) : ( - {summary.total_pairs} pairs - )} + + {summary.total_pairs} + {job.total_items && + summary.total_pairs < job.total_items && + `/${job.total_items}`}{" "} + pairs +
))} diff --git a/app/(main)/evaluations/page.tsx b/app/(main)/evaluations/page.tsx index e139f92..df6c5a5 100644 --- a/app/(main)/evaluations/page.tsx +++ b/app/(main)/evaluations/page.tsx @@ -17,6 +17,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"; @@ -37,14 +38,8 @@ function SimplifiedEvalContent() { }); const { sidebarCollapsed } = useApp(); - const { apiKeys } = useAuth(); - const [selectedKeyId, setSelectedKeyId] = useState(""); + const { activeKey } = useAuth(); const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - // Dataset creation state const [datasetName, setDatasetName] = useState(""); const [datasetDescription, setDatasetDescription] = useState(""); @@ -74,41 +69,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( + "/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) => { const file = event.target.files?.[0]; if (!file) return; @@ -163,7 +150,6 @@ function SimplifiedEvalContent() { reader.readAsText(file); }; - // Create dataset const handleCreateDataset = async () => { if (!uploadedFile) { toast.error("Please select a CSV file"); @@ -173,8 +159,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; } @@ -191,29 +176,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(""); @@ -229,9 +202,8 @@ function SimplifiedEvalContent() { } }; - // Run evaluation const handleRunEvaluation = async () => { - if (!selectedKeyId) { + if (!activeKey?.key) { toast.error("Please select an API key first"); return; } @@ -248,12 +220,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 = { @@ -263,31 +229,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( @@ -323,7 +271,7 @@ function SimplifiedEvalContent() { /> {/* Tab Content */} - {!mounted || apiKeys.length === 0 ? ( + {!mounted || !activeKey ? (
) : ( ); } -// Wrapper component with Suspense + export default function SimplifiedEval() { return ( }> diff --git a/app/components/ConfigDrawer.tsx b/app/components/ConfigDrawer.tsx index 250fb3a..05c5561 100644 --- a/app/components/ConfigDrawer.tsx +++ b/app/components/ConfigDrawer.tsx @@ -6,7 +6,8 @@ "use client"; -import React, { useState } from "react"; +import { useEffect, useState } from "react"; +import { MODEL_OPTIONS, isGpt5Model } from "@/app/lib/models"; import { colors } from "@/app/lib/colors"; import { SavedConfig } from "./SimplifiedConfigEditor"; import { Tool } from "@/app/lib/types/configs"; @@ -39,27 +40,6 @@ interface ConfigDrawerProps { onApplyConfig: (configId: string) => void; } -const MODEL_OPTIONS = { - openai: [ - { value: "gpt-4o", label: "GPT-4o" }, - { value: "gpt-4o-mini", label: "GPT-4o Mini" }, - { value: "gpt-4-turbo", label: "GPT-4 Turbo" }, - { value: "gpt-4", label: "GPT-4" }, - { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }, - ], - // anthropic: [ - // { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet' }, - // { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' }, - // { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet' }, - // { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku' }, - // ], - // google: [ - // { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' }, - // { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' }, - // { value: 'gemini-pro', label: 'Gemini Pro' }, - // ], -}; - function generateDiff( text1: string, text2: string, @@ -113,8 +93,10 @@ export default function ConfigDrawer({ ); const [isCreatingNew, setIsCreatingNew] = useState(false); + const isGpt5 = isGpt5Model(currentConfig.modelName); + // Sync isCreatingNew with selectedConfigId - React.useEffect(() => { + useEffect(() => { if (selectedConfigId) { setIsCreatingNew(false); } @@ -555,43 +537,48 @@ export default function ConfigDrawer({
{/* Temperature */} -
- - - onConfigChange("temperature", parseFloat(e.target.value)) - } - style={{ width: "100%", accentColor: colors.accent.primary }} - /> -
- Focused (0) - Balanced (0.5) - Creative (1) + {!isGpt5 && ( +
+ + + onConfigChange("temperature", parseFloat(e.target.value)) + } + style={{ + width: "100%", + accentColor: colors.accent.primary, + }} + /> +
+ Focused (0) + Balanced (0.5) + Creative (1) +
-
+ )} {/* Tools Section */}
@@ -693,36 +680,38 @@ export default function ConfigDrawer({ }} />
-
- - - updateTool( - index, - "max_num_results", - parseInt(e.target.value) || 20, - ) - } - style={{ - width: "100%", - padding: "6px", - border: `1px solid ${colors.border}`, - borderRadius: "4px", - fontSize: "12px", - }} - /> -
+ {!isGpt5 && ( +
+ + + updateTool( + index, + "max_num_results", + parseInt(e.target.value) || 20, + ) + } + style={{ + width: "100%", + padding: "6px", + border: `1px solid ${colors.border}`, + borderRadius: "4px", + fontSize: "12px", + }} + /> +
+ )}
))} diff --git a/app/components/ConfigSelector.tsx b/app/components/ConfigSelector.tsx index fa604ad..5b1a9a1 100644 --- a/app/components/ConfigSelector.tsx +++ b/app/components/ConfigSelector.tsx @@ -54,11 +54,11 @@ export default function ConfigSelector({ const [promptExpanded, setPromptExpanded] = useState(false); const [isPromptOverflowing, setIsPromptOverflowing] = useState(false); const promptRef = useRef(null); - const [expandedConfigId, setExpandedConfigId] = useState(null); // config group is expanded in the dropdown + const [expandedConfigId, setExpandedConfigId] = useState(null); const [loadingVersionsFor, setLoadingVersionsFor] = useState>( new Set(), - ); // State for use which config groups are currently loading their version list - const [isLoadingPreview, setIsLoadingPreview] = useState(false); // True while full config details are being fetched for the preview pane + ); + const [isLoadingPreview, setIsLoadingPreview] = useState(false); // When a config group is expanded, eagerly load full version details (provider, // modelName, temperature). @@ -87,13 +87,11 @@ export default function ConfigSelector({ (c) => c.config_id === selectedConfigId && c.version === selectedVersion, ); - // Config name from lightweight metadata const selectedConfigName = selectedConfigId ? allConfigMeta.find((m) => m.id === selectedConfigId)?.name : undefined; // Auto-load full config details for the preview pane whenever the selection changes - // and the full data isn't already in the loaded set. useEffect(() => { if (!selectedConfigId || !selectedVersion) { setIsLoadingPreview(false); @@ -330,7 +328,7 @@ export default function ConfigSelector({ style={{ color: colors.text.secondary }} > {full - ? `${full.provider}/${full.modelName} • T:${full.temperature.toFixed(2)} • ${formatRelativeTime(item.inserted_at)}` + ? `${full.provider}/${full.modelName} ${full.temperature !== undefined ? `• T:${full.temperature.toFixed(2)}` : ""} • ${formatRelativeTime(item.inserted_at)}` : formatRelativeTime(item.inserted_at)} ); @@ -593,7 +591,6 @@ export default function ConfigSelector({ ); })} - {/* Spinner while version list is being fetched */} {isExpanded && isLoadingGroup && (
)} - {/* Empty state: expanded but no versions returned */} + {isExpanded && !isLoadingGroup && versionItems.length === 0 && ( @@ -678,20 +675,24 @@ export default function ConfigSelector({ {selectedConfig.provider}/{selectedConfig.modelName}
-
-
- Temperature -
-
- {selectedConfig.temperature.toFixed(2)} + + {selectedConfig.temperature !== undefined && ( +
+
+ Temperature +
+
+ {selectedConfig.temperature.toFixed(2)} +
-
+ )} + {selectedConfig.tools && selectedConfig.tools.length > 0 && ( <>
diff --git a/app/components/evaluations/DatasetsTab.tsx b/app/components/evaluations/DatasetsTab.tsx index f89fd29..f25ae5e 100644 --- a/app/components/evaluations/DatasetsTab.tsx +++ b/app/components/evaluations/DatasetsTab.tsx @@ -5,6 +5,7 @@ import { colors } from "@/app/lib/colors"; import { APIKey } from "@/app/lib/types/credentials"; import { Dataset } from "@/app/(main)/datasets/page"; import { useToast } from "@/app/components/Toast"; +import { apiFetch } from "@/app/lib/apiClient"; import EvalDatasetDescription from "./EvalDatasetDescription"; import Loader from "@/app/components/Loader"; @@ -24,8 +25,7 @@ export interface DatasetsTabProps { resetForm: () => void; storedDatasets: Dataset[]; isDatasetsLoading: boolean; - apiKeys: APIKey[]; - selectedKeyId: string; + activeKey: APIKey; loadStoredDatasets: () => void; toast: ReturnType; } @@ -46,8 +46,7 @@ export default function DatasetsTab({ resetForm, storedDatasets, isDatasetsLoading, - apiKeys, - selectedKeyId, + activeKey, loadStoredDatasets, toast, }: DatasetsTabProps) { @@ -73,19 +72,13 @@ export default function DatasetsTab({ }, [showDuplicationInfo]); const handleDeleteDataset = async (datasetId: number) => { - const selectedKey = apiKeys.find((k) => k.id === selectedKeyId); - if (!selectedKey) return; + if (!activeKey?.key) return; setDeletingId(datasetId); try { - const response = await fetch(`/api/evaluations/datasets/${datasetId}`, { + await apiFetch(`/api/evaluations/datasets/${datasetId}`, activeKey.key, { method: "DELETE", - headers: { "X-API-KEY": selectedKey.key }, }); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || "Failed to delete dataset"); - } toast.success("Dataset deleted"); loadStoredDatasets(); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -106,23 +99,18 @@ export default function DatasetsTab({ } | null>(null); const handleViewDataset = async (datasetId: number, datasetName: string) => { - const selectedKey = apiKeys.find((k) => k.id === selectedKeyId); - if (!selectedKey) return; + if (!activeKey?.key) return; setViewingId(datasetId); try { - const response = await fetch( + const data = await apiFetch<{ + data?: { signed_url?: string }; + signed_url?: string; + csv_content?: string; + }>( `/api/evaluations/datasets/${datasetId}?include_signed_url=true&fetch_content=true`, - { - method: "GET", - headers: { "X-API-KEY": selectedKey.key }, - }, + activeKey.key, ); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || "Failed to get dataset"); - } - const data = await response.json(); const signedUrl = data?.data?.signed_url || data?.signed_url; const csvText = data?.csv_content; if (!csvText) { diff --git a/app/components/evaluations/EvaluationsTab.tsx b/app/components/evaluations/EvaluationsTab.tsx index 76f6401..9a57448 100644 --- a/app/components/evaluations/EvaluationsTab.tsx +++ b/app/components/evaluations/EvaluationsTab.tsx @@ -9,13 +9,13 @@ import Loader from "@/app/components/Loader"; import EvalRunCard from "./EvalRunCard"; import EvalDatasetDescription from "./EvalDatasetDescription"; import { APIKey } from "@/app/lib/types/credentials"; +import { apiFetch } from "@/app/lib/apiClient"; type Tab = "datasets" | "evaluations"; export interface EvaluationsTabProps { leftPanelWidth: number; - apiKeys: APIKey[]; - selectedKeyId: string; + activeKey: APIKey; storedDatasets: Dataset[]; selectedDatasetId: string; setSelectedDatasetId: (id: string) => void; @@ -31,8 +31,7 @@ export interface EvaluationsTabProps { export default function EvaluationsTab({ leftPanelWidth, - apiKeys, - selectedKeyId, + activeKey, storedDatasets, selectedDatasetId, setSelectedDatasetId, @@ -65,33 +64,19 @@ export default function EvaluationsTab({ // Fetch evaluation jobs const fetchEvaluations = useCallback(async () => { - if (!selectedKeyId) { + if (!activeKey?.key) { setError("Please select an API key first"); return; } - const selectedKey = apiKeys.find((k) => k.id === selectedKeyId); - if (!selectedKey) { - setError("Selected API key not found"); - return; - } setIsLoading(true); setError(null); try { - const response = await fetch("/api/evaluations", { - method: "GET", - headers: { "X-API-KEY": selectedKey.key }, - }); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.error || - errorData.message || - `Failed to fetch evaluations: ${response.status}`, - ); - } - const data = await response.json(); + const data = await apiFetch( + "/api/evaluations", + activeKey.key, + ); setEvalJobs(Array.isArray(data) ? data : data.data || []); } catch (err: unknown) { setError( @@ -100,25 +85,21 @@ export default function EvaluationsTab({ } finally { setIsLoading(false); } - }, [apiKeys, selectedKeyId]); + }, [activeKey]); // Fetch assistant config const fetchAssistantConfig = useCallback( async (assistantId: string) => { - if (!selectedKeyId) return; - const selectedKey = apiKeys.find((k) => k.id === selectedKeyId); - if (!selectedKey) return; + if (!activeKey?.key) return; try { - const response = await fetch(`/api/assistant/${assistantId}`, { - method: "GET", - headers: { "X-API-KEY": selectedKey.key }, - }); - if (!response.ok) return; - const result = await response.json(); + const result = await apiFetch<{ + success: boolean; + data?: AssistantConfig; + }>(`/api/assistant/${assistantId}`, activeKey.key); if (result.success && result.data) { setAssistantConfigs((prev) => - new Map(prev).set(assistantId, result.data), + new Map(prev).set(assistantId, result.data!), ); } } catch (err) { @@ -128,7 +109,7 @@ export default function EvaluationsTab({ ); } }, - [apiKeys, selectedKeyId], + [activeKey], ); useEffect(() => { @@ -140,8 +121,8 @@ export default function EvaluationsTab({ }, [evalJobs, assistantConfigs, fetchAssistantConfig]); useEffect(() => { - if (selectedKeyId) fetchEvaluations(); - }, [selectedKeyId, fetchEvaluations]); + if (activeKey?.key) fetchEvaluations(); + }, [activeKey, fetchEvaluations]); return (
diff --git a/app/components/prompt-editor/ConfigEditorPane.tsx b/app/components/prompt-editor/ConfigEditorPane.tsx index f564fba..63502cc 100644 --- a/app/components/prompt-editor/ConfigEditorPane.tsx +++ b/app/components/prompt-editor/ConfigEditorPane.tsx @@ -7,6 +7,7 @@ import { ConfigVersionItems, } from "@/app/lib/types/configs"; import { formatRelativeTime } from "@/app/lib/utils"; +import { MODEL_OPTIONS, isGpt5Model } from "@/app/lib/models"; import { ChevronRightIcon, ChevronDownIcon, @@ -15,11 +16,7 @@ import { SpinnerIcon, InfoIcon, } from "@/app/components/icons"; -import { - MODEL_OPTIONS, - PROVIDER_TYPES, - PROVIDES_OPTIONS, -} from "@/app/lib/constants"; +import { PROVIDER_TYPES, PROVIDES_OPTIONS } from "@/app/lib/constants"; interface ConfigEditorPaneProps { configBlob: ConfigBlob; @@ -119,6 +116,7 @@ export default function ConfigEditorPane({ const provider = configBlob.completion.provider; const params = configBlob.completion.params; + const isGpt5 = isGpt5Model(params.model); const tools = (params.tools || []) as Tool[]; // Find currently selected config from loaded set @@ -673,34 +671,35 @@ export default function ConfigEditorPane({
- {/* Temperature */} -
- - - handleTemperatureChange(parseFloat(e.target.value)) - } - className="w-full" - style={{ accentColor: colors.accent.primary }} - /> -
- 0 - 2 + {!isGpt5 && ( +
+ + + handleTemperatureChange(parseFloat(e.target.value)) + } + className="w-full" + style={{ accentColor: colors.accent.primary }} + /> +
+ 0 + 2 +
-
+ )} {/* Tools */}
@@ -717,8 +716,6 @@ export default function ConfigEditorPane({ style={{ backgroundColor: colors.accent.primary, color: colors.bg.primary, - border: "none", - cursor: "pointer", }} > + Add Tool @@ -778,55 +775,58 @@ export default function ConfigEditorPane({ }} />
-
-
- -
setShowTooltip(index)} - onMouseLeave={() => setShowTooltip(null)} - > - - {showTooltip === index && ( -
- Controls how many matching results are returned -
- from the search -
-
- )} + + {!isGpt5 && ( +
+
+ +
setShowTooltip(index)} + onMouseLeave={() => setShowTooltip(null)} + > + + {showTooltip === index && ( +
+ Controls how many matching results are returned +
+ from the search +
+
+ )} +
+ + handleUpdateTool( + index, + "max_num_results", + parseInt(e.target.value) || 20, + ) + } + className="w-full px-2 py-1 rounded text-xs focus:outline-none" + style={{ + border: `1px solid ${colors.border}`, + backgroundColor: colors.bg.primary, + color: colors.text.primary, + }} + />
- - handleUpdateTool( - index, - "max_num_results", - parseInt(e.target.value) || 20, - ) - } - className="w-full px-2 py-1 rounded text-xs focus:outline-none border border-gray-300" - style={{ - backgroundColor: colors.bg.primary, - color: colors.text.primary, - }} - /> -
+ )}
))}
- {/* Commit Message */}
- {/* Save Button */}
@@ -282,41 +285,43 @@ export default function CurrentConfigTab({
{/* Temperature Slider */} -
- - onTemperatureChange(parseFloat(e.target.value))} - style={{ width: "100%" }} - /> -
- Focused (0) - Balanced (0.5) - Creative (1) + {!isGpt5 && ( +
+ + onTemperatureChange(parseFloat(e.target.value))} + style={{ width: "100%" }} + /> +
+ Focused (0) + Balanced (0.5) + Creative (1) +
-
+ )} {/* Tools Section */}
@@ -414,36 +419,38 @@ export default function CurrentConfigTab({ }} />
-
- - - updateTool( - index, - "max_num_results", - parseInt(e.target.value) || 20, - ) - } - style={{ - width: "100%", - padding: "6px", - border: `1px solid ${colors.border}`, - borderRadius: "4px", - fontSize: "12px", - }} - /> -
+ {!isGpt5 && ( +
+ + + updateTool( + index, + "max_num_results", + parseInt(e.target.value) || 20, + ) + } + style={{ + width: "100%", + padding: "6px", + border: `1px solid ${colors.border}`, + borderRadius: "4px", + fontSize: "12px", + }} + /> +
+ )}
))}
diff --git a/app/lib/constants.ts b/app/lib/constants.ts index 4cb99d6..3ffbcdf 100644 --- a/app/lib/constants.ts +++ b/app/lib/constants.ts @@ -2,6 +2,8 @@ * Constants for Management */ +import { ConfigBlob } from "@/app/lib/types/promptEditor"; + /** localStorage key for the config cache */ export const CACHE_KEY = "kaapi_configs_cache"; @@ -45,3 +47,16 @@ export const MODEL_OPTIONS = { // { value: 'gemini-pro', label: 'Gemini Pro' }, // ], }; + +export const DEFAULT_CONFIG: ConfigBlob = { + completion: { + provider: "openai", + type: "text", + params: { + model: "gpt-4o-mini", + instructions: "", + temperature: 0.7, + tools: [], + }, + }, +}; diff --git a/app/lib/models.ts b/app/lib/models.ts new file mode 100644 index 0000000..21dbfc9 --- /dev/null +++ b/app/lib/models.ts @@ -0,0 +1,43 @@ +export interface ModelOption { + value: string; + label: string; +} + +export const MODEL_OPTIONS: Record = { + openai: [ + // GPT-5 family + { value: "gpt-5.4", label: "GPT-5.4" }, + { value: "gpt-5.4-pro", label: "GPT-5.4 Pro" }, + { value: "gpt-5.4-mini", label: "GPT-5.4 Mini" }, + { value: "gpt-5.4-nano", label: "GPT-5.4 Nano" }, + { value: "gpt-5", label: "GPT-5" }, + { value: "gpt-5-mini", label: "GPT-5 Mini" }, + { value: "gpt-5-nano", label: "GPT-5 Nano" }, + // GPT-4 family + { value: "gpt-4.1", label: "GPT-4.1" }, + { value: "gpt-4o", label: "GPT-4o" }, + { value: "gpt-4o-mini", label: "GPT-4o Mini" }, + { value: "gpt-4-turbo", label: "GPT-4 Turbo" }, + { value: "gpt-4", label: "GPT-4" }, + { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }, + ], + // anthropic: [ + // { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet' }, + // { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' }, + // { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet' }, + // { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku' }, + // ], + // google: [ + // { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' }, + // { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' }, + // { value: 'gemini-pro', label: 'Gemini Pro' }, + // ], +}; + +/** + * Returns true if the given model ID is a GPT-5 family model. + * GPT-5 models do not support temperature or max_num_results parameters. + */ +export function isGpt5Model(model: string): boolean { + return model.startsWith("gpt-5"); +} diff --git a/app/lib/utils.ts b/app/lib/utils.ts index ea87190..8d2eb68 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -8,6 +8,7 @@ import { Tool, } from "@/app/lib/types/configs"; import { SavedConfig, ConfigGroup } from "./types/configs"; +import { isGpt5Model } from "./models"; export function timeAgo(dateStr: string): string { const date = @@ -55,7 +56,6 @@ export const formatRelativeTime = (timestamp: string | number): string => { return new Date(date).toLocaleDateString(); }; -// Call this when a config is saved/updated to invalidate cache export const invalidateConfigCache = (): void => { clearConfigCache(); }; @@ -117,7 +117,9 @@ export const flattenConfigVersion = ( modelName: params.model || "", provider: blob.completion.provider, type: blob.completion.type || "text", - temperature: params.temperature ?? 0.7, + temperature: isGpt5Model(params.model) + ? params.temperature + : (params.temperature ?? 0.7), vectorStoreIds: tools[0]?.knowledge_base_ids?.[0] || "", tools, commit_message: version.commit_message, @@ -158,3 +160,18 @@ export const groupConfigs = ( }; }); }; + +export const escapeCSVValue = (value: string): string => { + return value.replace(/"/g, '""').replace(/\n/g, " "); +}; + +export const sanitizeCSVCell = ( + value: string, + preventFormulaInjection = false, +): string => { + let sanitized = escapeCSVValue(value); + if (preventFormulaInjection && /^[=+\-@]/.test(sanitized)) { + sanitized = " " + sanitized; + } + return `"${sanitized}"`; +};