Workspaces layout, automation page
This commit is contained in:
@@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } f
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useVisibleClaudeModels } from '@/state/useModels';
|
||||
import { Card } from '@/components/Card';
|
||||
import type { ChatMessage } from 'widgets/Chat';
|
||||
import type { ChatMessage } from 'apps/Chat';
|
||||
import { useClaude } from '@/Screens/Dashboard/Chat/useClaude';
|
||||
import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat';
|
||||
type CapabilitySummary = {
|
||||
@@ -20,13 +20,25 @@ type CapabilitySummary = {
|
||||
scope: 'global' | 'user';
|
||||
};
|
||||
|
||||
type CapabilityDetail = CapabilitySummary & {
|
||||
export type CapabilityDetail = CapabilitySummary & {
|
||||
body: string;
|
||||
rawFrontmatter: string;
|
||||
filePath: string;
|
||||
chatSessionId: string | null;
|
||||
};
|
||||
|
||||
type CapabilityListProps = {
|
||||
kind: string;
|
||||
endpoint: string;
|
||||
queryKey: string;
|
||||
selected: string | null;
|
||||
onSelect: (dirName: string) => void;
|
||||
onCreate?: (dirName: string) => void;
|
||||
search?: string;
|
||||
showCreate?: boolean;
|
||||
onShowCreateChange?: (value: boolean) => void;
|
||||
};
|
||||
|
||||
type CapabilityPageProps = {
|
||||
kind: string;
|
||||
endpoint: string;
|
||||
@@ -41,10 +53,11 @@ type CapabilityChatProps = {
|
||||
resourceDir: string;
|
||||
chatSessionId: string | null;
|
||||
isNew?: boolean;
|
||||
description?: string;
|
||||
onResponseEnd?: () => void;
|
||||
};
|
||||
|
||||
const CapabilityChat = ({
|
||||
export const CapabilityChat = ({
|
||||
kind,
|
||||
endpoint,
|
||||
dirName,
|
||||
@@ -52,6 +65,7 @@ const CapabilityChat = ({
|
||||
resourceDir,
|
||||
chatSessionId,
|
||||
isNew,
|
||||
description,
|
||||
onResponseEnd,
|
||||
}: CapabilityChatProps) => {
|
||||
const client = useClient();
|
||||
@@ -63,7 +77,7 @@ const CapabilityChat = ({
|
||||
const defaultInput = chatSessionId
|
||||
? undefined
|
||||
: isNew
|
||||
? `Help me create the content for this new ${kind} file`
|
||||
? description ?? `Help me create the content for this new ${kind} file`
|
||||
: `Help me understand and improve this ${kind} file`;
|
||||
|
||||
const storage = useMemo(
|
||||
@@ -110,7 +124,7 @@ const CapabilityChat = ({
|
||||
);
|
||||
};
|
||||
|
||||
const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
|
||||
export const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -129,18 +143,150 @@ const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const CapabilityList = ({ kind, endpoint, queryKey, selected, onSelect, onCreate, search: externalSearch, showCreate, onShowCreateChange }: CapabilityListProps) => {
|
||||
const client = useClient();
|
||||
const qc = useQueryClient();
|
||||
const [internalCreating, setInternalCreating] = useState(false);
|
||||
const creating = showCreate ?? internalCreating;
|
||||
const setCreating = onShowCreateChange ?? setInternalCreating;
|
||||
const [newName, setNewName] = useState('');
|
||||
const [internalSearch, setInternalSearch] = useState('');
|
||||
const search = externalSearch ?? internalSearch;
|
||||
const newNameRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const { data: items = [] } = useQuery<CapabilitySummary[]>({
|
||||
queryKey: [queryKey],
|
||||
queryFn: () => client.get<CapabilitySummary[]>(endpoint),
|
||||
});
|
||||
|
||||
const handleCreate = async () => {
|
||||
const name = newName.trim();
|
||||
if (!name) return;
|
||||
try {
|
||||
const res = await client.post<{ name: string; dirName: string }>(endpoint, { name });
|
||||
await qc.invalidateQueries({ queryKey: [queryKey] });
|
||||
setCreating(false);
|
||||
setNewName('');
|
||||
(onCreate ?? onSelect)(res.dirName);
|
||||
} catch {
|
||||
toast.error(`Failed to create ${kind}`);
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = items.filter(
|
||||
(item) =>
|
||||
!search ||
|
||||
item.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.description?.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="shrink-0 px-4 py-2 border-b border-duck-dark/10 bg-white/60 flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-duck-dark/70">{kind}s</span>
|
||||
{!creating && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setCreating(true);
|
||||
setTimeout(() => newNameRef.current?.focus(), 0);
|
||||
}}
|
||||
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4 text-duck-dark/50" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{creating && (
|
||||
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10 bg-duck-teal/5 flex items-center gap-1.5">
|
||||
<input
|
||||
ref={newNameRef}
|
||||
value={newName}
|
||||
onChange={(ev) => setNewName(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
handleCreate();
|
||||
}
|
||||
if (ev.key === 'Escape') {
|
||||
setCreating(false);
|
||||
setNewName('');
|
||||
}
|
||||
}}
|
||||
placeholder={`${kind} name...`}
|
||||
className="flex-1 min-w-0 rounded border border-duck-dark/20 bg-white px-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
disabled={!newName.trim()}
|
||||
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors disabled:opacity-30"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5 text-duck-teal" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCreating(false);
|
||||
setNewName('');
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
{externalSearch === undefined && (
|
||||
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-duck-dark/30" />
|
||||
<input
|
||||
value={internalSearch}
|
||||
onChange={(ev) => setInternalSearch(ev.target.value)}
|
||||
placeholder={`Search ${kind.toLowerCase()}s...`}
|
||||
className="w-full rounded border border-duck-dark/15 bg-white/80 pl-7 pr-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{filtered.map((item) => (
|
||||
<button
|
||||
key={item.dirName}
|
||||
onClick={() => onSelect(item.dirName)}
|
||||
className={`w-full text-left px-4 py-3 border-b border-duck-dark/5 cursor-pointer transition-colors ${
|
||||
selected === item.dirName ? 'bg-duck-teal/10' : 'hover:bg-duck-dark/5'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-duck-dark truncate">{item.name}</span>
|
||||
<span
|
||||
className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full font-medium ${
|
||||
item.scope === 'user' ? 'bg-duck-teal/20 text-duck-teal' : 'bg-duck-dark/10 text-duck-dark/60'
|
||||
}`}
|
||||
>
|
||||
{item.scope}
|
||||
</span>
|
||||
</div>
|
||||
{item.description && <p className="text-xs text-duck-dark/50 mt-1 line-clamp-2">{item.description}</p>}
|
||||
</button>
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<p className="text-sm text-duck-dark/40 px-4 py-6 text-center">No {kind.toLowerCase()}s found</p>
|
||||
)}
|
||||
{items.length > 0 && filtered.length === 0 && (
|
||||
<p className="text-sm text-duck-dark/40 px-4 py-6 text-center">No matches</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps) => {
|
||||
const client = useClient();
|
||||
const qc = useQueryClient();
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
const newNameRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const { data: items = [] } = useQuery<CapabilitySummary[]>({
|
||||
queryKey: [queryKey],
|
||||
@@ -166,21 +312,11 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
const name = newName.trim();
|
||||
if (!name) return;
|
||||
try {
|
||||
const res = await client.post<{ name: string; dirName: string }>(endpoint, { name });
|
||||
await qc.invalidateQueries({ queryKey: [queryKey] });
|
||||
setCreating(false);
|
||||
setNewName('');
|
||||
setSelected(res.dirName);
|
||||
setShowDetail(true);
|
||||
setIsNew(true);
|
||||
setEditing(true);
|
||||
} catch {
|
||||
toast.error(`Failed to create ${kind}`);
|
||||
}
|
||||
const handleCreate = (dirName: string) => {
|
||||
setSelected(dirName);
|
||||
setShowDetail(true);
|
||||
setIsNew(true);
|
||||
setEditing(true);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -197,13 +333,6 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = items.filter(
|
||||
(item) =>
|
||||
!search ||
|
||||
item.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.description?.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full p-2 md:p-4 gap-2 md:gap-4">
|
||||
@@ -211,97 +340,14 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps
|
||||
<Card
|
||||
className={`md:w-72 shrink-0 overflow-hidden flex flex-col ${showDetail ? 'hidden md:flex' : 'flex-1 md:flex-none'}`}
|
||||
>
|
||||
<div className="shrink-0 px-4 py-2 border-b border-duck-dark/10 bg-white/60 flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-duck-dark/70">{kind}s</span>
|
||||
{!creating && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setCreating(true);
|
||||
setTimeout(() => newNameRef.current?.focus(), 0);
|
||||
}}
|
||||
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4 text-duck-dark/50" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{creating && (
|
||||
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10 bg-duck-teal/5 flex items-center gap-1.5">
|
||||
<input
|
||||
ref={newNameRef}
|
||||
value={newName}
|
||||
onChange={(ev) => setNewName(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
handleCreate();
|
||||
}
|
||||
if (ev.key === 'Escape') {
|
||||
setCreating(false);
|
||||
setNewName('');
|
||||
}
|
||||
}}
|
||||
placeholder={`${kind} name...`}
|
||||
className="flex-1 min-w-0 rounded border border-duck-dark/20 bg-white px-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
disabled={!newName.trim()}
|
||||
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors disabled:opacity-30"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5 text-duck-teal" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCreating(false);
|
||||
setNewName('');
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-duck-dark/30" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(ev) => setSearch(ev.target.value)}
|
||||
placeholder={`Search ${kind.toLowerCase()}s...`}
|
||||
className="w-full rounded border border-duck-dark/15 bg-white/80 pl-7 pr-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{filtered.map((item) => (
|
||||
<button
|
||||
key={item.dirName}
|
||||
onClick={() => selectItem(item.dirName)}
|
||||
className={`w-full text-left px-4 py-3 border-b border-duck-dark/5 cursor-pointer transition-colors ${
|
||||
selected === item.dirName ? 'bg-duck-teal/10' : 'hover:bg-duck-dark/5'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-duck-dark truncate">{item.name}</span>
|
||||
<span
|
||||
className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full font-medium ${
|
||||
item.scope === 'user' ? 'bg-duck-teal/20 text-duck-teal' : 'bg-duck-dark/10 text-duck-dark/60'
|
||||
}`}
|
||||
>
|
||||
{item.scope}
|
||||
</span>
|
||||
</div>
|
||||
{item.description && <p className="text-xs text-duck-dark/50 mt-1 line-clamp-2">{item.description}</p>}
|
||||
</button>
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<p className="text-sm text-duck-dark/40 px-4 py-6 text-center">No {kind.toLowerCase()}s found</p>
|
||||
)}
|
||||
{items.length > 0 && filtered.length === 0 && (
|
||||
<p className="text-sm text-duck-dark/40 px-4 py-6 text-center">No matches</p>
|
||||
)}
|
||||
</div>
|
||||
<CapabilityList
|
||||
kind={kind}
|
||||
endpoint={endpoint}
|
||||
queryKey={queryKey}
|
||||
selected={selected}
|
||||
onSelect={selectItem}
|
||||
onCreate={handleCreate}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Right panel — detail + chat */}
|
||||
|
||||
Reference in New Issue
Block a user