diff --git a/app/components/ConfigCard.tsx b/app/components/ConfigCard.tsx index 096d180..73a0444 100644 --- a/app/components/ConfigCard.tsx +++ b/app/components/ConfigCard.tsx @@ -8,7 +8,8 @@ import React, { useState } from 'react'; import { useRouter } from 'next/navigation'; import { colors } from '@/app/lib/colors'; -import { ConfigGroup, SavedConfig, formatRelativeTime } from '@/app/lib/useConfigs'; +import { ConfigGroup, SavedConfig } from '@/app/lib/types/configs'; +import { formatRelativeTime } from '@/app/lib/utils'; interface ConfigCardProps { configGroup: ConfigGroup; diff --git a/app/components/ConfigSelector.tsx b/app/components/ConfigSelector.tsx index b53b0b9..fa604ad 100644 --- a/app/components/ConfigSelector.tsx +++ b/app/components/ConfigSelector.tsx @@ -3,13 +3,20 @@ * Allows selecting a saved config with "Edit in Prompt Editor" link */ -"use client" +"use client"; -import { useState, useRef, useLayoutEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { colors } from '@/app/lib/colors'; -import { useConfigs, SavedConfig, formatRelativeTime } from '@/app/lib/useConfigs'; -import { ChevronUpIcon, ChevronDownIcon, EditIcon, GearIcon, CheckIcon } from '@/app/components/icons'; +import { useState, useRef, useLayoutEffect, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { colors } from "@/app/lib/colors"; +import { useConfigs } from "@/app/hooks/useConfigs"; +import { + ChevronUpIcon, + ChevronDownIcon, + EditIcon, + GearIcon, + CheckIcon, +} from "@/app/components/icons"; +import { formatRelativeTime } from "@/app/lib/utils"; interface ConfigSelectorProps { selectedConfigId: string; @@ -33,16 +40,40 @@ export default function ConfigSelector({ experimentName, }: ConfigSelectorProps) { const router = useRouter(); - const { configs, configGroups, isLoading, error } = useConfigs(); + const { + configs, + isLoading, + error, + loadVersionsForConfig, + loadSingleVersion, + allConfigMeta, + versionItemsMap, + } = useConfigs({ pageSize: 0 }); const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); + const [searchQuery, setSearchQuery] = useState(""); 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 [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 + + // When a config group is expanded, eagerly load full version details (provider, + // modelName, temperature). + useEffect(() => { + if (!expandedConfigId) return; + const items = versionItemsMap[expandedConfigId]; + if (!items || items.length === 0) return; + items.forEach((item) => { + loadSingleVersion(item.config_id, item.version); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [expandedConfigId, versionItemsMap]); // Reset expanded state and recheck overflow whenever selected config changes. useLayoutEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect setPromptExpanded(false); const el = promptRef.current; if (!el) return; @@ -51,44 +82,113 @@ export default function ConfigSelector({ setIsPromptOverflowing(el.scrollHeight > el.clientHeight); }, [selectedConfigId, selectedVersion, configs]); - // Find currently selected config + // Find currently selected config (only present after loadSingleVersion has completed) const selectedConfig = configs.find( - c => c.config_id === selectedConfigId && c.version === selectedVersion + (c) => c.config_id === selectedConfigId && c.version === selectedVersion, ); - // Filter config groups based on search query - const filteredConfigGroups = searchQuery.trim() - ? configGroups.filter(group => - group.name.toLowerCase().includes(searchQuery.toLowerCase()) + // 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); + return; + } + const alreadyLoaded = configs.find( + (c) => c.config_id === selectedConfigId && c.version === selectedVersion, + ); + if (alreadyLoaded) { + setIsLoadingPreview(false); + return; + } + setIsLoadingPreview(true); + loadSingleVersion(selectedConfigId, selectedVersion) + .then((result) => { + if (!result) setIsLoadingPreview(false); + }) + .catch(() => setIsLoadingPreview(false)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedConfigId, selectedVersion, configs]); + + // Dropdown display list: all configs from the lightweight allConfigMeta, + // filtered by search query. Version details are loaded on-demand per group. + const filteredDisplayGroups = searchQuery.trim() + ? allConfigMeta.filter((m) => + m.name.toLowerCase().includes(searchQuery.toLowerCase()), ) - : configGroups; + : allConfigMeta; - // Handle config selection - const handleSelect = (config: SavedConfig) => { - onConfigSelect(config.config_id, config.version); + const handleSelectVersionItem = (config_id: string, version: number) => { + onConfigSelect(config_id, version); setIsDropdownOpen(false); - setSearchQuery(''); // Clear search on selection + setSearchQuery(""); }; - // Handle dropdown close const handleCloseDropdown = () => { setIsDropdownOpen(false); - setSearchQuery(''); // Clear search on close + setSearchQuery(""); // Clear search on close + }; + + const handleOpenDropdown = () => { + if (disabled) return; + if (!isDropdownOpen) { + const autoExpand = selectedConfigId || null; + setExpandedConfigId(autoExpand); + if ( + autoExpand && + !versionItemsMap[autoExpand] && + !loadingVersionsFor.has(autoExpand) + ) { + setLoadingVersionsFor((prev) => new Set(prev).add(autoExpand)); + loadVersionsForConfig(autoExpand).finally(() => { + setLoadingVersionsFor((prev) => { + const s = new Set(prev); + s.delete(autoExpand); + return s; + }); + }); + } + } + setIsDropdownOpen((prev) => !prev); + }; + + // Toggle a config group’s expansion; load its version list on first expand + const handleToggleGroup = (config_id: string) => { + if (expandedConfigId === config_id) { + setExpandedConfigId(null); + return; + } + setExpandedConfigId(config_id); + if (!versionItemsMap[config_id] && !loadingVersionsFor.has(config_id)) { + setLoadingVersionsFor((prev) => new Set(prev).add(config_id)); + loadVersionsForConfig(config_id).finally(() => { + setLoadingVersionsFor((prev) => { + const s = new Set(prev); + s.delete(config_id); + return s; + }); + }); + } }; // Build URL params preserving evaluation context const buildEditorUrl = (configId?: string, version?: number) => { const params = new URLSearchParams(); if (configId && version) { - params.set('config', configId); - params.set('version', version.toString()); + params.set("config", configId); + params.set("version", version.toString()); } else { - params.set('new', 'true'); + params.set("new", "true"); } // Preserve evaluation context - if (datasetId) params.set('dataset', datasetId); - if (experimentName) params.set('experiment', experimentName); - params.set('from', 'evaluations'); // Mark that we came from evaluations + if (datasetId) params.set("dataset", datasetId); + if (experimentName) params.set("experiment", experimentName); + params.set("from", "evaluations"); // Mark that we came from evaluations return `/configurations/prompt-editor?${params.toString()}`; }; @@ -100,26 +200,42 @@ export default function ConfigSelector({ // Navigate to Config Library const handleBrowseLibrary = () => { const params = new URLSearchParams(); - if (datasetId) params.set('dataset', datasetId); - if (experimentName) params.set('experiment', experimentName); - params.set('from', 'evaluations'); + if (datasetId) params.set("dataset", datasetId); + if (experimentName) params.set("experiment", experimentName); + params.set("from", "evaluations"); router.push(`/configurations?${params.toString()}`); }; if (isLoading) { return (
-
-

- {compact ? 'Configuration *' : 'Select Configuration'} +
+

+ {compact ? "Configuration *" : "Select Configuration"}

Loading configurations...
@@ -130,33 +246,118 @@ export default function ConfigSelector({ if (error) { return (
-
-

- {compact ? 'Configuration *' : 'Select Configuration'} -

-
+

+ {compact ? "Configuration *" : "Select Configuration"} +

+
+
{error}
); } + const noConfigsAvailable = () => { + return ( +
+ +

+ No configurations found +

+

+ Create a configuration in the Prompt Editor first +

+ +
+ ); + }; + + const getModelVersionAndTime = (item: { + config_id: string; + version: number; + inserted_at: string; + }) => { + const full = configs.find( + (c) => c.config_id === item.config_id && c.version === item.version, + ); + return ( +
+ {full + ? `${full.provider}/${full.modelName} • T:${full.temperature.toFixed(2)} • ${formatRelativeTime(item.inserted_at)}` + : formatRelativeTime(item.inserted_at)} +
+ ); + }; + return (
- {/* Header */} -
+
-

- {compact ? 'Configuration *' : 'Select Configuration'} +

+ {compact ? "Configuration *" : "Select Configuration"}

{!compact && ( @@ -173,8 +374,12 @@ export default function ConfigSelector({ border: `1px solid ${colors.border}`, color: colors.text.primary, }} - onMouseEnter={(e) => e.currentTarget.style.backgroundColor = colors.bg.secondary} - onMouseLeave={(e) => e.currentTarget.style.backgroundColor = colors.bg.primary} + onMouseEnter={(e) => + (e.currentTarget.style.backgroundColor = colors.bg.secondary) + } + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = colors.bg.primary) + } > Browse Library @@ -186,47 +391,25 @@ export default function ConfigSelector({ border: `1px solid ${colors.border}`, color: colors.text.primary, }} - onMouseEnter={(e) => e.currentTarget.style.backgroundColor = colors.bg.secondary} - onMouseLeave={(e) => e.currentTarget.style.backgroundColor = colors.bg.primary} + onMouseEnter={(e) => + (e.currentTarget.style.backgroundColor = colors.bg.secondary) + } + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = colors.bg.primary) + } > - {selectedConfig ? 'Edit Config' : 'Create Config'} + {selectedConfig ? "Edit Config" : "Create Config"}
- {/* No configs available */} - {configGroups.length === 0 ? ( -
- -

- No configurations found -

-

- Create a configuration in the Prompt Editor first -

- -
+ {allConfigMeta.length === 0 ? ( + noConfigsAvailable() ) : ( <> - {/* Dropdown Selector */} -
+
{isDropdownOpen ? ( - /* Search Input when dropdown is open */ - {filteredConfigGroups.length === 0 ? ( -
- {searchQuery ? `No configurations match "${searchQuery}"` : 'No configurations available'} + {filteredDisplayGroups.length === 0 ? ( +
+ {searchQuery + ? `No configurations match "${searchQuery}"` + : "No configurations available"}
) : ( - filteredConfigGroups.map((group) => ( -
- {/* Config group header */} -
- {group.name} ({group.totalVersions} version{group.totalVersions !== 1 ? 's' : ''}) -
- {/* Versions */} - {group.versions.map((version) => ( - + {/* Version items — lightweight list, loaded on first expand */} + {isExpanded && + !isLoadingGroup && + versionItems.map((item) => { + const isSelected = + selectedConfigId === item.config_id && + selectedVersion === item.version; + return ( + + ); + })} + {/* Spinner while version list is being fetched */} + {isExpanded && isLoadingGroup && ( +
+ Loading versions…
-
- {selectedConfig?.id === version.id && ( - )} - - ))} -
- )))} + {/* Empty state: expanded but no versions returned */} + {isExpanded && + !isLoadingGroup && + versionItems.length === 0 && ( +
+ No versions available +
+ )} +
+ ); + }) + )}
)}
+ {/* Preview: loading state while full config details are being fetched */} + {isLoadingPreview && !selectedConfig && ( +
+ + + + + + Loading configuration details… + +
+ )} + {/* Selected Config Preview */} - {selectedConfig && ( + {selectedConfig && !isLoadingPreview && (
-
+
Provider & Model
-
+
{selectedConfig.provider}/{selectedConfig.modelName}
-
+
Temperature
-
+
{selectedConfig.temperature.toFixed(2)}
{selectedConfig.tools && selectedConfig.tools.length > 0 && ( <>
-
+
Knowledge Base IDs
-
- {selectedConfig.tools.map(tool => tool.knowledge_base_ids).flat().join(', ') || 'None'} +
+ {selectedConfig.tools + .map((tool) => tool.knowledge_base_ids) + .flat() + .join(", ") || "None"}
-
+
Max Results
-
+
{selectedConfig.tools[0].max_num_results}
@@ -382,32 +730,34 @@ export default function ConfigSelector({
{/* Prompt Preview */} -
+
-
+
Prompt Preview
{selectedConfig.instructions && isPromptOverflowing && ( )}
- {selectedConfig.instructions || 'No instructions set'} + {selectedConfig.instructions || "No instructions set"}
@@ -417,10 +767,7 @@ export default function ConfigSelector({ {/* Click outside to close dropdown */} {isDropdownOpen && ( -
+
)}
); diff --git a/app/components/prompt-editor/ConfigDiffPane.tsx b/app/components/prompt-editor/ConfigDiffPane.tsx index 5602601..1e6c9f8 100644 --- a/app/components/prompt-editor/ConfigDiffPane.tsx +++ b/app/components/prompt-editor/ConfigDiffPane.tsx @@ -1,6 +1,5 @@ -import React from 'react'; import { colors } from '@/app/lib/colors'; -import { SavedConfig } from '@/app/lib/useConfigs'; +import { SavedConfig } from '@/app/lib/types/configs'; interface ConfigDiffPaneProps { selectedCommit: SavedConfig; @@ -140,7 +139,7 @@ export default function ConfigDiffPane({ Before (v{compareWith.version})
void; + onLoadConfig: (config: SavedConfig | null) => void; commitMessage: string; onCommitMessageChange: (message: string) => void; onSave: () => void; @@ -19,37 +22,15 @@ interface ConfigEditorPaneProps { // Collapse functionality collapsed?: boolean; onToggle?: () => void; + allConfigMeta?: ConfigPublic[]; // Lightweight list of all configs + versionItemsMap?: Record; // Lightweight version items + loadVersionsForConfig?: (config_id: string) => Promise; + loadSingleVersion?: ( + config_id: string, + version: number, + ) => Promise; } -// Group configs by name for nested dropdown -interface ConfigGroupForDropdown { - config_id: string; - name: string; - versions: SavedConfig[]; -} - -// Provider-specific models -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' }, - // ], -}; - export default function ConfigEditorPane({ configBlob, onConfigChange, @@ -64,34 +45,76 @@ export default function ConfigEditorPane({ isSaving = false, collapsed = false, onToggle, + allConfigMeta = [], + versionItemsMap = {}, + loadVersionsForConfig, + loadSingleVersion, }: ConfigEditorPaneProps) { const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [expandedConfigId, setExpandedConfigId] = useState(null); // config group is expanded in the Load dropdown + const [loadingVersionsFor, setLoadingVersionsFor] = useState>( + new Set(), + ); // groups are currently loading their version list + + const handleOpenLoadDropdown = () => { + if (!isDropdownOpen) { + // Auto-expand the currently selected config group, load its version list if needed + if (selectedConfigId) { + const selected = savedConfigs.find((c) => c.id === selectedConfigId); + if (selected) { + setExpandedConfigId(selected.config_id); + if (!versionItemsMap[selected.config_id] && loadVersionsForConfig) { + setLoadingVersionsFor((prev) => + new Set(prev).add(selected.config_id), + ); + loadVersionsForConfig(selected.config_id).finally(() => { + setLoadingVersionsFor((prev) => { + const s = new Set(prev); + s.delete(selected.config_id); + return s; + }); + }); + } + } + } + } + setIsDropdownOpen((prev) => !prev); + }; + + const handleToggleGroup = (config_id: string) => { + if (expandedConfigId === config_id) { + setExpandedConfigId(null); + return; + } + setExpandedConfigId(config_id); + if ( + !versionItemsMap[config_id] && + !loadingVersionsFor.has(config_id) && + loadVersionsForConfig + ) { + setLoadingVersionsFor((prev) => new Set(prev).add(config_id)); + loadVersionsForConfig(config_id).finally(() => { + setLoadingVersionsFor((prev) => { + const s = new Set(prev); + s.delete(config_id); + return s; + }); + }); + } + }; const [showTooltip, setShowTooltip] = useState(null); const provider = configBlob.completion.provider; const params = configBlob.completion.params; const tools = (params.tools || []) as Tool[]; - // Group configs by config_id for nested dropdown - const configGroups = useMemo(() => { - const grouped = new Map(); - savedConfigs.forEach((config) => { - const existing = grouped.get(config.config_id) || []; - existing.push(config); - grouped.set(config.config_id, existing); - }); - return Array.from(grouped.entries()).map(([config_id, versions]) => { - const sortedVersions = versions.sort((a, b) => b.version - a.version); - return { - config_id, - name: sortedVersions[0].name, - versions: sortedVersions, - } as ConfigGroupForDropdown; - }); - }, [savedConfigs]); + // Find currently selected config from loaded set + const selectedConfig = savedConfigs.find((c) => c.id === selectedConfigId); - // Find currently selected config - const selectedConfig = savedConfigs.find(c => c.id === selectedConfigId); + // Config name hint text: use allConfigMeta for accurate new-vs-existing detection + const existingConfigForHint = configName.trim() + ? allConfigMeta.find((m) => m.name === configName.trim()) + : undefined; const handleProviderChange = (newProvider: string) => { onConfigChange({ @@ -102,13 +125,14 @@ export default function ConfigEditorPane({ provider: newProvider as any, params: { ...params, - model: MODEL_OPTIONS[newProvider as keyof typeof MODEL_OPTIONS][0].value, + model: + MODEL_OPTIONS[newProvider as keyof typeof MODEL_OPTIONS][0].value, }, }, }); }; - const handleTypeChange = (newType: 'text' | 'stt' | 'tts') => { + const handleTypeChange = (newType: "text" | "stt" | "tts") => { onConfigChange({ ...configBlob, completion: { @@ -142,8 +166,8 @@ export default function ConfigEditorPane({ const newTools = [ ...tools, { - type: 'file_search' as const, - knowledge_base_ids: [''], + type: "file_search" as const, + knowledge_base_ids: [""], max_num_results: 20, }, ]; @@ -169,7 +193,7 @@ export default function ConfigEditorPane({ const handleUpdateTool = (index: number, field: keyof Tool, value: unknown) => { const newTools = [...tools]; - if (field === 'knowledge_base_ids') { + if (field === "knowledge_base_ids") { newTools[index][field] = [value as string]; } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -188,11 +212,11 @@ export default function ConfigEditorPane({
{/* Header */} @@ -200,15 +224,18 @@ export default function ConfigEditorPane({ className="border-b flex items-center flex-shrink-0" style={{ borderColor: colors.border, - padding: collapsed ? '0' : '12px 16px', - justifyContent: collapsed ? 'center' : 'space-between', - height: collapsed ? '40px' : 'auto', - transition: 'padding 0.2s ease-in-out', + padding: collapsed ? "0" : "12px 16px", + justifyContent: collapsed ? "center" : "space-between", + height: collapsed ? "40px" : "auto", + transition: "padding 0.2s ease-in-out", }} > {/* Title - hidden when collapsed, shown first when expanded */} {!collapsed && ( -

+

Configuration

)} @@ -218,13 +245,13 @@ export default function ConfigEditorPane({ onClick={onToggle} className="rounded flex-shrink-0 flex items-center justify-center" style={{ - width: '28px', - height: '28px', - borderWidth: '1px', + width: "28px", + height: "28px", + borderWidth: "1px", borderColor: colors.border, backgroundColor: colors.bg.primary, color: colors.text.secondary, - transition: 'all 0.15s ease', + transition: "all 0.15s ease", }} onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = colors.bg.secondary; @@ -234,7 +261,7 @@ export default function ConfigEditorPane({ e.currentTarget.style.backgroundColor = colors.bg.primary; e.currentTarget.style.color = colors.text.secondary; }} - title={collapsed ? 'Show configuration' : 'Hide configuration'} + title={collapsed ? "Show configuration" : "Hide configuration"} > - + )} @@ -264,8 +296,8 @@ export default function ConfigEditorPane({ Configuration @@ -275,477 +307,666 @@ export default function ConfigEditorPane({ {/* Content - hidden when collapsed */} {!collapsed && ( -
-
- {/* Load Saved Config - Nested dropdown matching Evaluations page pattern */} -
- - - - {/* Dropdown Menu */} - {isDropdownOpen && ( -
+ + + + - {/* Grouped Configs */} - {configGroups.map((group) => ( -
- {/* Config group header */} -
+ {/* New Config Option */} +
- {/* Versions */} - {group.versions.map((version) => ( - + + {/* Grouped Configs — built from lightweight allConfigMeta */} + {allConfigMeta.map((meta) => { + const isExpanded = expandedConfigId === meta.id; + const isLoadingGroup = loadingVersionsFor.has(meta.id); + const items = versionItemsMap[meta.id] ?? []; + return ( +
+ {/* Config group header */} +
-
- {version.provider}/{version.modelName} • {formatRelativeTime(version.timestamp)} + + + + + {/* Version items — only when expanded */} + {isExpanded && + !isLoadingGroup && + items.map((item) => { + const isSelected = + selectedConfig?.config_id === meta.id && + selectedConfig?.version === item.version; + return ( + + ); + })} + {isExpanded && isLoadingGroup && ( +
+ Loading versions…
-
- {selectedConfig?.id === version.id && ( - - - )} - - ))} -
- ))} -
- )} +
+ ); + })} +
+ )} - {/* Click outside to close dropdown */} - {isDropdownOpen && ( -
setIsDropdownOpen(false)} - /> - )} -
+ {/* Click outside to close dropdown */} + {isDropdownOpen && ( +
setIsDropdownOpen(false)} + /> + )} +
- {/* Config Name */} -
- - onConfigNameChange(e.target.value)} - placeholder="e.g., my-config" - className="w-full px-3 py-2 rounded-md text-sm focus:outline-none" - style={{ - border: `1px solid ${colors.border}`, - backgroundColor: colors.bg.primary, - color: colors.text.primary, - }} - /> - {configName.trim() && (() => { - const existingConfig = savedConfigs.find(c => c.name === configName.trim()); - return ( -

- {existingConfig + {/* Config Name */} +

+ + onConfigNameChange(e.target.value)} + placeholder="e.g., my-config" + className="w-full px-3 py-2 rounded-md text-sm focus:outline-none" + style={{ + border: `1px solid ${colors.border}`, + backgroundColor: colors.bg.primary, + color: colors.text.primary, + }} + /> + {configName.trim() && ( +

+ {existingConfigForHint ? `💡 Will create a new version for "${configName}"` - : `✨ Will create a new config "${configName}"` - } + : `✨ Will create a new config "${configName}"`}

- ); - })()} -
- - {/* Provider */} -
- - -
- - {/* Type */} -
- - -

- Standard text-based LLM completion -

-
- - {/* Model */} -
- - -
+ )} +
- {/* Temperature */} -
- - handleTemperatureChange(parseFloat(e.target.value))} - className="w-full" - style={{ accentColor: colors.accent.primary }} - /> -
- 0 - 2 + {/* Provider */} +
+ +
-
- {/* Tools */} -
-
+ {/* Type */} +
- + {PROVIDER_TYPES.map((option) => ( + + ))} + +

+ Standard text-based LLM completion +

- {tools.map((tool, index) => ( -
+ + - handleUpdateTool(index, 'knowledge_base_ids', e.target.value) - } - placeholder="vs_abc123" - 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, - }} - /> -
-
-
-
+ + {/* Temperature */} +
+ + + handleTemperatureChange(parseFloat(e.target.value)) + } + className="w-full" + style={{ accentColor: colors.accent.primary }} + /> +
+ 0 + 2 +
+
+ + {/* Tools */} +
+
+ + +
+ {tools.map((tool, index) => ( +
+
+ + File Search + + +
+
+ -
setShowTooltip(index)} - onMouseLeave={() => setShowTooltip(null)} - > - + handleUpdateTool( + index, + "knowledge_base_ids", + e.target.value, + ) + } + placeholder="vs_abc123" + 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" - style={{ - border: `1px solid ${colors.border}`, - backgroundColor: colors.bg.primary, - color: colors.text.primary, - }} - />
-
- ))} -
+ ))} +
- {/* Commit Message */} -
- - onCommitMessageChange(e.target.value)} - placeholder="Describe your changes..." - className="w-full px-3 py-2 rounded-md text-sm focus:outline-none" + {/* Commit Message */} +
+ + onCommitMessageChange(e.target.value)} + placeholder="Describe your changes..." + className="w-full px-3 py-2 rounded-md text-sm focus:outline-none" + style={{ + border: `1px solid ${colors.border}`, + backgroundColor: colors.bg.primary, + color: colors.text.primary, + }} + /> +
+ + {/* Save Button */} +
- - {/* Save Button */} -
-
)}
); diff --git a/app/components/prompt-editor/DiffView.tsx b/app/components/prompt-editor/DiffView.tsx index bedc977..65cdd42 100644 --- a/app/components/prompt-editor/DiffView.tsx +++ b/app/components/prompt-editor/DiffView.tsx @@ -1,22 +1,26 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { colors } from '@/app/lib/colors'; import PromptDiffPane from './PromptDiffPane'; import ConfigDiffPane from './ConfigDiffPane'; -import { SavedConfig, formatRelativeTime } from '@/app/lib/useConfigs'; +import { SavedConfig, ConfigVersionItems } from '@/app/lib/types/configs'; +import { formatRelativeTime } from '@/app/lib/utils'; interface DiffViewProps { selectedCommit: SavedConfig; compareWith: SavedConfig | null; commits: SavedConfig[]; onCompareChange: (commit: SavedConfig | null) => void; - onLoadVersion: (versionId: string) => void; + onLoadVersion: (config: SavedConfig) => void; + loadVersionsForConfig?: (config_id: string) => Promise; // lightweight version list for a given config_id + versionItemsMap?: Record; // Lightweight version items per config_id. When provided, the compare dropdown shows ALL version + onFetchVersionDetail?: (config_id: string, version: number) => Promise; // Fetches a single version's full details } // Group configs by name for the dropdown interface ConfigGroupForCompare { config_id: string; name: string; - versions: SavedConfig[]; + items: ConfigVersionItems[]; } export default function DiffView({ @@ -24,10 +28,22 @@ export default function DiffView({ compareWith, commits, onCompareChange, - onLoadVersion + onLoadVersion, + loadVersionsForConfig, + versionItemsMap, + onFetchVersionDetail, }: DiffViewProps) { - // Group configs by config_id for nested dropdown - const configGroups = useMemo(() => { + const [isLoadingCompare, setIsLoadingCompare] = useState(false); + + // Build groups for the compare dropdown. + const configGroups = useMemo((): ConfigGroupForCompare[] => { + if (versionItemsMap && Object.keys(versionItemsMap).length > 0) { + return Object.entries(versionItemsMap).map(([config_id, items]) => { + const nameFallback = commits.find(c => c.config_id === config_id)?.name ?? config_id; + const sorted = [...items].sort((a, b) => b.version - a.version); + return { config_id, name: nameFallback, items: sorted }; + }); + } const grouped = new Map(); commits.forEach((config) => { const existing = grouped.get(config.config_id) || []; @@ -35,18 +51,47 @@ export default function DiffView({ grouped.set(config.config_id, existing); }); return Array.from(grouped.entries()).map(([config_id, versions]) => { - const sortedVersions = versions.sort((a, b) => b.version - a.version); + const sorted = versions.sort((a, b) => b.version - a.version); return { config_id, - name: sortedVersions[0].name, - versions: sortedVersions, - } as ConfigGroupForCompare; + name: sorted[0].name, + items: sorted.map(v => ({ + id: v.id, + config_id: v.config_id, + version: v.version, + commit_message: v.commit_message ?? null, + inserted_at: v.timestamp, + updated_at: v.timestamp, + })), + }; }); - }, [commits]); + }, [commits, versionItemsMap]); + + // Encode option value as "config_id:version" so we can look up or fetch on select + const encodeValue = (config_id: string, version: number) => `${config_id}:${version}`; + const decodeValue = (val: string): { config_id: string; version: number } | null => { + const idx = val.lastIndexOf(':'); + if (idx === -1) return null; + return { config_id: val.slice(0, idx), version: parseInt(val.slice(idx + 1), 10) }; + }; + const currentValue = compareWith ? encodeValue(compareWith.config_id, compareWith.version) : ''; + + const handleCompareSelect = async (rawValue: string) => { + if (!rawValue) { onCompareChange(null); return; } + const decoded = decodeValue(rawValue); + if (!decoded) return; + const { config_id, version } = decoded; + + // Fast path: already loaded + let detail = commits.find(c => c.config_id === config_id && c.version === version); + if (detail) { onCompareChange(detail); return; } - // Format timestamp - const formatTimestamp = (timestamp: string) => { - return formatRelativeTime(timestamp); + if (onFetchVersionDetail) { + setIsLoadingCompare(true); + detail = (await onFetchVersionDetail(config_id, version)) ?? undefined; + setIsLoadingCompare(false); + } + onCompareChange(detail ?? null); }; return ( @@ -60,34 +105,38 @@ export default function DiffView({ {selectedCommit.name} v{selectedCommit.version}
- {formatTimestamp(selectedCommit.timestamp)} • {selectedCommit.provider}/{selectedCommit.modelName} + {formatRelativeTime(selectedCommit.timestamp)} • {selectedCommit.provider}/{selectedCommit.modelName} {selectedCommit.commit_message && ` • ${selectedCommit.commit_message}`}