|
|
@@ -0,0 +1,200 @@
|
|
|
+<script lang="ts">
|
|
|
+ import { Send, SquarePen } from '@lucide/svelte';
|
|
|
+ import type { Snippet } from 'svelte';
|
|
|
+
|
|
|
+ import { goto } from '$app/navigation';
|
|
|
+ import { resolve } from '$app/paths';
|
|
|
+
|
|
|
+ import * as agentsApi from '$lib/api/agents';
|
|
|
+ import { Card } from '$lib/components/common/card';
|
|
|
+ import { ConfirmDialog } from '$lib/components/common/confirmation';
|
|
|
+ import { Icon } from '$lib/components/common/icon';
|
|
|
+ import { MoreMenu } from '$lib/components/common/more-menu';
|
|
|
+ import { PropertyTable, toDeploymentProps } from '$lib/components/common/property-table';
|
|
|
+ import { toInfoProps } from '$lib/components/common/property-table/types.js';
|
|
|
+ import { StatusBadge } from '$lib/components/common/status-badge';
|
|
|
+ import Button from '$lib/components/controls/button/button.svelte';
|
|
|
+ import * as DropdownMenu from '$lib/components/controls/dropdown-menu';
|
|
|
+ import { ManagementTabs } from '$lib/components/management/index.js';
|
|
|
+ import { agentsStore } from '$lib/stores/agents.svelte.js';
|
|
|
+ import { getBreadcrumbContext } from '$lib/stores/breadcrumb.svelte.js';
|
|
|
+ import { toAgentCR } from '$lib/types/custom-resources';
|
|
|
+ import type { Changes, CoreResources } from '$lib/types/workspace';
|
|
|
+ import * as yaml from '$lib/yaml';
|
|
|
+
|
|
|
+ let { data } = $props();
|
|
|
+
|
|
|
+ $effect.root(() => {
|
|
|
+ agentsStore.upsert(data.agent);
|
|
|
+ agentsStore.refreshStatus(data.agent.id);
|
|
|
+ });
|
|
|
+
|
|
|
+ const agent = $derived(agentsStore.get(data.agent.id) ?? data.agent);
|
|
|
+ const status = $derived(agentsStore.getStatus(agent.id));
|
|
|
+
|
|
|
+ const infoProps = $derived(toInfoProps(agent));
|
|
|
+ const configProps = $derived.by(() => {
|
|
|
+ const props: Record<string, string | number | string[] | Snippet> = {};
|
|
|
+ props['Model'] = modelRef;
|
|
|
+ props['Knowledge Bases'] = kbRefs;
|
|
|
+ props['Tools'] = toolRefs;
|
|
|
+ return props;
|
|
|
+ });
|
|
|
+ const deploymentProps = $derived.by(() => {
|
|
|
+ return toDeploymentProps(agent);
|
|
|
+ });
|
|
|
+ const cr = $derived.by(() => yaml.stringify(toAgentCR(agent)));
|
|
|
+ let deleteOpen = $state(false);
|
|
|
+
|
|
|
+ $effect.root(() => {
|
|
|
+ agentsStore.upsert(data.agent);
|
|
|
+ });
|
|
|
+
|
|
|
+ const breadcrumb = getBreadcrumbContext();
|
|
|
+ $effect(() => {
|
|
|
+ breadcrumb.set({
|
|
|
+ module: 'Agents',
|
|
|
+ pages: [{ name: agent.name, href: resolve('/workloads/agents/[id]', { id: agent.id }) }],
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ const onTest = () => {
|
|
|
+ goto(resolve(`/utilities/testing/agent?agent=${agent.id}`));
|
|
|
+ };
|
|
|
+
|
|
|
+ const onDeploy = () => {
|
|
|
+ agentsStore.update(agent);
|
|
|
+ };
|
|
|
+
|
|
|
+ const onDelete = async () => {
|
|
|
+ await agentsStore.remove(agent.id);
|
|
|
+ goto(resolve('/workloads/agents'));
|
|
|
+ };
|
|
|
+
|
|
|
+ const fetchResources = async (): Promise<CoreResources> => {
|
|
|
+ return agentsApi.getResources(data.workspaceId!, agent.id);
|
|
|
+ };
|
|
|
+
|
|
|
+ const fetchChanges = async (): Promise<Changes> => {
|
|
|
+ return agentsApi.getChanges(data.workspaceId!, agent.id);
|
|
|
+ };
|
|
|
+</script>
|
|
|
+
|
|
|
+<svelte:head>
|
|
|
+ <title>{agent.name} - LocoStack</title>
|
|
|
+</svelte:head>
|
|
|
+
|
|
|
+{#snippet refLink(href: string, icon: string | undefined, label: string)}
|
|
|
+ <Button {href} size="sm" variant="outline">
|
|
|
+ <Icon {icon} size={14} />
|
|
|
+ <div>{label}</div>
|
|
|
+ </Button>
|
|
|
+{/snippet}
|
|
|
+
|
|
|
+{#snippet modelRef()}
|
|
|
+ {@render refLink(
|
|
|
+ resolve('/workloads/models/[id]', { id: agent.model.id }),
|
|
|
+ agent.model.icon,
|
|
|
+ agent.model.name || agent.model.id,
|
|
|
+ )}
|
|
|
+{/snippet}
|
|
|
+
|
|
|
+{#snippet kbRefs()}
|
|
|
+ <div class="flex flex-wrap gap-2">
|
|
|
+ {#each agent.kbs as kb (kb.id)}
|
|
|
+ {@render refLink(resolve('/workloads/kbs/[id]', { id: kb.id }), undefined, kb.name || kb.id)}
|
|
|
+ {:else}
|
|
|
+ <div class="text-white/75 text-sm">None</div>
|
|
|
+ {/each}
|
|
|
+ </div>
|
|
|
+{/snippet}
|
|
|
+
|
|
|
+{#snippet toolRefs()}
|
|
|
+ <div class="flex flex-wrap gap-2">
|
|
|
+ {#each agent.tools as tool (tool.id)}
|
|
|
+ {@render refLink(
|
|
|
+ resolve('/workloads/tools/[id]', { id: tool.id }),
|
|
|
+ tool.icon,
|
|
|
+ tool.name || tool.id,
|
|
|
+ )}
|
|
|
+ {:else}
|
|
|
+ <div class="text-white/75 text-sm">None</div>
|
|
|
+ {/each}
|
|
|
+ </div>
|
|
|
+{/snippet}
|
|
|
+
|
|
|
+<div class="mx-auto max-w-2xl p-6">
|
|
|
+ <!-- Page header -->
|
|
|
+ <div class="mb-4 flex items-center justify-between gap-4">
|
|
|
+ <div class="flex items-center gap-4">
|
|
|
+ <Icon icon={agent.icon} />
|
|
|
+ <div class="flex flex-1 flex-col overflow-hidden">
|
|
|
+ <div class="truncate text-lg font-medium text-white/75">{agent.name}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <StatusBadge status={agent.status || 'not-deployed'} />
|
|
|
+ <Button
|
|
|
+ href={resolve('/workloads/agents/[id]/edit', { id: agent.id })}
|
|
|
+ size="sm"
|
|
|
+ variant="outline"
|
|
|
+ >
|
|
|
+ <SquarePen size={14} strokeWidth={2} />
|
|
|
+ Edit
|
|
|
+ </Button>
|
|
|
+ <MoreMenu ondelete={() => (deleteOpen = true)}>
|
|
|
+ {#snippet menu()}
|
|
|
+ {#if agent.status}
|
|
|
+ <DropdownMenu.Item onclick={() => onTest()}>
|
|
|
+ <Send size={14} strokeWidth={2} />
|
|
|
+ Test
|
|
|
+ </DropdownMenu.Item>
|
|
|
+ {:else}
|
|
|
+ <DropdownMenu.Item onclick={() => onDeploy()}>
|
|
|
+ <Send size={14} strokeWidth={2} />
|
|
|
+ Deploy
|
|
|
+ </DropdownMenu.Item>
|
|
|
+ {/if}
|
|
|
+ {/snippet}
|
|
|
+ </MoreMenu>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Card class="mb-4" title="Basic Information">
|
|
|
+ <PropertyTable properties={infoProps} />
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Card class="mb-4" title="Configuration">
|
|
|
+ <PropertyTable properties={configProps} />
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Card class="mb-4" title="System Prompt">
|
|
|
+ <div class="text-white/75">{agent.systemPrompt}</div>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Card class="mb-8" title="Deployment">
|
|
|
+ <PropertyTable properties={deploymentProps} />
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <ManagementTabs
|
|
|
+ conditions={status?.conditions}
|
|
|
+ {cr}
|
|
|
+ onloadresources={fetchResources}
|
|
|
+ onloadchanges={fetchChanges}
|
|
|
+ />
|
|
|
+</div>
|
|
|
+
|
|
|
+<!-- Delete confirmation dialog -->
|
|
|
+<ConfirmDialog
|
|
|
+ confirmLabel="Delete"
|
|
|
+ confirmingLabel="Deleting..."
|
|
|
+ title="Delete agent?"
|
|
|
+ variant="destructive"
|
|
|
+ onconfirm={() => onDelete()}
|
|
|
+ bind:open={deleteOpen}
|
|
|
+>
|
|
|
+ {#snippet description()}
|
|
|
+ <strong class="text-white/90">"{agent.name}"</strong> will be permanently deleted.
|
|
|
+ {/snippet}
|
|
|
+</ConfirmDialog>
|