Newer
Older
cortex-hub / frontend / src / features / settings / components / shared / ProviderPanel.js
import React from 'react';

const ProviderPanel = ({ 
  id, 
  prefs, 
  sectionKey, 
  status, 
  providers,
  expandedProvider,
  setExpandedProvider,
  verifying,
  handleVerifyProvider,
  handleDeleteProvider,
  handleConfigChange,
  labelClass,
  inputClass,
  fetchedModels
}) => {
  const isExpanded = expandedProvider === `${sectionKey}_${id}`;
  const providerType = prefs.provider_type || id.split('_')[0];
  const isVerifying = verifying === `${sectionKey}_${id}`;

  return (
    <div className={`bg-white dark:bg-gray-800 border ${isExpanded ? 'border-indigo-300 dark:border-indigo-700 ring-4 ring-indigo-50 dark:ring-indigo-900/20' : 'border-gray-100 dark:border-gray-700'} rounded-3xl overflow-hidden shadow-sm transition-all duration-300`}>
      <div
        className={`px-6 py-5 flex items-center justify-between cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${status === 'error' ? 'bg-red-50/30 dark:bg-red-900/10' : ''}`}
        onClick={() => setExpandedProvider(isExpanded ? null : `${sectionKey}_${id}`)}
      >
        <div className="flex items-center gap-4">
          <div className={`w-10 h-10 rounded-2xl flex items-center justify-center font-black text-xs ${status === 'success' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300' : (status === 'error' ? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' : 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400')}`}>
            {id.substring(0, 2).toUpperCase()}
          </div>
          <div>
            <h4 className="font-black text-gray-900 dark:text-white uppercase tracking-tight flex items-center gap-2">
              {id}
              {status === 'success' && <svg className="w-4 h-4 text-emerald-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /></svg>}
              {status === 'error' && <svg className="w-4 h-4 text-red-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" /></svg>}
            </h4>
            <p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest">{providerType}</p>
          </div>
        </div>
        <div className="flex items-center gap-3">
          <button
            onClick={(e) => { e.stopPropagation(); handleVerifyProvider(sectionKey, id, prefs); }}
            disabled={isVerifying}
            className={`px-3 py-1.5 rounded-lg text-[9px] font-black uppercase tracking-widest transition-all ${isVerifying ? 'bg-gray-100 text-gray-400 animate-pulse' : 'bg-indigo-50 text-indigo-600 hover:bg-indigo-100 dark:bg-indigo-900/30 dark:text-indigo-400'}`}
          >
            {isVerifying ? 'Verifying...' : 'Test Connection'}
          </button>
          <button
            onClick={(e) => { e.stopPropagation(); handleDeleteProvider(sectionKey, id); }}
            className="p-1 px-2 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
          >
            <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
          </button>
          <svg className={`w-5 h-5 text-gray-400 transition-transform duration-300 ${isExpanded ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M19 9l-7 7-7-7" /></svg>
        </div>
      </div>

      {isExpanded && (
        <div className="p-6 border-t border-gray-100 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-900/30 space-y-6 animate-slide-down">
          <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
            <div className="space-y-1.5">
              <label className={labelClass}>API Key / Secret</label>
              <div className="relative">
                <input
                  type="password"
                  id={`api-key-${sectionKey}-${id}`}
                  name={`api-key-${sectionKey}-${id}`}
                  value={prefs.api_key || ''}
                  onChange={e => handleConfigChange(sectionKey, 'providers', { ...providers, [id]: { ...prefs, api_key: e.target.value } }, id)}
                  placeholder="••••••••••••••••"
                  className={inputClass}
                  autoComplete="new-password"
                />
              </div>
            </div>
            <div className="space-y-1.5">
              <label className={labelClass}>{sectionKey === 'tts' && providerType === 'gcloud_tts' ? 'Default Voice' : 'Primary Model'}</label>
              <input
                type="text"
                id={`model-${sectionKey}-${id}`}
                name={`model-${sectionKey}-${id}`}
                list={`models-${sectionKey}-${id}`}
                value={(() => {
                  const val = (sectionKey === 'tts' && providerType === 'gcloud_tts' ? prefs.voice : prefs.model);
                  if (!val) return '';
                  return typeof val === 'object' ? (val.model_name || val.id || val.voice_id || '') : val;
                })() || ''}
                onChange={e => handleConfigChange(sectionKey, 'providers', { ...providers, [id]: { ...prefs, [sectionKey === 'tts' && providerType === 'gcloud_tts' ? 'voice' : 'model']: e.target.value } }, id)}
                placeholder="e.g. gpt-4, whisper-1"
                className={inputClass}
                autoComplete="off"
              />
              <datalist id={`models-${sectionKey}-${id}`}>
                {(fetchedModels[`${sectionKey}_${providerType}`] || []).map(m => (
                  <option key={typeof m === 'string' ? m : m.model_name} value={typeof m === 'string' ? m : m.model_name} />
                ))}
              </datalist>
            </div>
            <div className="space-y-1.5">
              <label className={labelClass}>Provider Type (System)</label>
              <input type="text" value={providerType} readOnly className={`${inputClass} opacity-50 cursor-not-allowed`} />
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

export default ProviderPanel;