359 lines
15 KiB
TypeScript
359 lines
15 KiB
TypeScript
import {
|
|
mdiArrowLeft,
|
|
mdiChartTimelineVariant,
|
|
mdiPencilOutline,
|
|
mdiRobotOutline,
|
|
mdiTextBoxOutline,
|
|
mdiTuneVariant,
|
|
} from '@mdi/js';
|
|
import Head from 'next/head';
|
|
import Link from 'next/link';
|
|
import React, { ReactElement, useEffect } from 'react';
|
|
|
|
import BaseIcon from '../../components/BaseIcon';
|
|
import LoadingSpinner from '../../components/LoadingSpinner';
|
|
import SectionMain from '../../components/SectionMain';
|
|
import { getPageTitle } from '../../config';
|
|
import LayoutAuthenticated from '../../layouts/Authenticated';
|
|
import { fetch } from '../../stores/agents/agentsSlice';
|
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
|
import { useRouter } from 'next/router';
|
|
|
|
const actionButtonClassName =
|
|
'inline-flex items-center justify-center rounded-[8px] border px-4 py-2 text-sm font-medium';
|
|
|
|
function formatDate(value: string) {
|
|
if (!value) {
|
|
return 'No date';
|
|
}
|
|
|
|
const date = new Date(value);
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return value;
|
|
}
|
|
|
|
return date.toLocaleString('en-US', {
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
month: 'short',
|
|
});
|
|
}
|
|
|
|
function formatTemperature(value: any) {
|
|
if (value === null || value === undefined || value === '') {
|
|
return 'Auto';
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function formatTokenLimit(value: any) {
|
|
if (!value) {
|
|
return 'Default';
|
|
}
|
|
|
|
return `${value} max tokens`;
|
|
}
|
|
|
|
function parseMetadata(value: string) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function RelatedItem({
|
|
href,
|
|
meta,
|
|
title,
|
|
}: {
|
|
href: string;
|
|
meta: string;
|
|
title: string;
|
|
}) {
|
|
return (
|
|
<Link
|
|
className="flex items-start justify-between gap-4 rounded-[10px] border border-slate-200 bg-white px-4 py-3 transition-colors hover:border-slate-300"
|
|
href={href}
|
|
>
|
|
<div className="min-w-0">
|
|
<p className="truncate text-[14px] font-medium text-slate-900">{title}</p>
|
|
<p className="mt-1 text-[12px] leading-5 text-slate-500">{meta}</p>
|
|
</div>
|
|
<BaseIcon className="shrink-0 text-slate-400" path={mdiChartTimelineVariant} size={16} />
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
const AgentsView = () => {
|
|
const router = useRouter();
|
|
const dispatch = useAppDispatch();
|
|
const { agents, loading } = useAppSelector((state) => state.agents);
|
|
const { id } = router.query;
|
|
|
|
useEffect(() => {
|
|
if (typeof id !== 'string') {
|
|
return;
|
|
}
|
|
|
|
dispatch(fetch({ id }));
|
|
}, [dispatch, id]);
|
|
|
|
const agent = !Array.isArray(agents) && agents && typeof agents === 'object' ? agents : null;
|
|
const metadata = parseMetadata(agent?.metadata_json || '');
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>{getPageTitle('Agent details')}</title>
|
|
</Head>
|
|
<SectionMain>
|
|
<div className="flex w-full flex-col gap-5">
|
|
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
|
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
<div>
|
|
<Link
|
|
className="inline-flex items-center gap-2 text-[12px] font-medium text-slate-500"
|
|
href="/agents/agents-list"
|
|
>
|
|
<BaseIcon path={mdiArrowLeft} size={14} />
|
|
Back to agents
|
|
</Link>
|
|
<p className="mt-4 text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
|
Agent
|
|
</p>
|
|
<h1 className="mt-3 text-[2rem] font-semibold tracking-[-0.04em] text-slate-900">
|
|
{agent?.name || 'Agent details'}
|
|
</h1>
|
|
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-500">
|
|
Review the prompt, model behavior, and recent activity linked to this assistant profile.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Link
|
|
className={`${actionButtonClassName} border-slate-200 bg-white text-slate-700`}
|
|
href="/agents/agents-list"
|
|
>
|
|
Close
|
|
</Link>
|
|
<Link
|
|
className={`${actionButtonClassName} border-slate-900 bg-slate-900 text-white`}
|
|
href={`/agents/agents-edit/?id=${id}`}
|
|
>
|
|
<BaseIcon className="mr-1" path={mdiPencilOutline} size={16} />
|
|
Edit agent
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{loading && !agent && (
|
|
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-10">
|
|
<LoadingSpinner />
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !agent && (
|
|
<div className="rounded-[12px] border border-dashed border-slate-200 bg-slate-50 px-6 py-12 text-center">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.2em] text-slate-400">
|
|
No data
|
|
</p>
|
|
<h2 className="mt-3 text-[22px] font-semibold tracking-[-0.03em] text-slate-900">
|
|
We could not load this agent.
|
|
</h2>
|
|
<p className="mt-3 text-[14px] leading-6 text-slate-500">
|
|
Try going back to the agents list and opening it again.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{agent && (
|
|
<>
|
|
<div className="grid gap-5 2xl:grid-cols-[minmax(0,1fr)_320px]">
|
|
<div className="space-y-5">
|
|
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
|
|
<div className="mb-5 flex items-start gap-3">
|
|
<div className="mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
|
|
<BaseIcon path={mdiRobotOutline} size={20} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[15px] font-medium text-slate-900">Identity</p>
|
|
<p className="mt-1 text-sm leading-6 text-slate-500">
|
|
Basic positioning and the model this assistant runs on.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="rounded-[10px] border border-slate-200 px-4 py-3">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-400">
|
|
Name
|
|
</p>
|
|
<p className="mt-2 text-[16px] font-medium text-slate-900">
|
|
{agent.name || 'Untitled agent'}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-[10px] border border-slate-200 px-4 py-3">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-400">
|
|
Model
|
|
</p>
|
|
<p className="mt-2 text-[16px] font-medium text-slate-900">
|
|
{agent.model || 'Default model'}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-[10px] border border-slate-200 px-4 py-3 md:col-span-2">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-400">
|
|
Description
|
|
</p>
|
|
<p className="mt-2 text-[14px] leading-6 text-slate-600">
|
|
{agent.description || 'No description yet.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
|
|
<div className="mb-5 flex items-start gap-3">
|
|
<div className="mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
|
|
<BaseIcon path={mdiTextBoxOutline} size={20} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[15px] font-medium text-slate-900">System prompt</p>
|
|
<p className="mt-1 text-sm leading-6 text-slate-500">
|
|
The standing instructions this agent follows in every conversation.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-[10px] border border-slate-200 bg-slate-50 px-4 py-4">
|
|
<pre className="whitespace-pre-wrap text-[13px] leading-6 text-slate-700">
|
|
{agent.system_prompt || 'No system prompt yet.'}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
|
|
<div className="mb-5 flex items-start gap-3">
|
|
<div className="mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
|
|
<BaseIcon path={mdiTuneVariant} size={20} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[15px] font-medium text-slate-900">Behavior</p>
|
|
<p className="mt-1 text-sm leading-6 text-slate-500">
|
|
Output shaping, default availability, and any structured metadata stored with the agent.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="rounded-[10px] border border-slate-200 px-4 py-3">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-400">
|
|
Temperature
|
|
</p>
|
|
<p className="mt-2 text-[16px] font-medium text-slate-900">
|
|
{formatTemperature(agent.temperature)}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-[10px] border border-slate-200 px-4 py-3">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-400">
|
|
Output
|
|
</p>
|
|
<p className="mt-2 text-[16px] font-medium text-slate-900">
|
|
{formatTokenLimit(agent.max_output_tokens)}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-[10px] border border-slate-200 px-4 py-3">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-400">
|
|
Availability
|
|
</p>
|
|
<p className="mt-2 text-[16px] font-medium text-slate-900">
|
|
{agent.is_active ? 'Active' : 'Inactive'}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-[10px] border border-slate-200 px-4 py-3">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-400">
|
|
Preset type
|
|
</p>
|
|
<p className="mt-2 text-[16px] font-medium text-slate-900">
|
|
{agent.is_default ? 'Default preset' : 'Custom preset'}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-[10px] border border-slate-200 px-4 py-3 md:col-span-2">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-400">
|
|
Metadata JSON
|
|
</p>
|
|
<pre className="mt-2 whitespace-pre-wrap text-[13px] leading-6 text-slate-600">
|
|
{metadata ? JSON.stringify(metadata, null, 2) : agent.metadata_json || 'No metadata.'}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-5">
|
|
<div className="rounded-[12px] border border-slate-200 bg-white px-5 py-5">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
|
Related conversations
|
|
</p>
|
|
<div className="mt-4 space-y-3">
|
|
{Array.isArray(agent.conversations_agent) && agent.conversations_agent.length > 0 ? (
|
|
agent.conversations_agent.map((item: any) => (
|
|
<RelatedItem
|
|
key={item.id}
|
|
href={`/conversations/conversations-view/?id=${item.id}`}
|
|
meta={`${item.status || 'unknown'} · ${formatDate(item.last_message_at)}`}
|
|
title={item.title || 'Untitled conversation'}
|
|
/>
|
|
))
|
|
) : (
|
|
<p className="text-[13px] leading-6 text-slate-500">
|
|
No conversations linked to this agent yet.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-[12px] border border-slate-200 bg-white px-5 py-5">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
|
Usage events
|
|
</p>
|
|
<div className="mt-4 space-y-3">
|
|
{Array.isArray(agent.usage_events_agent) && agent.usage_events_agent.length > 0 ? (
|
|
agent.usage_events_agent.map((item: any) => (
|
|
<RelatedItem
|
|
key={item.id}
|
|
href={`/usage_events/usage_events-view/?id=${item.id}`}
|
|
meta={`${item.provider || 'provider'} · ${formatDate(item.occurred_at)}`}
|
|
title={item.event_type || 'Usage event'}
|
|
/>
|
|
))
|
|
) : (
|
|
<p className="text-[13px] leading-6 text-slate-500">
|
|
No usage activity has been recorded for this agent yet.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</SectionMain>
|
|
</>
|
|
);
|
|
};
|
|
|
|
AgentsView.getLayout = function getLayout(page: ReactElement) {
|
|
return <LayoutAuthenticated permission="READ_AGENTS">{page}</LayoutAuthenticated>;
|
|
};
|
|
|
|
export default AgentsView;
|