@vertesia/tools-admin-ui 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +120 -0
  3. package/lib/AdminApp.d.ts +13 -0
  4. package/lib/AdminApp.d.ts.map +1 -0
  5. package/lib/AdminContext.d.ts +10 -0
  6. package/lib/AdminContext.d.ts.map +1 -0
  7. package/lib/components/CollectionCard.d.ts +5 -0
  8. package/lib/components/CollectionCard.d.ts.map +1 -0
  9. package/lib/components/DetailPage.d.ts +13 -0
  10. package/lib/components/DetailPage.d.ts.map +1 -0
  11. package/lib/components/EndpointPanel.d.ts +5 -0
  12. package/lib/components/EndpointPanel.d.ts.map +1 -0
  13. package/lib/components/HeroSection.d.ts +9 -0
  14. package/lib/components/HeroSection.d.ts.map +1 -0
  15. package/lib/components/ResourceCard.d.ts +5 -0
  16. package/lib/components/ResourceCard.d.ts.map +1 -0
  17. package/lib/components/ResourceSection.d.ts +10 -0
  18. package/lib/components/ResourceSection.d.ts.map +1 -0
  19. package/lib/components/SearchBar.d.ts +10 -0
  20. package/lib/components/SearchBar.d.ts.map +1 -0
  21. package/lib/components/SummaryBadge.d.ts +5 -0
  22. package/lib/components/SummaryBadge.d.ts.map +1 -0
  23. package/lib/components/index.d.ts +9 -0
  24. package/lib/components/index.d.ts.map +1 -0
  25. package/lib/hooks.d.ts +26 -0
  26. package/lib/hooks.d.ts.map +1 -0
  27. package/lib/index.d.ts +7 -0
  28. package/lib/index.d.ts.map +1 -0
  29. package/lib/pages/HomePage.d.ts +2 -0
  30. package/lib/pages/HomePage.d.ts.map +1 -0
  31. package/lib/pages/InteractionCollection.d.ts +2 -0
  32. package/lib/pages/InteractionCollection.d.ts.map +1 -0
  33. package/lib/pages/InteractionDetail.d.ts +2 -0
  34. package/lib/pages/InteractionDetail.d.ts.map +1 -0
  35. package/lib/pages/SkillCollection.d.ts +2 -0
  36. package/lib/pages/SkillCollection.d.ts.map +1 -0
  37. package/lib/pages/SkillDetail.d.ts +2 -0
  38. package/lib/pages/SkillDetail.d.ts.map +1 -0
  39. package/lib/pages/TemplateCollection.d.ts +2 -0
  40. package/lib/pages/TemplateCollection.d.ts.map +1 -0
  41. package/lib/pages/TemplateDetail.d.ts +2 -0
  42. package/lib/pages/TemplateDetail.d.ts.map +1 -0
  43. package/lib/pages/ToolCollection.d.ts +2 -0
  44. package/lib/pages/ToolCollection.d.ts.map +1 -0
  45. package/lib/pages/TypeCollection.d.ts +2 -0
  46. package/lib/pages/TypeCollection.d.ts.map +1 -0
  47. package/lib/pages/TypeDetail.d.ts +2 -0
  48. package/lib/pages/TypeDetail.d.ts.map +1 -0
  49. package/lib/tools-admin-ui.js +935 -0
  50. package/lib/tools-admin-ui.js.map +1 -0
  51. package/lib/types.d.ts +89 -0
  52. package/lib/types.d.ts.map +1 -0
  53. package/package.json +50 -0
  54. package/src/AdminApp.tsx +87 -0
  55. package/src/AdminContext.ts +17 -0
  56. package/src/admin.css +650 -0
  57. package/src/components/CollectionCard.tsx +23 -0
  58. package/src/components/DetailPage.tsx +40 -0
  59. package/src/components/EndpointPanel.tsx +24 -0
  60. package/src/components/HeroSection.tsx +88 -0
  61. package/src/components/ResourceCard.tsx +25 -0
  62. package/src/components/ResourceSection.tsx +31 -0
  63. package/src/components/SearchBar.tsx +35 -0
  64. package/src/components/SummaryBadge.tsx +9 -0
  65. package/src/components/index.ts +8 -0
  66. package/src/dev/env.ts +14 -0
  67. package/src/dev/main.tsx +37 -0
  68. package/src/hooks.ts +36 -0
  69. package/src/index.ts +6 -0
  70. package/src/pages/HomePage.tsx +99 -0
  71. package/src/pages/InteractionCollection.tsx +59 -0
  72. package/src/pages/InteractionDetail.tsx +92 -0
  73. package/src/pages/SkillCollection.tsx +111 -0
  74. package/src/pages/SkillDetail.tsx +112 -0
  75. package/src/pages/TemplateCollection.tsx +54 -0
  76. package/src/pages/TemplateDetail.tsx +68 -0
  77. package/src/pages/ToolCollection.tsx +55 -0
  78. package/src/pages/TypeCollection.tsx +60 -0
  79. package/src/pages/TypeDetail.tsx +63 -0
  80. package/src/types.ts +304 -0
  81. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,24 @@
1
+ import { useState } from 'react';
2
+
3
+ export function EndpointPanel({ label, path }: { label: string; path: string }) {
4
+ const [copied, setCopied] = useState(false);
5
+
6
+ function handleCopy() {
7
+ const url = window.location.origin + path;
8
+ navigator.clipboard.writeText(url);
9
+ setCopied(true);
10
+ setTimeout(() => setCopied(false), 1500);
11
+ }
12
+
13
+ return (
14
+ <div className="vta-endpoint">
15
+ <div className="vta-endpoint-label">{label}</div>
16
+ <div className="vta-endpoint-box">
17
+ <code className="vta-endpoint-code">{path}</code>
18
+ <button onClick={handleCopy} className="vta-copy-btn" title="Copy full URL">
19
+ {copied ? '\u2713' : '\u29C9'}
20
+ </button>
21
+ </div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,88 @@
1
+ import type { ResourceItem, ResourceType } from '../types.js';
2
+ import { EndpointPanel } from './EndpointPanel.js';
3
+ import { SummaryBadge } from './SummaryBadge.js';
4
+
5
+ interface HeroSectionProps {
6
+ title: string;
7
+ version: string;
8
+ resources: ResourceItem[];
9
+ }
10
+
11
+ function getInitials(title: string): string {
12
+ return title.split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
13
+ }
14
+
15
+ function countByType(resources: ResourceItem[]): Record<string, number> {
16
+ const counts: Record<string, number> = {};
17
+ for (const r of resources) {
18
+ counts[r.type] = (counts[r.type] || 0) + 1;
19
+ }
20
+ return counts;
21
+ }
22
+
23
+ const badgeLabels: { type: ResourceType; label: string }[] = [
24
+ { type: 'tool', label: 'tool' },
25
+ { type: 'skill', label: 'skill' },
26
+ { type: 'interaction', label: 'interaction' },
27
+ { type: 'type', label: 'content type' },
28
+ { type: 'template', label: 'template' },
29
+ { type: 'mcp', label: 'MCP provider' },
30
+ ];
31
+
32
+ export function HeroSection({ title, version, resources }: HeroSectionProps) {
33
+ const counts = countByType(resources);
34
+
35
+ return (
36
+ <header className="vta-hero">
37
+ <div className="vta-hero-main">
38
+ <div className="vta-hero-identity">
39
+ <div className="vta-hero-logo">
40
+ {getInitials(title)}
41
+ </div>
42
+ <div>
43
+ <p className="vta-hero-eyebrow">Tools Server</p>
44
+ <h1 className="vta-hero-title">{title}</h1>
45
+ </div>
46
+ </div>
47
+
48
+ <p className="vta-hero-tagline">
49
+ Discover the tools, skills, interactions, and content types exposed by this server.
50
+ </p>
51
+
52
+ <div className="vta-hero-summary">
53
+ {badgeLabels.map(({ type, label }) => (
54
+ <SummaryBadge key={type} count={counts[type] || 0} label={label} />
55
+ ))}
56
+ </div>
57
+
58
+ <div className="vta-hero-links">
59
+ <a href="/app/" target="_blank" className="vta-link-primary">
60
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
61
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
62
+ <line x1="9" y1="3" x2="9" y2="21" />
63
+ </svg>
64
+ UI Plugin Dev
65
+ </a>
66
+ <a href="/lib/plugin.js" className="vta-link-secondary">
67
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
68
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
69
+ <polyline points="7 10 12 15 17 10" />
70
+ <line x1="12" y1="15" x2="12" y2="3" />
71
+ </svg>
72
+ Plugin Bundle
73
+ </a>
74
+ </div>
75
+ </div>
76
+
77
+ <aside className="vta-hero-panel">
78
+ <EndpointPanel label="Base endpoint" path="/api" />
79
+ <EndpointPanel label="Package endpoint" path="/api/package" />
80
+ <p className="vta-hero-hint">
81
+ Use <strong>POST /api/tools/&lt;collection&gt;</strong> or{' '}
82
+ <strong>POST /api/skills/&lt;collection&gt;</strong> to call these from your apps or agents.
83
+ </p>
84
+ <p className="vta-hero-version">v{version}</p>
85
+ </aside>
86
+ </header>
87
+ );
88
+ }
@@ -0,0 +1,25 @@
1
+ import type { ResourceItem } from '../types.js';
2
+
3
+ export function ResourceCard({ resource }: { resource: ResourceItem }) {
4
+ return (
5
+ <div className="vta-card">
6
+ <span className={`vta-card-type vta-card-type--${resource.type}`}>
7
+ {resource.type}
8
+ </span>
9
+ <div className="vta-card-title">{resource.title}</div>
10
+ <div className="vta-card-desc">
11
+ {resource.description || 'No description'}
12
+ </div>
13
+ {resource.tags && resource.tags.length > 0 && (
14
+ <div className="vta-card-tags">
15
+ {resource.tags.map(tag => (
16
+ <span key={tag} className="vta-tag">{tag}</span>
17
+ ))}
18
+ </div>
19
+ )}
20
+ {resource.url && (
21
+ <div className="vta-card-url">{resource.url}</div>
22
+ )}
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,31 @@
1
+ import type { ResourceItem } from '../types.js';
2
+ import { ResourceCard } from './ResourceCard.js';
3
+
4
+ interface ResourceSectionProps {
5
+ title: string;
6
+ subtitle: string;
7
+ resources: ResourceItem[];
8
+ showDivider?: boolean;
9
+ }
10
+
11
+ export function ResourceSection({ title, subtitle, resources, showDivider }: ResourceSectionProps) {
12
+ if (resources.length === 0) return null;
13
+
14
+ return (
15
+ <section>
16
+ {showDivider && <hr className="vta-divider" />}
17
+ <div>
18
+ <h2 className="vta-section-title">
19
+ {title}
20
+ <span className="vta-section-count">({resources.length})</span>
21
+ </h2>
22
+ <p className="vta-section-subtitle">{subtitle}</p>
23
+ </div>
24
+ <div className="vta-card-grid">
25
+ {resources.map(r => (
26
+ <ResourceCard key={`${r.type}:${r.name}`} resource={r} />
27
+ ))}
28
+ </div>
29
+ </section>
30
+ );
31
+ }
@@ -0,0 +1,35 @@
1
+ interface SearchBarProps {
2
+ value: string;
3
+ onChange: (value: string) => void;
4
+ placeholder?: string;
5
+ resultCount?: number;
6
+ totalCount?: number;
7
+ }
8
+
9
+ export function SearchBar({ value, onChange, placeholder, resultCount, totalCount }: SearchBarProps) {
10
+ const hasQuery = value.trim().length > 0;
11
+ const noResults = hasQuery && resultCount === 0;
12
+
13
+ return (
14
+ <div className="vta-search">
15
+ <input
16
+ type="search"
17
+ value={value}
18
+ onChange={e => onChange(e.target.value)}
19
+ placeholder={placeholder || 'Search collections...'}
20
+ className="vta-search-input"
21
+ autoComplete="off"
22
+ />
23
+ {hasQuery && !noResults && (
24
+ <p className="vta-search-hint">
25
+ Showing {resultCount} of {totalCount} resources
26
+ </p>
27
+ )}
28
+ {noResults && (
29
+ <p className="vta-search-empty">
30
+ No resources match this search.
31
+ </p>
32
+ )}
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,9 @@
1
+ export function SummaryBadge({ count, label }: { count: number; label: string }) {
2
+ if (count === 0) return null;
3
+ return (
4
+ <span className="vta-badge">
5
+ <span className="vta-badge-dot" />
6
+ {count} {label}{count !== 1 ? 's' : ''}
7
+ </span>
8
+ );
9
+ }
@@ -0,0 +1,8 @@
1
+ export { HeroSection } from './HeroSection.js';
2
+ export { SearchBar } from './SearchBar.js';
3
+ export { ResourceSection } from './ResourceSection.js';
4
+ export { ResourceCard } from './ResourceCard.js';
5
+ export { CollectionCard } from './CollectionCard.js';
6
+ export { EndpointPanel } from './EndpointPanel.js';
7
+ export { SummaryBadge } from './SummaryBadge.js';
8
+ export { DetailPage } from './DetailPage.js';
package/src/dev/env.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { Env } from "@vertesia/ui/env";
2
+
3
+ Env.init({
4
+ name: "Tools Admin UI",
5
+ version: "1.0.0",
6
+ isLocalDev: true,
7
+ isDocker: true,
8
+ type: "development",
9
+ endpoints: {
10
+ studio: "https://api.vertesia.io",
11
+ zeno: "https://api.vertesia.io",
12
+ sts: "https://sts.vertesia.io",
13
+ }
14
+ });
@@ -0,0 +1,37 @@
1
+ /// <reference types="vite/client" />
2
+ import "./env.js"
3
+ import { StrictMode } from 'react';
4
+ import { createRoot } from 'react-dom/client';
5
+ import { RouterProvider } from '@vertesia/ui/router';
6
+ import { AdminApp } from '../AdminApp.js';
7
+ import { VertesiaShell } from '@vertesia/ui/shell';
8
+
9
+ const baseUrl = import.meta.env.VITE_API_BASE_URL;
10
+
11
+ const root = createRoot(document.getElementById('root')!);
12
+
13
+ /**
14
+ * In dev mode we wrap with RouterProvider since there's no parent router.
15
+ * In production the plugin host (Studio) provides the router context.
16
+ */
17
+ const devRoutes = [{ path: '/*', Component: () => <AdminApp baseUrl={baseUrl} /> }];
18
+
19
+ if (!baseUrl) {
20
+ root.render(
21
+ <div style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif', color: '#ef4444' }}>
22
+ <h2>Missing environment variable</h2>
23
+ <p><code>VITE_API_BASE_URL</code> is not defined.</p>
24
+ <p>Create a <code>.env.local</code> file in this package with:</p>
25
+ <pre style={{ background: '#f3f4f6', padding: '1rem', borderRadius: '8px', color: '#111827' }}>
26
+ VITE_API_BASE_URL=http://localhost:3000/api</pre>
27
+ </div>,
28
+ );
29
+ } else {
30
+ root.render(
31
+ <StrictMode>
32
+ <VertesiaShell>
33
+ <RouterProvider routes={devRoutes} />
34
+ </VertesiaShell>
35
+ </StrictMode>,
36
+ );
37
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Data fetching hooks for the admin panel.
3
+ */
4
+
5
+ import { useFetch } from '@vertesia/ui/core';
6
+ import type { ServerInfo, ResourceData } from './types.js';
7
+ import { buildResourceData } from './types.js';
8
+
9
+ /**
10
+ * Fetches the tool server info (message, version, endpoints).
11
+ */
12
+ export function useServerInfo(baseUrl: string) {
13
+ return useFetch<ServerInfo>(() =>
14
+ fetch(baseUrl).then(r => r.json()),
15
+ [baseUrl]
16
+ );
17
+ }
18
+
19
+ /**
20
+ * Fetches all 5 resource endpoints in parallel and builds collections + flat resource list.
21
+ * MCP endpoints are passed separately since they come from serverInfo.
22
+ */
23
+ export function useResourceData(baseUrl: string, mcpEndpoints?: string[]) {
24
+ return useFetch<ResourceData>(() => {
25
+ const fetchJson = (path: string) => fetch(`${baseUrl}/${path}`).then(r => r.json());
26
+ return Promise.all([
27
+ fetchJson('interactions'),
28
+ fetchJson('tools'),
29
+ fetchJson('skills'),
30
+ fetchJson('types'),
31
+ fetchJson('templates'),
32
+ ]).then(([interactions, tools, skills, types, templates]) =>
33
+ buildResourceData(interactions, tools, skills, types, templates, mcpEndpoints)
34
+ );
35
+ }, [baseUrl, mcpEndpoints]);
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { AdminApp } from './AdminApp.js';
2
+ export type { AdminAppProps } from './AdminApp.js';
3
+ export type { ServerInfo, ResourceType, ResourceItem, CollectionInfo } from './types.js';
4
+ export { buildResourceData, filterResources } from './types.js';
5
+ export { AdminContext, useAdminContext } from './AdminContext.js';
6
+ export type { AdminContextValue } from './AdminContext.js';
@@ -0,0 +1,99 @@
1
+ import { useMemo, useState } from 'react';
2
+ import type { ResourceType } from '../types.js';
3
+ import { filterResources } from '../types.js';
4
+ import { HeroSection, SearchBar, ResourceSection, CollectionCard } from '../components/index.js';
5
+ import { useAdminContext } from '../AdminContext.js';
6
+
7
+ const sections: { type: ResourceType; title: string; subtitle: string }[] = [
8
+ { type: 'tool', title: 'Tools', subtitle: 'Remote tools available to agents via Vertesia.' },
9
+ { type: 'skill', title: 'Skills', subtitle: 'Reusable instructions and scripts packaged as tools.' },
10
+ { type: 'interaction', title: 'Interactions', subtitle: 'Conversation blueprints surfaced in the Vertesia UI.' },
11
+ { type: 'type', title: 'Content Types', subtitle: 'Schema definitions for structured content in the data store.' },
12
+ { type: 'template', title: 'Rendering Templates', subtitle: 'Document and presentation templates for content generation.' },
13
+ { type: 'mcp', title: 'MCP Providers', subtitle: 'Remote MCP servers available through this tools server.' },
14
+ ];
15
+
16
+ export function HomePage() {
17
+ const { serverInfo, collections, resources } = useAdminContext();
18
+ const [search, setSearch] = useState('');
19
+
20
+ const filtered = useMemo(() =>
21
+ filterResources(resources, search),
22
+ [resources, search]
23
+ );
24
+
25
+ const isSearching = search.trim().length > 0;
26
+
27
+ return (
28
+ <div className="vta-root">
29
+ <HeroSection
30
+ title={serverInfo.message.replace('Vertesia Tools API', 'Tools Server')}
31
+ version={serverInfo.version}
32
+ resources={resources}
33
+ />
34
+
35
+ <SearchBar
36
+ value={search}
37
+ onChange={setSearch}
38
+ placeholder="Search tools, skills, interactions, types, templates..."
39
+ resultCount={filtered.length}
40
+ totalCount={resources.length}
41
+ />
42
+
43
+ {isSearching ? (
44
+ /* Search mode: show individual resource cards */
45
+ sections.map((section, i) => {
46
+ const sectionItems = filtered.filter(r => r.type === section.type);
47
+ return (
48
+ <ResourceSection
49
+ key={section.type}
50
+ title={section.title}
51
+ subtitle={section.subtitle}
52
+ resources={sectionItems}
53
+ showDivider={i > 0}
54
+ />
55
+ );
56
+ })
57
+ ) : (
58
+ /* Browse mode: show collection cards grouped by type */
59
+ sections.map((section, i) => {
60
+ const sectionCollections = collections.filter(c => c.type === section.type);
61
+ const mcpResources = section.type === 'mcp'
62
+ ? resources.filter(r => r.type === 'mcp')
63
+ : [];
64
+
65
+ if (sectionCollections.length === 0 && mcpResources.length === 0) return null;
66
+
67
+ return (
68
+ <section key={section.type}>
69
+ {i > 0 && <hr className="vta-divider" />}
70
+ <div>
71
+ <h2 className="vta-section-title">
72
+ {section.title}
73
+ <span className="vta-section-count">
74
+ ({sectionCollections.length}{sectionCollections.length === 1
75
+ ? ' collection' : ' collections'})
76
+ </span>
77
+ </h2>
78
+ <p className="vta-section-subtitle">{section.subtitle}</p>
79
+ </div>
80
+ <div className="vta-card-grid">
81
+ {sectionCollections.map(col => (
82
+ <CollectionCard key={`${col.type}:${col.name}`} collection={col} />
83
+ ))}
84
+ {mcpResources.map(r => (
85
+ <div key={r.name} className="vta-card">
86
+ <span className="vta-card-type vta-card-type--mcp">mcp</span>
87
+ <div className="vta-card-title">{r.title}</div>
88
+ <div className="vta-card-desc">{r.description || 'No description'}</div>
89
+ {r.url && <div className="vta-card-url">{r.url}</div>}
90
+ </div>
91
+ ))}
92
+ </div>
93
+ </section>
94
+ );
95
+ })
96
+ )}
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,59 @@
1
+ import { useFetch } from '@vertesia/ui/core';
2
+ import { useParams, NavLink } from '@vertesia/ui/router';
3
+ import type { CatalogInteractionRef } from '@vertesia/common';
4
+ import { useAdminContext } from '../AdminContext.js';
5
+ import { DetailPage } from '../components/DetailPage.js';
6
+
7
+ export function InteractionCollection() {
8
+ const collection = useParams('collection');
9
+ const { baseUrl } = useAdminContext();
10
+
11
+ const { data: interactions, isLoading, error } = useFetch<CatalogInteractionRef[]>(
12
+ () => fetch(`${baseUrl}/interactions/${collection}`).then(r => {
13
+ if (!r.ok) throw new Error(`Failed to load collection: ${r.statusText}`);
14
+ return r.json();
15
+ }),
16
+ [baseUrl, collection]
17
+ );
18
+
19
+ if (isLoading) {
20
+ return <div className="vta-loading">Loading collection...</div>;
21
+ }
22
+
23
+ if (error || !interactions) {
24
+ return <div className="vta-error">Failed to load collection &ldquo;{collection}&rdquo;.</div>;
25
+ }
26
+
27
+ return (
28
+ <DetailPage
29
+ type="interaction"
30
+ title={collection}
31
+ description={`${interactions.length} interaction${interactions.length !== 1 ? 's' : ''} in this collection.`}
32
+ >
33
+ <div className="vta-card-grid">
34
+ {interactions.map(inter => (
35
+ <NavLink
36
+ key={inter.id}
37
+ href={`/interactions/${collection}/${inter.name}`}
38
+ className="vta-card-link"
39
+ >
40
+ <div className="vta-card vta-card--link">
41
+ <span className="vta-card-type vta-card-type--interaction">interaction</span>
42
+ <div className="vta-card-title">{inter.title || inter.name}</div>
43
+ <div className="vta-card-desc">
44
+ {inter.description || 'No description'}
45
+ </div>
46
+ {inter.tags && inter.tags.length > 0 && (
47
+ <div className="vta-card-tags">
48
+ {inter.tags.map(tag => (
49
+ <span key={tag} className="vta-tag">{tag}</span>
50
+ ))}
51
+ </div>
52
+ )}
53
+ </div>
54
+ </NavLink>
55
+ ))}
56
+ </div>
57
+ </DetailPage>
58
+ );
59
+ }
@@ -0,0 +1,92 @@
1
+ import { useFetch } from '@vertesia/ui/core';
2
+ import { useParams } from '@vertesia/ui/router';
3
+ import type { InteractionSpec } from '@vertesia/common';
4
+ import { useAdminContext } from '../AdminContext.js';
5
+ import { DetailPage } from '../components/DetailPage.js';
6
+ import { useUserSession } from '@vertesia/ui/session';
7
+
8
+ type InteractionResponse = InteractionSpec & { id: string };
9
+
10
+ export function InteractionDetail() {
11
+ const { client } = useUserSession();
12
+ const params = useParams();
13
+ const collection = params.collection;
14
+ const name = params.name;
15
+ const { baseUrl } = useAdminContext();
16
+
17
+ const { data: interaction, isLoading, error } = useFetch<InteractionResponse>(
18
+ () => client.getRawJWT().then(token => fetch(`${baseUrl}/interactions/${collection}/${name}`, {
19
+ headers: {
20
+ Authorization: `Bearer ${token}`,
21
+ },
22
+ })).then(r => {
23
+ if (!r.ok) throw new Error(`Failed to load interaction: ${r.statusText}`);
24
+ return r.json();
25
+ }),
26
+ [baseUrl, collection, name]
27
+ );
28
+
29
+ if (isLoading) {
30
+ return <div className="vta-loading">Loading interaction...</div>;
31
+ }
32
+
33
+ if (error || !interaction) {
34
+ return <div className="vta-error">Failed to load interaction &ldquo;{name}&rdquo;.</div>;
35
+ }
36
+
37
+ const { agent_runner_options } = interaction;
38
+ const hasAgentFlags = agent_runner_options &&
39
+ (agent_runner_options.is_agent || agent_runner_options.is_tool || agent_runner_options.is_skill);
40
+
41
+ return (
42
+ <DetailPage
43
+ type="interaction"
44
+ title={interaction.title || interaction.name}
45
+ description={interaction.description}
46
+ tags={interaction.tags}
47
+ backHref={`/interactions/${collection}`}
48
+ >
49
+ {/* Prompts */}
50
+ {interaction.prompts && interaction.prompts.length > 0 && (
51
+ <div className="vta-detail-section">
52
+ <h2>Prompts</h2>
53
+ {interaction.prompts.map((prompt, i) => (
54
+ <div key={i} className="vta-detail-card">
55
+ <div className="vta-detail-card-header">
56
+ <span className={`vta-detail-role vta-detail-role--${prompt.role}`}>
57
+ {prompt.role}
58
+ </span>
59
+ {prompt.name && (
60
+ <span className="vta-detail-prompt-name">{prompt.name}</span>
61
+ )}
62
+ </div>
63
+ <pre className="vta-detail-code">{prompt.content}</pre>
64
+ </div>
65
+ ))}
66
+ </div>
67
+ )}
68
+
69
+ {/* Result schema */}
70
+ {interaction.result_schema && (
71
+ <div className="vta-detail-section">
72
+ <h2>Result Schema</h2>
73
+ <pre className="vta-detail-code">
74
+ {JSON.stringify(interaction.result_schema, null, 2)}
75
+ </pre>
76
+ </div>
77
+ )}
78
+
79
+ {/* Agent runner options */}
80
+ {hasAgentFlags && (
81
+ <div className="vta-detail-section">
82
+ <h2>Agent Runner</h2>
83
+ <div className="vta-detail-flags">
84
+ {agent_runner_options.is_agent && <span className="vta-detail-flag">Agent</span>}
85
+ {agent_runner_options.is_tool && <span className="vta-detail-flag">Tool</span>}
86
+ {agent_runner_options.is_skill && <span className="vta-detail-flag">Skill</span>}
87
+ </div>
88
+ </div>
89
+ )}
90
+ </DetailPage>
91
+ );
92
+ }