import { useState, useEffect, useRef } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import { toast } from 'sonner'; import { Pencil, Trash2, Plus, X, Loader2, ArrowLeft } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { useGlobal } from 'hooks/useGlobal'; import { useClient } from 'hooks/useClient'; import { usePiChat, EmbeddableChat } from 'officerdev'; import { useResources, type ResourceDetail, type PingResult } from 'state/useResources'; import { FrontmatterBlock } from '../../CapabilityPage'; const isSensitiveKey = (key: string) => /key|secret|password|token/i.test(key); type ConfigEditorProps = { resourceName: string; config: Record; onSaved: () => void; }; const ConfigEditor = ({ resourceName, config, onSaved }: ConfigEditorProps) => { const { saveConfig, pingResource } = useResources(); const configEntries = Object.entries(config); const [fields, setFields] = useState<[string, string][]>(configEntries); const [newKey, setNewKey] = useState(''); const [newValue, setNewValue] = useState(''); const [pinging, setPinging] = useState(false); const [pingResult, setPingResult] = useState(null); const [saving, setSaving] = useState(false); useEffect(() => { setFields(Object.entries(config)); }, [config]); const hasUrl = fields.some(([k, v]) => k === 'url' && v); const handleAddField = () => { const key = newKey.trim(); if (!key || fields.some(([k]) => k === key)) return; setFields([...fields, [key, newValue]]); setNewKey(''); setNewValue(''); }; const handleRemoveField = (index: number) => { setFields(fields.filter((_, i) => i !== index)); }; const handleFieldValue = (index: number, value: string) => { setFields(fields.map((f, i) => (i === index ? [f[0]!, value] : f))); }; const handlePing = async () => { const url = fields.find(([k]) => k === 'url')?.[1]; if (!url) return; setPinging(true); setPingResult(null); try { const result = await pingResource(resourceName, url); setPingResult(result); } catch { setPingResult({ reachable: false, latencyMs: null }); } finally { setPinging(false); } }; const handleSave = async () => { setSaving(true); try { const oldKeys = Object.keys(config); const newKeys = new Set(fields.map(([k]) => k)); const patch: Record = {}; for (const [key, value] of fields) { patch[key] = value; } for (const key of oldKeys) { if (!newKeys.has(key)) patch[key] = null; } await saveConfig(resourceName, patch); onSaved(); toast.success('Configuration saved'); } catch { toast.error('Failed to save configuration'); } finally { setSaving(false); } }; return (

Configuration

{fields.map(([key, value], index) => (
handleFieldValue(index, ev.target.value)} type={isSensitiveKey(key) ? 'password' : 'text'} className="h-8 text-xs flex-1" />
))}
setNewKey(ev.target.value)} placeholder="key" className="h-8 text-xs w-28 shrink-0" onKeyDown={(ev) => ev.key === 'Enter' && handleAddField()} /> setNewValue(ev.target.value)} placeholder="value" className="h-8 text-xs flex-1" onKeyDown={(ev) => ev.key === 'Enter' && handleAddField()} />
{hasUrl && ( )} {pingResult && ( {pingResult.reachable ? `Reachable (${pingResult.latencyMs}ms)` : 'Unreachable'} )}
); }; type ResourceChatProps = { detail: ResourceDetail; isNew?: boolean; onResponseEnd: () => void; }; const ResourceChat = ({ detail, isNew, onResponseEnd }: ResourceChatProps) => { const promptFrontmatter = `\nconfig file: ${detail.configPath}\nresource file: ${detail.filePath}\nguide: ${detail.guidePath}\n\nYou are helping configure a resource. Read the GUIDE.md for instructions on how to help. Read the RESOURCE.md for context about what this resource is. Write config values to the config.json file.\n`; const defaultInput = isNew ? 'Help me set up this new resource' : 'Help me configure this resource'; const pi = usePiChat(undefined, undefined, { replaceUrl: false }); const onResponseEndRef = useRef(onResponseEnd); onResponseEndRef.current = onResponseEnd; const wasGenerating = useRef(false); useEffect(() => { if (wasGenerating.current && !pi.isGenerating) { onResponseEndRef.current(); } wasGenerating.current = pi.isGenerating; }, [pi.isGenerating]); return ( ); }; export const Resources = () => { const client = useClient(); const qc = useQueryClient(); const [selectedName] = useGlobal('RESOURCE_SELECTED', null); const [editing, setEditing] = useState(false); const [isNew, setIsNew] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(false); const [showDetail, setShowDetail] = useState(false); const { data: detail, refetch } = useQuery({ queryKey: ['RESOURCES', selectedName], queryFn: () => client.get(`/server-settings/resources/${selectedName}`), enabled: !!selectedName, }); useEffect(() => { if (selectedName) { setShowDetail(true); setEditing(false); } }, [selectedName]); const handleDelete = async () => { if (!selectedName) return; try { await client.delete(`/server-settings/resources/${selectedName}`); setDeleteConfirm(false); setEditing(false); await qc.invalidateQueries({ queryKey: ['RESOURCES'] }); } catch { toast.error('Failed to delete resource'); } }; const canDelete = detail && detail.scope === 'global' && !detail.filePath.includes('/seed/'); if (!selectedName || !detail) { return (

Select a resource to view its configuration

); } return ( <>
{/* Detail panel */}
{detail.name} {detail.scope} {canDelete && ( )}
{detail.rawFrontmatter && } {detail.body && (
{detail.body}
)} refetch()} />
{/* Chat panel */} {editing && (
{detail.name} — Chat
{ refetch(); qc.invalidateQueries({ queryKey: ['RESOURCES'] }); }} />
)}
Delete Resource Are you sure you want to delete "{detail.name}"? This action cannot be undone.
); };