resources

This commit is contained in:
2026-02-24 16:44:22 +00:00
parent d6ffe43a11
commit 05f0d0e8f7
39 changed files with 1385 additions and 1057 deletions
@@ -1,42 +1,66 @@
import { useState } from 'react';
import { Server, Wrench, Loader2, RefreshCw, Trash2 } from 'lucide-react';
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 { useGlobal } from 'hooks/useGlobal';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { Pencil, Trash2, Plus, X, Loader2, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { useResources, getResourceCategory, type Resource, type PingResult } from 'state/useResources';
import { RUN_COMMAND_CHANNEL, type RunCommandState } from './run-command-channel';
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';
type ConnectionSectionProps = {
resource: Resource;
const isSensitiveKey = (key: string) => /key|secret|password|token/i.test(key);
type ConfigEditorProps = {
resourceName: string;
config: Record<string, string>;
onSaved: () => void;
};
const ConnectionSection = ({ resource }: ConnectionSectionProps) => {
const { saveConnectionConfig, pingResource } = useResources();
const [url, setUrl] = useState(resource.connectionConfig?.url ?? '');
const [apiKey, setApiKey] = useState(resource.connectionConfig?.credentials?.apiKey ?? '');
const [username, setUsername] = useState(resource.connectionConfig?.credentials?.username ?? '');
const [password, setPassword] = useState(resource.connectionConfig?.credentials?.password ?? '');
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<PingResult | null>(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(resource.id, url);
const result = await pingResource(resourceName, url);
setPingResult(result);
} catch {
setPingResult({ reachable: false, latencyMs: null });
@@ -48,74 +72,83 @@ const ConnectionSection = ({ resource }: ConnectionSectionProps) => {
const handleSave = async () => {
setSaving(true);
try {
const credentials =
apiKey || username || password
? { apiKey: apiKey || undefined, username: username || undefined, password: password || undefined }
: undefined;
await saveConnectionConfig(resource.id, { url, credentials });
const oldKeys = Object.keys(config);
const newKeys = new Set(fields.map(([k]) => k));
const patch: Record<string, string | null> = {};
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);
}
};
const hasCredentials = !!(
resource.connectionConfig?.credentials?.apiKey || resource.connectionConfig?.credentials?.username
);
return (
<div className="mb-6">
<h3 className="text-sm font-semibold text-duck-dark mb-3">Connection</h3>
<div className="flex flex-col gap-3">
<div>
<label className="text-xs text-duck-dark/50 mb-1 block">Base URL</label>
<Input
value={url}
onChange={(ev) => setUrl(ev.target.value)}
placeholder="http://127.0.0.1:64202"
className="h-8 text-xs"
/>
</div>
{(hasCredentials || apiKey) && (
<div>
<label className="text-xs text-duck-dark/50 mb-1 block">API Key</label>
<h3 className="text-sm font-semibold text-duck-dark mb-3">Configuration</h3>
<div className="flex flex-col gap-2">
{fields.map(([key, value], index) => (
<div key={key} className="flex items-center gap-2">
<label className="text-xs text-duck-dark/50 w-28 shrink-0 truncate" title={key}>
{key}
</label>
<Input
value={apiKey}
onChange={(ev) => setApiKey(ev.target.value)}
placeholder="Optional"
type="password"
className="h-8 text-xs"
value={value}
onChange={(ev) => handleFieldValue(index, ev.target.value)}
type={isSensitiveKey(key) ? 'password' : 'text'}
className="h-8 text-xs flex-1"
/>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 shrink-0 text-duck-dark/30 hover:text-red-500 cursor-pointer"
onClick={() => handleRemoveField(index)}
>
<X className="h-3 w-3" />
</Button>
</div>
)}
{(hasCredentials || username || password) && (
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-duck-dark/50 mb-1 block">Username</label>
<Input
value={username}
onChange={(ev) => setUsername(ev.target.value)}
placeholder="Optional"
className="h-8 text-xs"
/>
</div>
<div className="flex-1">
<label className="text-xs text-duck-dark/50 mb-1 block">Password</label>
<Input
value={password}
onChange={(ev) => setPassword(ev.target.value)}
placeholder="Optional"
type="password"
className="h-8 text-xs"
/>
</div>
</div>
)}
))}
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handlePing} disabled={pinging || !url} className="text-xs">
{pinging && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
Test Connection
<Input
value={newKey}
onChange={(ev) => setNewKey(ev.target.value)}
placeholder="key"
className="h-8 text-xs w-28 shrink-0"
onKeyDown={(ev) => ev.key === 'Enter' && handleAddField()}
/>
<Input
value={newValue}
onChange={(ev) => setNewValue(ev.target.value)}
placeholder="value"
className="h-8 text-xs flex-1"
onKeyDown={(ev) => ev.key === 'Enter' && handleAddField()}
/>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 shrink-0 text-duck-dark/30 hover:text-duck-teal cursor-pointer"
onClick={handleAddField}
disabled={!newKey.trim()}
>
<Plus className="h-3 w-3" />
</Button>
<Button size="sm" onClick={handleSave} disabled={saving || !url} className="text-xs">
</div>
<div className="flex items-center gap-2 mt-1">
{hasUrl && (
<Button variant="outline" size="sm" onClick={handlePing} disabled={pinging} className="text-xs cursor-pointer">
{pinging && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
Test Connection
</Button>
)}
<Button size="sm" onClick={handleSave} disabled={saving} className="text-xs cursor-pointer">
{saving && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
Save
</Button>
@@ -130,318 +163,187 @@ const ConnectionSection = ({ resource }: ConnectionSectionProps) => {
);
};
type LocalAvailabilitySectionProps = {
resource: Resource;
onRun: (command: string) => void;
type ResourceChatProps = {
detail: ResourceDetail;
isNew?: boolean;
onResponseEnd: () => void;
};
type ResourceAction = 'install' | 'uninstall' | 'verify' | 'update' | 'manage';
const ResourceChat = ({ detail, isNew, onResponseEnd }: ResourceChatProps) => {
const promptFrontmatter = `<frontmatter>\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</frontmatter>`;
const defaultInput = isNew
? 'Help me set up this new resource'
: 'Help me configure this resource';
const LocalAvailabilitySection = ({ resource, onRun }: LocalAvailabilitySectionProps) => {
const { runCommand } = useResources();
const [runningAction, setRunningAction] = useState<ResourceAction | null>(null);
const pi = usePiChat(undefined, undefined, { replaceUrl: false });
const isSudo = (cmd: string) => cmd.trimStart().startsWith('sudo');
const onResponseEndRef = useRef(onResponseEnd);
onResponseEndRef.current = onResponseEnd;
const handleAction = async (action: ResourceAction, command: string) => {
if (isSudo(command)) {
onRun(command);
return;
const wasGenerating = useRef(false);
useEffect(() => {
if (wasGenerating.current && !pi.isGenerating) {
onResponseEndRef.current();
}
setRunningAction(action);
try {
const result = await runCommand(resource.id, action);
if (result.exitCode === 0) {
toast.success('Command completed successfully');
} else {
toast.error(result.output || `Command failed (exit code ${result.exitCode})`, { duration: 8000 });
}
} catch {
toast.error('Failed to run command');
} finally {
setRunningAction(null);
}
};
wasGenerating.current = pi.isGenerating;
}, [pi.isGenerating]);
return (
<div className="flex flex-col gap-2">
{resource.installed ? (
<>
<div>
<span className="text-xs bg-green-100 text-green-700 rounded-full px-2 py-0.5">Installed</span>
</div>
{resource.version && (
<div className="flex items-center gap-2">
{resource.updateCommand && (
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer"
onClick={() => handleAction('update', resource.updateCommand!)}
disabled={!!runningAction}
>
<RefreshCw className={`h-3 w-3 mr-1 ${runningAction === 'update' ? 'animate-spin' : ''}`} />
Update
</Button>
)}
<span className="text-xs text-duck-dark/50 dark:text-foreground/50">{resource.version}</span>
</div>
)}
<div className="flex items-center gap-1.5">
{!resource.version && resource.updateCommand && (
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer"
onClick={() => handleAction('update', resource.updateCommand!)}
disabled={!!runningAction}
>
<RefreshCw className={`h-3 w-3 mr-1 ${runningAction === 'update' ? 'animate-spin' : ''}`} />
Update
</Button>
)}
{resource.verifyCommand && (
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer"
onClick={() => handleAction('verify', resource.verifyCommand!)}
disabled={!!runningAction}
>
{runningAction === 'verify' && <Loader2 className="h-3 w-3 mr-1 animate-spin" />}
Verify
</Button>
)}
{resource.manageCommand && (
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer"
onClick={() => handleAction('manage', resource.manageCommand!)}
disabled={!!runningAction}
>
{runningAction === 'manage' && <Loader2 className="h-3 w-3 mr-1 animate-spin" />}
Manage
</Button>
)}
</div>
{resource.uninstallCommand && (
<div className="flex items-center mt-1.5">
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer text-red-500 hover:text-red-600 border-red-500/30 hover:border-red-500/50 hover:bg-red-500/5"
onClick={() => handleAction('uninstall', resource.uninstallCommand!)}
disabled={!!runningAction}
>
{runningAction === 'uninstall' ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : (
<Trash2 className="h-3 w-3 mr-1" />
)}
Uninstall
</Button>
</div>
)}
</>
) : (
<div className="flex items-center gap-3">
<span className="text-xs bg-duck-dark/5 text-duck-dark/40 rounded-full px-2 py-0.5">Not installed</span>
{resource.installCommand && (
<Button
size="sm"
className="h-6 text-xs bg-duck-teal text-duck-yellow hover:bg-duck-teal/90 cursor-pointer"
onClick={() => handleAction('install', resource.installCommand!)}
disabled={runningAction === 'install'}
>
{runningAction === 'install' ? 'Installing...' : 'Install'}
</Button>
)}
</div>
)}
</div>
);
};
type CatalogCardProps = {
resource: Resource;
onSelect: (id: string) => void;
};
const CatalogCard = ({ resource: r, onSelect }: CatalogCardProps) => {
const category = getResourceCategory(r);
return (
<button
onClick={() => onSelect(r.id)}
className="flex flex-col gap-2 p-4 rounded-lg border border-duck-dark/10 text-left cursor-pointer transition-colors hover:border-duck-teal/30 hover:bg-duck-teal/5"
>
<div className="flex items-center gap-2">
{r.port ? (
<Server className="h-3.5 w-3.5 text-duck-teal shrink-0" />
) : (
<Wrench className="h-3.5 w-3.5 text-duck-dark/40 shrink-0" />
)}
<span className="text-sm font-medium text-duck-dark truncate">{r.name}</span>
</div>
<p className="text-xs text-duck-dark/40 line-clamp-1">{r.subtitle}</p>
<div className="flex items-center gap-1.5">
<span className="text-[10px] bg-duck-dark/5 text-duck-dark/40 rounded-full px-1.5 py-0.5">{r.type}</span>
<span
className={`text-[10px] rounded-full px-1.5 py-0.5 ${category === 'api-based' ? 'bg-duck-teal/10 text-duck-teal' : 'bg-duck-dark/5 text-duck-dark/40'}`}
>
{category === 'api-based' ? 'API' : 'CLI'}
</span>
</div>
</button>
);
};
const ResourceCatalog = () => {
const { resources, isLoading } = useResources();
const [, setSelectedId] = useGlobal<string | null>('RESOURCE_SELECTED', null);
const [, setShowCatalog] = useGlobal<boolean>('RESOURCE_CATALOG', false);
const [search, setSearch] = useState('');
const query = search.toLowerCase();
const filtered =
resources?.filter(
(r: Resource) =>
!r.installed &&
(r.name.toLowerCase().includes(query) ||
r.subtitle.toLowerCase().includes(query) ||
r.description.toLowerCase().includes(query)),
) ?? [];
const apiBased = filtered.filter((r: Resource) => getResourceCategory(r) === 'api-based');
const localCli = filtered.filter((r: Resource) => getResourceCategory(r) === 'local-cli');
const handleSelect = (id: string) => {
setShowCatalog(false);
setSelectedId(id);
};
return (
<div className="h-full overflow-y-auto p-6">
<h2 className="text-lg font-bold text-duck-dark mb-1">Resource Catalog</h2>
<p className="text-sm text-duck-dark/40 mb-4">All available resources. Select one to configure.</p>
<Input
placeholder="Search resources..."
value={search}
onChange={(ev) => setSearch(ev.target.value)}
className="h-8 text-xs mb-4 max-w-xs"
/>
{isLoading && <p className="text-xs text-duck-dark/50">Loading...</p>}
{apiBased.length > 0 && (
<div className="mb-6">
<div className="flex items-center gap-2 mb-3">
<Server className="h-3.5 w-3.5 text-duck-teal" />
<span className="text-xs font-semibold uppercase tracking-wider text-duck-dark/40">API Based</span>
</div>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-3">
{apiBased.map((r: Resource) => (
<CatalogCard key={r.id} resource={r} onSelect={handleSelect} />
))}
</div>
</div>
)}
{localCli.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<Wrench className="h-3.5 w-3.5 text-duck-dark/40" />
<span className="text-xs font-semibold uppercase tracking-wider text-duck-dark/40">Local CLI</span>
</div>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-3">
{localCli.map((r: Resource) => (
<CatalogCard key={r.id} resource={r} onSelect={handleSelect} />
))}
</div>
</div>
)}
</div>
);
};
const ResourceDetail = ({ resource }: { resource: Resource }) => {
const category = getResourceCategory(resource);
const [, setRunCommand] = usePanelChannel<RunCommandState>(RUN_COMMAND_CHANNEL, null);
const [confirmCommand, setConfirmCommand] = useState<string | null>(null);
const handleRun = (command: string) => {
const isSudo = command.trimStart().startsWith('sudo');
if (isSudo) {
setConfirmCommand(command);
} else {
setRunCommand({ command });
}
};
return (
<>
<div className="h-full overflow-y-auto p-6">
<div className="flex items-center gap-2 mb-1">
{resource.port ? (
<Server className="h-4 w-4 text-duck-teal shrink-0" />
) : (
<Wrench className="h-4 w-4 text-duck-dark/40 shrink-0" />
)}
<h2 className="text-lg font-bold text-duck-dark">{resource.name}</h2>
<span className="text-sm text-duck-dark/40">{resource.subtitle}</span>
</div>
<div className="flex items-center gap-2 mb-4">
<span className="text-xs bg-duck-dark/5 text-duck-dark/50 rounded-full px-2 py-0.5">{resource.type}</span>
<span
className={`text-xs rounded-full px-2 py-0.5 ${category === 'api-based' ? 'bg-duck-teal/10 text-duck-teal' : 'bg-duck-dark/5 text-duck-dark/50'}`}
>
{category === 'api-based' ? 'API Based' : 'Local CLI'}
</span>
{resource.port && (
<span className="text-xs bg-duck-teal/10 text-duck-teal rounded-full px-2 py-0.5">:{resource.port}</span>
)}
</div>
<p className="text-sm text-duck-dark/70 mb-6">{resource.description}</p>
{category === 'api-based' && <ConnectionSection key={resource.id} resource={resource} />}
<LocalAvailabilitySection resource={resource} onRun={handleRun} />
</div>
<AlertDialog open={!!confirmCommand} onOpenChange={(open) => !open && setConfirmCommand(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Run with elevated privileges</AlertDialogTitle>
</AlertDialogHeader>
<p className="text-sm text-duck-dark/70 dark:text-foreground/70">
For this operation the script must be run with elevated privileges (sudo) on the host machine.
<br />
Not to worry, though, we wrote it and battle tested it ourselves.
</p>
<code className="text-sm font-mono bg-[#1a1a2e] text-[#e0e0e0] rounded-lg px-3 py-2 break-all">{confirmCommand}</code>
<AlertDialogFooter>
<AlertDialogCancel className="cursor-pointer">Cancel</AlertDialogCancel>
<AlertDialogAction
className="cursor-pointer bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold"
onClick={() => {
if (confirmCommand) setRunCommand({ command: confirmCommand });
setConfirmCommand(null);
}}
>
Run
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
<EmbeddableChat
chat={pi}
defaultInput={defaultInput}
promptPrefix={promptFrontmatter}
className="h-full"
/>
);
};
export const Resources = () => {
const { resources } = useResources();
const [selectedId] = useGlobal<string | null>('RESOURCE_SELECTED', null);
const [showCatalog] = useGlobal<boolean>('RESOURCE_CATALOG', false);
const client = useClient();
const qc = useQueryClient();
const [selectedName] = useGlobal<string | null>('RESOURCE_SELECTED', null);
const [editing, setEditing] = useState(false);
const [isNew, setIsNew] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState(false);
const [showDetail, setShowDetail] = useState(false);
const resource = selectedId ? resources?.find((r: Resource) => r.id === selectedId) : null;
const { data: detail, refetch } = useQuery<ResourceDetail>({
queryKey: ['RESOURCES', selectedName],
queryFn: () => client.get<ResourceDetail>(`/server-settings/resources/${selectedName}`),
enabled: !!selectedName,
});
if (showCatalog || !resource) return <ResourceCatalog />;
return <ResourceDetail resource={resource} />;
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 (
<div className="h-full flex items-center justify-center">
<p className="text-sm text-duck-dark/40">Select a resource to view its configuration</p>
</div>
);
}
return (
<>
<div className="flex flex-col h-full">
{/* Detail panel */}
<div className={`flex-1 overflow-hidden flex flex-col min-h-0 ${editing ? 'hidden md:flex' : ''}`}>
<div className="shrink-0 px-4 py-2 border-b border-duck-dark/10 bg-background/60 flex items-center gap-2">
<button
onClick={() => setShowDetail(false)}
className="md:hidden p-1 -ml-1 rounded hover:bg-duck-dark/10 cursor-pointer"
>
<ArrowLeft className="h-4 w-4 text-duck-dark/60" />
</button>
<span className="text-sm font-medium text-duck-dark/70 flex-1">{detail.name}</span>
<span
className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full font-medium ${
detail.scope === 'global' ? 'bg-duck-teal/20 text-duck-teal' : 'bg-duck-dark/10 text-duck-dark/60'
}`}
>
{detail.scope}
</span>
<button
onClick={() => setEditing((e) => !e)}
className={`p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors ${editing ? 'bg-duck-teal/10' : ''}`}
>
<Pencil className={`h-3.5 w-3.5 ${editing ? 'text-duck-teal' : 'text-duck-dark/50'}`} />
</button>
{canDelete && (
<button
onClick={() => setDeleteConfirm(true)}
className="p-1 rounded hover:bg-red-50 cursor-pointer transition-colors"
>
<Trash2 className="h-3.5 w-3.5 text-duck-dark/50 hover:text-red-500" />
</button>
)}
</div>
<div className="overflow-y-auto flex-1 p-6">
{detail.rawFrontmatter && <FrontmatterBlock yaml={detail.rawFrontmatter} />}
{detail.body && (
<article className="skill-md mb-6">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{detail.body}
</ReactMarkdown>
</article>
)}
<ConfigEditor
key={selectedName}
resourceName={selectedName}
config={detail.config}
onSaved={() => refetch()}
/>
</div>
</div>
{/* Chat panel */}
{editing && (
<div className="flex-1 overflow-hidden flex flex-col min-h-0">
<div className="shrink-0 px-4 py-1.5 border-b border-duck-dark/10 bg-background/60 flex items-center gap-2">
<button
onClick={() => setEditing(false)}
className="md:hidden p-1 -ml-1 rounded hover:bg-duck-dark/10 cursor-pointer"
>
<ArrowLeft className="h-4 w-4 text-duck-dark/60" />
</button>
<span className="text-xs font-medium text-duck-dark/50 flex-1">{detail.name} Chat</span>
<button
onClick={() => setEditing(false)}
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
>
<X className="h-3.5 w-3.5 text-duck-dark/50" />
</button>
</div>
<ResourceChat
key={detail.filePath}
detail={detail}
isNew={isNew}
onResponseEnd={() => {
refetch();
qc.invalidateQueries({ queryKey: ['RESOURCES'] });
}}
/>
</div>
)}
</div>
<Dialog open={deleteConfirm} onOpenChange={setDeleteConfirm}>
<DialogContent className="sm:max-w-md z-[700]">
<DialogHeader>
<DialogTitle>Delete Resource</DialogTitle>
<DialogDescription>
Are you sure you want to delete &quot;{detail.name}&quot;? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-2 mt-2">
<Button variant="outline" onClick={() => setDeleteConfirm(false)} className="cursor-pointer">
Cancel
</Button>
<Button variant="destructive" onClick={handleDelete} className="cursor-pointer">
Delete
</Button>
</div>
</DialogContent>
</Dialog>
</>
);
};