import { useCallback, useEffect, useState } from 'react' import { PageLoader } from '@/components/page-loader' import { Button } from '@/components/ui/button' import { DisclosureCaret } from '@/components/ui/disclosure-caret' import { getMemoryProviderConfig, saveMemoryProviderConfig } from '@/hermes' import { useI18n } from '@/i18n' import { localizeMemoryField } from '@/i18n/tr-memory-fields' import { SlidersHorizontal } from '@/lib/icons' import { notifyError } from '@/store/notifications' import type { MemoryProviderConfig, MemoryProviderField } from '@/types/hermes' import { ListRow, Pill } from '../primitives' import { FieldControl, FieldTitle } from './field-control' import { ProviderConfigModal } from './provider-config-modal' // Inline fields only: the compact panel must never re-write modal-owned keys. function seedValues(config: MemoryProviderConfig): Record { return Object.fromEntries( config.fields.filter(field => field.inline).map(field => [field.key, field.kind === 'secret' ? '' : field.value]) ) } export function ProviderConfigPanel({ profile, provider }: { profile?: string; provider: string }) { const { t, locale } = useI18n() const copy = t.settings.runtime.memory const [config, setConfig] = useState(null) const [loadError, setLoadError] = useState(null) const [values, setValues] = useState>({}) const [saved, setSaved] = useState>({}) const [expanded, setExpanded] = useState(true) const [showModal, setShowModal] = useState(false) const refresh = useCallback(async () => { try { const next = await getMemoryProviderConfig(provider, profile) const seed = seedValues(next) setConfig(next) setValues(seed) setSaved(seed) setLoadError(null) } catch (err) { setConfig(null) setLoadError(err instanceof Error ? err.message : copy.loadFailed) } }, [profile, provider, copy.loadFailed]) useEffect(() => { setConfig(null) void refresh() }, [refresh]) // Autosave, matching the settings page around the panel: one-key partial PUT // on commit, silent on success, no full refresh (it would reset sibling drafts). const commitField = useCallback( async (field: MemoryProviderField, value: string) => { if (value === (saved[field.key] ?? '') || (field.kind === 'secret' && !value.trim())) { return } try { await saveMemoryProviderConfig(provider, { [field.key]: value }, profile) if (field.kind === 'secret') { setValues(current => ({ ...current, [field.key]: '' })) setConfig( current => current && { ...current, fields: current.fields.map(f => (f.key === field.key ? { ...f, is_set: true } : f)) } ) } else { setSaved(current => ({ ...current, [field.key]: value })) } } catch (err) { notifyError(err, copy.saveFailed(field.label)) } }, [profile, provider, saved, copy] ) // Providers without a declared config surface (e.g. builtin) render nothing. if (config && config.fields.length === 0) { return null } if (!config) { if (loadError) { return (
{copy.loadFailed}: {loadError}
) } return } const fields = config.fields.map(field => localizeMemoryField(config.name, field, locale)) const inlineFields = fields.filter(field => field.inline) const secretFields = fields.filter(field => field.kind === 'secret') const hasFullConfig = config.fields.some(field => !field.inline) return (
{hasFullConfig && ( )}
{expanded && (
{inlineFields.map(field => (
setValues(current => ({ ...current, [field.key]: value }))} onCommit={value => void commitField(field, value)} value={values[field.key] ?? ''} /> } description={field.description} title={} />
))}
)} {hasFullConfig && ( )}
) }