|
|
@@ -102,6 +102,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
}, [activeTab, currentUser]);
|
|
|
|
|
|
const [kbSettings, setKbSettings] = useState<any>(null);
|
|
|
+ const [localKbSettings, setLocalKbSettings] = useState<any>(null);
|
|
|
+ const [isSavingKbSettings, setIsSavingKbSettings] = useState(false);
|
|
|
const fetchKnowledgeBaseSettings = async () => {
|
|
|
if (!authToken) return;
|
|
|
setIsLoading(true);
|
|
|
@@ -112,6 +114,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
if (res.ok) {
|
|
|
const data = await res.json();
|
|
|
setKbSettings(data);
|
|
|
+ setLocalKbSettings(data);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error(error);
|
|
|
@@ -120,24 +123,36 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const handleUpdateKbSettings = async (key: string, value: any) => {
|
|
|
- if (!authToken) return;
|
|
|
- const newSettings = { ...kbSettings, [key]: value };
|
|
|
+ const handleUpdateKbSettings = (key: string, value: any) => {
|
|
|
+ setLocalKbSettings((prev: any) => ({ ...prev, [key]: value }));
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSaveKbSettings = async () => {
|
|
|
+ if (!authToken || !localKbSettings) return;
|
|
|
+ setIsSavingKbSettings(true);
|
|
|
try {
|
|
|
const res = await fetch('/api/v1/admin/settings', {
|
|
|
method: 'PUT',
|
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
|
|
|
- body: JSON.stringify(newSettings)
|
|
|
+ body: JSON.stringify(localKbSettings)
|
|
|
});
|
|
|
if (res.ok) {
|
|
|
- setKbSettings(newSettings);
|
|
|
- showSuccess('Knowledge Base settings updated');
|
|
|
+ setKbSettings(localKbSettings);
|
|
|
+ showSuccess('Knowledge Base settings saved');
|
|
|
+ } else {
|
|
|
+ showError('Failed to save settings');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- showError('Failed to update settings');
|
|
|
+ showError('Error saving settings');
|
|
|
+ } finally {
|
|
|
+ setIsSavingKbSettings(false);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const handleCancelKbSettings = () => {
|
|
|
+ setLocalKbSettings(kbSettings);
|
|
|
+ };
|
|
|
+
|
|
|
const fetchSettingsAndGroups = async () => {
|
|
|
if (!authToken) return;
|
|
|
setIsSettingsLoading(true);
|
|
|
@@ -338,8 +353,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const handleBindAdmin = async (tenantId: string, userId: string) => {
|
|
|
- if (!userId) return;
|
|
|
+ const handleBindAdmin = async (tenantId: string, userId: string): Promise<boolean> => {
|
|
|
+ if (!userId) return false;
|
|
|
try {
|
|
|
const res = await fetch(`/api/v1/tenants/${tenantId}/admin`, {
|
|
|
method: 'PUT',
|
|
|
@@ -353,11 +368,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
fetchTenantsData(),
|
|
|
fetchUsers()
|
|
|
]);
|
|
|
+ return true;
|
|
|
} else {
|
|
|
showError('Failed to bind admin');
|
|
|
+ return false;
|
|
|
}
|
|
|
} catch (e) {
|
|
|
showError('Error binding admin');
|
|
|
+ return false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -823,17 +841,17 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<td className="px-6 py-4 text-xs font-mono text-slate-500">{t.domain || '-'}</td>
|
|
|
<td className="px-6 py-4 text-xs">
|
|
|
{(() => {
|
|
|
- const admin = users.find((u: any) =>
|
|
|
- (u.tenantId === t.id || u.tenant_id === t.id || (t.name === 'Default' && (u.tenantId === 'default' || u.tenant_id === 'default'))) &&
|
|
|
- (u.role === 'TENANT_ADMIN' || u.isAdmin)
|
|
|
- );
|
|
|
- if (admin) {
|
|
|
+ // Search admin in tenant members - the source of truth for multi-tenancy
|
|
|
+ const adminMember = t.members?.find((m: any) => m.role === 'TENANT_ADMIN' || m.role === 'SUPER_ADMIN');
|
|
|
+ const adminUser = adminMember?.user;
|
|
|
+
|
|
|
+ if (adminUser) {
|
|
|
return (
|
|
|
<div className="flex items-center gap-1.5 text-slate-600 font-medium">
|
|
|
<div className="w-5 h-5 rounded-full bg-indigo-50 flex items-center justify-center">
|
|
|
<User className="w-3 h-3 text-indigo-500" />
|
|
|
</div>
|
|
|
- {admin.username}
|
|
|
+ {adminUser.username}
|
|
|
<button
|
|
|
onClick={() => {
|
|
|
setBindingTenantId(t.id);
|
|
|
@@ -986,9 +1004,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
.map(u => (
|
|
|
<button
|
|
|
key={u.id}
|
|
|
- onClick={() => {
|
|
|
- handleBindAdmin(bindingTenantId, u.id);
|
|
|
- setBindingTenantId(null);
|
|
|
+ onClick={async () => {
|
|
|
+ const success = await handleBindAdmin(bindingTenantId, u.id);
|
|
|
+ if (success) setBindingTenantId(null);
|
|
|
}}
|
|
|
className="w-full flex items-center justify-between p-4 bg-slate-50/50 hover:bg-indigo-50 border border-slate-200/50 hover:border-indigo-200 rounded-[1.5rem] transition-all group"
|
|
|
>
|
|
|
@@ -1030,8 +1048,27 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
);
|
|
|
const renderKnowledgeBaseTab = () => (
|
|
|
<div className="space-y-8 animate-in slide-in-from-right duration-300 w-full max-w-5xl pb-10">
|
|
|
- {kbSettings && (
|
|
|
+ {localKbSettings && (
|
|
|
<>
|
|
|
+ {/* Save/Cancel Bar */}
|
|
|
+ <div className="flex justify-end gap-3 sticky top-0 z-20 py-4 bg-white/50 backdrop-blur-sm border-b border-slate-100 mb-6">
|
|
|
+ <button
|
|
|
+ onClick={handleCancelKbSettings}
|
|
|
+ disabled={isSavingKbSettings || JSON.stringify(localKbSettings) === JSON.stringify(kbSettings)}
|
|
|
+ className="px-6 py-2 text-sm font-bold text-slate-500 hover:text-slate-700 disabled:opacity-30"
|
|
|
+ >
|
|
|
+ Cancel
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={handleSaveKbSettings}
|
|
|
+ disabled={isSavingKbSettings || JSON.stringify(localKbSettings) === JSON.stringify(kbSettings)}
|
|
|
+ className="flex items-center gap-2 px-8 py-2 bg-indigo-600 text-white text-sm font-black uppercase tracking-widest rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 transition-all active:scale-95 disabled:opacity-50"
|
|
|
+ >
|
|
|
+ {isSavingKbSettings ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
|
|
|
+ Save Changes
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
{/* Model Configuration */}
|
|
|
<section className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-sm space-y-6">
|
|
|
<div className="flex items-center gap-3 text-slate-900 font-black uppercase tracking-widest text-[11px] border-b border-slate-100 pb-4">
|
|
|
@@ -1044,7 +1081,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div>
|
|
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Default LLM Model</label>
|
|
|
<select
|
|
|
- value={kbSettings.selectedLLMId || ''}
|
|
|
+ value={localKbSettings.selectedLLMId || ''}
|
|
|
onChange={(e) => handleUpdateKbSettings('selectedLLMId', e.target.value)}
|
|
|
className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all cursor-pointer appearance-none"
|
|
|
>
|
|
|
@@ -1058,7 +1095,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div>
|
|
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Embedding Model</label>
|
|
|
<select
|
|
|
- value={kbSettings.selectedEmbeddingId || ''}
|
|
|
+ value={localKbSettings.selectedEmbeddingId || ''}
|
|
|
onChange={(e) => handleUpdateKbSettings('selectedEmbeddingId', e.target.value)}
|
|
|
className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all"
|
|
|
>
|
|
|
@@ -1071,7 +1108,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div>
|
|
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Rerank Model</label>
|
|
|
<select
|
|
|
- value={kbSettings.selectedRerankId || ''}
|
|
|
+ value={localKbSettings.selectedRerankId || ''}
|
|
|
onChange={(e) => handleUpdateKbSettings('selectedRerankId', e.target.value)}
|
|
|
className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all"
|
|
|
>
|
|
|
@@ -1097,14 +1134,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div>
|
|
|
<div className="flex justify-between mb-3 px-1">
|
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Temperature</label>
|
|
|
- <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{kbSettings.temperature}</span>
|
|
|
+ <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{localKbSettings.temperature}</span>
|
|
|
</div>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="0"
|
|
|
max="1"
|
|
|
step="0.1"
|
|
|
- value={kbSettings.temperature || 0.7}
|
|
|
+ value={localKbSettings.temperature || 0.7}
|
|
|
onChange={(e) => handleUpdateKbSettings('temperature', parseFloat(e.target.value))}
|
|
|
className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
|
|
|
/>
|
|
|
@@ -1117,7 +1154,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Max Response Tokens</label>
|
|
|
<input
|
|
|
type="number"
|
|
|
- value={kbSettings.maxTokens || 2000}
|
|
|
+ value={localKbSettings.maxTokens || 2000}
|
|
|
onChange={(e) => handleUpdateKbSettings('maxTokens', parseInt(e.target.value))}
|
|
|
className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all"
|
|
|
/>
|
|
|
@@ -1138,14 +1175,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div>
|
|
|
<div className="flex justify-between mb-3 px-1">
|
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Top-K Results</label>
|
|
|
- <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{kbSettings.topK}</span>
|
|
|
+ <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{localKbSettings.topK}</span>
|
|
|
</div>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="1"
|
|
|
max="50"
|
|
|
step="1"
|
|
|
- value={kbSettings.topK || 10}
|
|
|
+ value={localKbSettings.topK || 10}
|
|
|
onChange={(e) => handleUpdateKbSettings('topK', parseInt(e.target.value))}
|
|
|
className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
|
|
|
/>
|
|
|
@@ -1153,14 +1190,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div>
|
|
|
<div className="flex justify-between mb-3 px-1">
|
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Similarity Threshold</label>
|
|
|
- <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{kbSettings.similarityThreshold}</span>
|
|
|
+ <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{localKbSettings.similarityThreshold}</span>
|
|
|
</div>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="0"
|
|
|
max="1"
|
|
|
step="0.05"
|
|
|
- value={kbSettings.similarityThreshold || 0.5}
|
|
|
+ value={localKbSettings.similarityThreshold || 0.5}
|
|
|
onChange={(e) => handleUpdateKbSettings('similarityThreshold', parseFloat(e.target.value))}
|
|
|
className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
|
|
|
/>
|
|
|
@@ -1174,14 +1211,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div className="text-[10px] text-slate-400 font-medium">Combine vector and full-text search results.</div>
|
|
|
</div>
|
|
|
<button
|
|
|
- onClick={() => handleUpdateKbSettings('enableFullTextSearch', !kbSettings.enableFullTextSearch)}
|
|
|
- className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${kbSettings.enableFullTextSearch ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
|
|
|
+ onClick={() => handleUpdateKbSettings('enableFullTextSearch', !localKbSettings.enableFullTextSearch)}
|
|
|
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${localKbSettings.enableFullTextSearch ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
|
|
|
>
|
|
|
- <span className={`${kbSettings.enableFullTextSearch ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
|
|
|
+ <span className={`${localKbSettings.enableFullTextSearch ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
- {kbSettings.enableFullTextSearch && (
|
|
|
+ {localKbSettings.enableFullTextSearch && (
|
|
|
<motion.div
|
|
|
initial={{ opacity: 0, y: -10 }}
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
@@ -1189,14 +1226,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
>
|
|
|
<div className="flex justify-between mb-2 px-1">
|
|
|
<label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Hybrid Weight (Vector vs Text)</label>
|
|
|
- <span className="text-sm font-black text-indigo-600">{kbSettings.hybridVectorWeight || 0.5}</span>
|
|
|
+ <span className="text-sm font-black text-indigo-600">{localKbSettings.hybridVectorWeight || 0.5}</span>
|
|
|
</div>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="0"
|
|
|
max="1"
|
|
|
step="0.05"
|
|
|
- value={kbSettings.hybridVectorWeight || 0.5}
|
|
|
+ value={localKbSettings.hybridVectorWeight || 0.5}
|
|
|
onChange={(e) => handleUpdateKbSettings('hybridVectorWeight', parseFloat(e.target.value))}
|
|
|
className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
|
|
|
/>
|
|
|
@@ -1214,10 +1251,10 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div className="text-[10px] text-slate-400 font-medium">Rewrites query for better recall.</div>
|
|
|
</div>
|
|
|
<button
|
|
|
- onClick={() => handleUpdateKbSettings('enableQueryExpansion', !kbSettings.enableQueryExpansion)}
|
|
|
- className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${kbSettings.enableQueryExpansion ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
|
|
|
+ onClick={() => handleUpdateKbSettings('enableQueryExpansion', !localKbSettings.enableQueryExpansion)}
|
|
|
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${localKbSettings.enableQueryExpansion ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
|
|
|
>
|
|
|
- <span className={`${kbSettings.enableQueryExpansion ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
|
|
|
+ <span className={`${localKbSettings.enableQueryExpansion ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
@@ -1227,10 +1264,10 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div className="text-[10px] text-slate-400 font-medium">Hypothetical Document Embeddings.</div>
|
|
|
</div>
|
|
|
<button
|
|
|
- onClick={() => handleUpdateKbSettings('enableHyDE', !kbSettings.enableHyDE)}
|
|
|
- className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${kbSettings.enableHyDE ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
|
|
|
+ onClick={() => handleUpdateKbSettings('enableHyDE', !localKbSettings.enableHyDE)}
|
|
|
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${localKbSettings.enableHyDE ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
|
|
|
>
|
|
|
- <span className={`${kbSettings.enableHyDE ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
|
|
|
+ <span className={`${localKbSettings.enableHyDE ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -1241,14 +1278,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div className="text-[10px] text-slate-400 font-medium">Re-score search results for higher accuracy.</div>
|
|
|
</div>
|
|
|
<button
|
|
|
- onClick={() => handleUpdateKbSettings('enableRerank', !kbSettings.enableRerank)}
|
|
|
- className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${kbSettings.enableRerank ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
|
|
|
+ onClick={() => handleUpdateKbSettings('enableRerank', !localKbSettings.enableRerank)}
|
|
|
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${localKbSettings.enableRerank ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
|
|
|
>
|
|
|
- <span className={`${kbSettings.enableRerank ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
|
|
|
+ <span className={`${localKbSettings.enableRerank ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
- {kbSettings.enableRerank && (
|
|
|
+ {localKbSettings.enableRerank && (
|
|
|
<motion.div
|
|
|
initial={{ opacity: 0, y: -10 }}
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
@@ -1256,14 +1293,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
>
|
|
|
<div className="flex justify-between mb-2 px-1">
|
|
|
<label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Rerank Similarity Threshold</label>
|
|
|
- <span className="text-sm font-black text-indigo-600">{kbSettings.rerankSimilarityThreshold || 0.5}</span>
|
|
|
+ <span className="text-sm font-black text-indigo-600">{localKbSettings.rerankSimilarityThreshold || 0.5}</span>
|
|
|
</div>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="0"
|
|
|
max="1"
|
|
|
step="0.05"
|
|
|
- value={kbSettings.rerankSimilarityThreshold || 0.5}
|
|
|
+ value={localKbSettings.rerankSimilarityThreshold || 0.5}
|
|
|
onChange={(e) => handleUpdateKbSettings('rerankSimilarityThreshold', parseFloat(e.target.value))}
|
|
|
className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
|
|
|
/>
|