"use client"; /** * Partner configuration panel: identity, soul, model, tool surface, and the * provisioned asset library (knowledge bases / skills / notebooks copied * into the partner workspace). */ import { useCallback, useEffect, useMemo, useState } from "react"; import { Loader2, Plus, Save, Trash2, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import PartnerModelSelect from "@/components/partners/PartnerModelSelect"; import { listLLMOptions, type LLMOption } from "@/lib/llm-options"; import type { LLMSelection } from "@/lib/unified-ws"; import { addPartnerAssets, getPartnerAssets, getPartnerSoul, getToolOptions, removePartnerAsset, savePartnerSoul, updatePartner, type PartnerAssets, type PartnerInfo, type ToolOptions, } from "@/lib/partners-api"; import AssetPicker, { type AssetSelection, } from "@/components/partners/AssetPicker"; import ToolPicker from "@/components/partners/ToolPicker"; import FaceEditor, { type FaceValue } from "@/components/partners/FaceEditor"; import SoulEditor from "@/components/partners/SoulEditor"; function Section({ title, description, children, action, }: { title: string; description?: string; children: React.ReactNode; action?: React.ReactNode; }) { return (

{title}

{description && (

{description}

)}
{action}
{children}
); } export default function PartnerConfigure({ partner, onToast, onUpdated, }: { partner: PartnerInfo; onToast: (msg: string) => void; onUpdated: () => void; }) { const { t } = useTranslation(); const partnerId = partner.partner_id; // Identity const [name, setName] = useState(partner.name); const [description, setDescription] = useState(partner.description ?? ""); const [face, setFace] = useState({ emoji: partner.emoji ?? "", color: partner.color ?? "", avatar: partner.avatar ?? "", }); const [language, setLanguage] = useState(partner.language ?? ""); const [savingIdentity, setSavingIdentity] = useState(false); // Soul const [soul, setSoul] = useState(""); const [soulLoaded, setSoulLoaded] = useState(false); const [savingSoul, setSavingSoul] = useState(false); // Model const [llmOptions, setLLMOptions] = useState([]); const [activeLLMDefault, setActiveLLMDefault] = useState( null, ); const [llmLoading, setLLMLoading] = useState(true); const [llmError, setLLMError] = useState(false); const [selection, setSelection] = useState( partner.llm_selection ?? null, ); const [backupSelection, setBackupSelection] = useState( partner.backup_llm_selection ?? null, ); // Tools const [toolOptions, setToolOptions] = useState(null); const [enabledTools, setEnabledTools] = useState([]); const [builtinTools, setBuiltinTools] = useState([]); const [mcpTools, setMcpTools] = useState([]); const [savingTools, setSavingTools] = useState(false); // Assets const [assets, setAssets] = useState(null); const [showAssetPicker, setShowAssetPicker] = useState(false); const [pendingAssets, setPendingAssets] = useState({ knowledge_bases: [], skills: [], notebooks: [], }); const [addingAssets, setAddingAssets] = useState(false); useEffect(() => { void getPartnerSoul(partnerId) .then((content) => { setSoul(content); setSoulLoaded(true); }) .catch(() => setSoulLoaded(true)); void getPartnerAssets(partnerId) .then(setAssets) .catch(() => {}); void (async () => { try { const payload = await listLLMOptions(); setLLMOptions(payload.options); setActiveLLMDefault(payload.active); setLLMError(false); } catch { setLLMError(true); } finally { setLLMLoading(false); } })(); void getToolOptions() .then((options) => { setToolOptions(options); setEnabledTools( partner.enabled_tools ?? options.tools.map((tool) => tool.name), ); setBuiltinTools( partner.builtin_tools ?? options.builtin_tools.map((tool) => tool.name), ); setMcpTools( partner.mcp_tools ?? options.mcp_tools.map((tool) => tool.name), ); }) .catch(() => {}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [partnerId]); const saveIdentity = async () => { setSavingIdentity(true); try { await updatePartner(partnerId, { name, description, emoji: face.emoji, color: face.color, avatar: face.avatar, language, }); onToast(t("Saved")); onUpdated(); } catch (e) { onToast(e instanceof Error ? e.message : t("Save failed")); } finally { setSavingIdentity(false); } }; const saveSoul = async () => { setSavingSoul(true); try { await savePartnerSoul(partnerId, soul); onToast(t("Soul saved")); } catch (e) { onToast(e instanceof Error ? e.message : t("Save failed")); } finally { setSavingSoul(false); } }; const saveModel = async (next: LLMSelection | null) => { setSelection(next); try { await updatePartner(partnerId, { llm_selection: next }); onToast(t("Model updated — applies from the next message")); onUpdated(); } catch (e) { onToast(e instanceof Error ? e.message : t("Save failed")); } }; const saveBackupModel = async (next: LLMSelection | null) => { setBackupSelection(next); try { await updatePartner(partnerId, { backup_llm_selection: next }); onToast(t("Model updated — applies from the next message")); onUpdated(); } catch (e) { onToast(e instanceof Error ? e.message : t("Save failed")); } }; const saveTools = async () => { setSavingTools(true); try { await updatePartner(partnerId, { enabled_tools: enabledTools, builtin_tools: builtinTools, mcp_tools: mcpTools, }); onToast(t("Tools updated — applies from the next message")); onUpdated(); } catch (e) { onToast(e instanceof Error ? e.message : t("Save failed")); } finally { setSavingTools(false); } }; const submitAssets = async () => { setAddingAssets(true); try { const result = await addPartnerAssets(partnerId, pendingAssets); setAssets(result.assets); if (result.errors.length > 0) { onToast( t("Some items failed: {{names}}", { names: result.errors.map((e) => e.name).join(", "), }), ); } else { onToast(t("Assets added")); } setPendingAssets({ knowledge_bases: [], skills: [], notebooks: [] }); setShowAssetPicker(false); } catch (e) { onToast(e instanceof Error ? e.message : t("Save failed")); } finally { setAddingAssets(false); } }; const removeAsset = useCallback( async (assetType: "knowledge_base" | "skill" | "notebook", id: string) => { try { const result = await removePartnerAsset(partnerId, assetType, id); setAssets(result.assets); } catch (e) { onToast(e instanceof Error ? e.message : t("Remove failed")); } }, [partnerId, onToast, t], ); const assetRows = useMemo(() => { if (!assets) return []; return [ ...assets.knowledge_bases.map((kb) => ({ type: "knowledge_base" as const, id: kb.name, label: kb.name, kind: t("Knowledge base"), })), ...assets.skills.map((skill) => ({ type: "skill" as const, id: skill.name, label: skill.name, kind: t("Skill"), })), ...assets.notebooks.map((nb) => ({ type: "notebook" as const, id: nb.id, label: nb.name || nb.id, kind: t("Notebook"), })), ]; }, [assets, t]); const pendingCount = pendingAssets.knowledge_bases.length + pendingAssets.skills.length + pendingAssets.notebooks.length; return (
void saveIdentity()} disabled={savingIdentity || !name.trim()} className="inline-flex items-center gap-1.5 rounded-lg bg-[var(--primary)] px-3 py-1.5 text-[12px] font-medium text-[var(--primary-foreground)] disabled:opacity-40" > {savingIdentity ? ( ) : ( )} {t("Save")} } >
setName(e.target.value)} className="w-full rounded-lg border border-[var(--border)] bg-transparent px-3 py-1.5 text-[13px] outline-none focus:border-[var(--ring)]" />
setDescription(e.target.value)} className="w-full rounded-lg border border-[var(--border)] bg-transparent px-3 py-1.5 text-[13px] outline-none focus:border-[var(--ring)]" />
{t("Face")}
void saveSoul()} disabled={savingSoul || !soulLoaded} className="inline-flex items-center gap-1.5 rounded-lg bg-[var(--primary)] px-3 py-1.5 text-[12px] font-medium text-[var(--primary-foreground)] disabled:opacity-40" > {savingSoul ? ( ) : ( )} {t("Save")} } >
void saveModel(next)} />
void saveBackupModel(next)} />
void saveTools()} disabled={savingTools || !toolOptions} className="inline-flex items-center gap-1.5 rounded-lg bg-[var(--primary)] px-3 py-1.5 text-[12px] font-medium text-[var(--primary-foreground)] disabled:opacity-40" > {savingTools ? ( ) : ( )} {t("Save")} } >
setShowAssetPicker((v) => !v)} className="inline-flex items-center gap-1.5 rounded-lg border border-[var(--border)] px-3 py-1.5 text-[12px] font-medium text-[var(--foreground)] hover:border-[var(--ring)]" > {showAssetPicker ? ( ) : ( )} {showAssetPicker ? t("Cancel") : t("Add")} } > {showAssetPicker && (
kb.name) ?? [], skills: assets?.skills.map((skill) => skill.name) ?? [], notebooks: assets?.notebooks.map((nb) => nb.id) ?? [], }} />
)} {assetRows.length === 0 ? (

{t( "Nothing assigned yet — this partner only knows what you tell it.", )}

) : (
    {assetRows.map((row) => (
  • {row.label} {row.kind}
  • ))}
)}
); }