Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions tavro_app/src/components/AgentHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import React from 'react';
import { AgentData } from '../types/agent';
import { Bot, ExternalLink, Globe, BookOpen, ShieldAlert, CheckCircle2 } from 'lucide-react';
import { Bot, ExternalLink, Globe, BookOpen, ShieldAlert, CheckCircle2, Loader2 } from 'lucide-react';
import { getAgentRiskLevel } from '../utils/agentRisk';

interface AgentHeaderProps { agent: AgentData; }
type AgentInlineField = 'name' | 'description' | 'instruction';

interface AgentHeaderProps {
agent: AgentData;
isEditing?: boolean;
editName?: string;
onEditNameChange?: (v: string) => void;
inlineEdit?: { field: AgentInlineField; value: string } | null;
inlineSaving?: AgentInlineField | null;
onStartInlineEdit?: (field: AgentInlineField) => void;
onInlineValueChange?: (value: string) => void;
onSaveInlineEdit?: () => void;
onCancelInlineEdit?: () => void;
}

const Badge: React.FC<{ text: string; color?: 'blue' | 'emerald' | 'amber' | 'rose' | 'slate' }> = ({ text, color = 'slate' }) => {
const cls = {
Expand All @@ -16,7 +29,18 @@ const Badge: React.FC<{ text: string; color?: 'blue' | 'emerald' | 'amber' | 'ro
return <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold border uppercase tracking-wide ${cls}`}>{text}</span>;
};

const AgentHeader: React.FC<AgentHeaderProps> = ({ agent }) => {
const AgentHeader: React.FC<AgentHeaderProps> = ({
agent,
isEditing,
editName,
onEditNameChange,
inlineEdit,
inlineSaving,
onStartInlineEdit,
onInlineValueChange,
onSaveInlineEdit,
onCancelInlineEdit,
}) => {
const id = agent.identification;
const caps = agent.capabilities;

Expand All @@ -43,15 +67,61 @@ const AgentHeader: React.FC<AgentHeaderProps> = ({ agent }) => {
: riskLevel === 'medium'
? 'text-amber-600'
: 'text-emerald-600';
const isInlineName = inlineEdit?.field === 'name';
const isSavingName = inlineSaving === 'name';
const nameSaveDisabled = isSavingName || !inlineEdit?.value.trim();
return (
<div className="bg-white border border-slate-200 shadow-sm rounded-2xl overflow-hidden flex flex-col">
<div className="p-6 bg-slate-50 border-b border-slate-100 flex flex-col md:flex-row justify-between items-start md:items-center gap-6 flex-wrap">
<div className="flex items-start gap-4 min-w-0 flex-1 md:max-w-[60%]">
<div className="p-3 bg-blue-600 text-white rounded-xl shadow-sm mt-1 shrink-0">
<Bot size={28} />
</div>
<div className="flex flex-col gap-1.5 min-w-0">
<h2 className="text-2xl font-bold text-slate-800 tracking-tight break-words">{agent.name}</h2>
<div className="flex flex-col gap-1.5 min-w-0 flex-1">
{isEditing ? (
<input
type="text"
value={editName ?? agent.name}
onChange={e => onEditNameChange?.(e.target.value)}
className="text-2xl font-bold text-slate-800 tracking-tight w-full border-b-2 border-blue-400 bg-transparent outline-none pb-0.5"
/>
) : isInlineName && inlineEdit ? (
<div className="flex items-center gap-2 w-full">
<input
type="text"
value={inlineEdit.value}
onChange={e => onInlineValueChange?.(e.target.value)}
className="text-2xl font-bold text-slate-800 tracking-tight flex-1 border-b-2 border-blue-400 bg-transparent outline-none pb-0.5"
autoFocus
/>
<button
type="button"
onClick={onSaveInlineEdit}
disabled={nameSaveDisabled}
title={!inlineEdit.value.trim() ? 'Agent Name is required' : 'Save'}
className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-lg bg-blue-600 text-xs font-black text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-300"
>
{isSavingName ? <Loader2 size={14} className="animate-spin" /> : '✓'}
</button>
<button
type="button"
onClick={onCancelInlineEdit}
disabled={isSavingName}
title="Cancel"
className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white text-xs font-black text-slate-600 hover:bg-slate-50 disabled:opacity-50"
>
</button>
</div>
) : (
<h2
onDoubleClick={() => onStartInlineEdit?.('name')}
title="Double-click to edit"
className="text-2xl font-bold text-slate-800 tracking-tight break-words cursor-text rounded-lg hover:bg-blue-50/50 transition-colors"
>
{agent.name}
</h2>
)}
<div className="flex items-center gap-2 flex-wrap">
<span className="font-mono text-xs bg-white px-2 py-0.5 rounded border border-slate-200 text-slate-600 break-all">
{id?.agent_id || 'N/A'}
Expand Down
130 changes: 114 additions & 16 deletions tavro_app/src/components/AgentIdentificationTab.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,67 @@
import React, { useEffect, useRef, useState } from 'react';
import { AgentData } from '../types/agent';
import { User, Tag, ChevronDown, ChevronUp } from 'lucide-react';
import { User, Tag, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';

interface AgentIdentificationTabProps { agent: AgentData; }
type AgentInlineField = 'name' | 'description' | 'instruction';

export const AgentIdentificationTab: React.FC<AgentIdentificationTabProps> = ({ agent }) => {
interface AgentIdentificationTabProps {
agent: AgentData;
isEditing?: boolean;
editDescription?: string;
onEditDescriptionChange?: (v: string) => void;
editInstruction?: string;
onEditInstructionChange?: (v: string) => void;
inlineEdit?: { field: AgentInlineField; value: string } | null;
inlineSaving?: AgentInlineField | null;
onStartInlineEdit?: (field: AgentInlineField) => void;
onInlineValueChange?: (value: string) => void;
onSaveInlineEdit?: () => void;
onCancelInlineEdit?: () => void;
}

export const AgentIdentificationTab: React.FC<AgentIdentificationTabProps> = ({
agent, isEditing,
editDescription, onEditDescriptionChange,
editInstruction, onEditInstructionChange,
inlineEdit, inlineSaving, onStartInlineEdit,
onInlineValueChange, onSaveInlineEdit, onCancelInlineEdit,
}) => {
const [instrOpen, setInstrOpen] = useState(false);
const [instrOverflow, setInstrOverflow] = useState(false);
const instructionContainerRef = useRef<HTMLDivElement | null>(null);
const id = agent.identification;
const COLLAPSED_MAX_HEIGHT_PX = 256; // max-h-64
const COLLAPSED_MAX_HEIGHT_PX = 128; // max-h-32
const isInlineDescription = inlineEdit?.field === 'description';
const isInlineInstruction = inlineEdit?.field === 'instruction';

const renderInlineActions = (field: AgentInlineField) => {
const isSaving = inlineSaving === field;
const isBlank = !inlineEdit?.value.trim();
const saveDisabled = isSaving || isBlank;

return (
<div className="flex shrink-0 gap-1">
<button
type="button"
onClick={onSaveInlineEdit}
disabled={saveDisabled}
title={isBlank ? 'This field is required' : 'Save'}
className="inline-flex h-6 w-6 items-center justify-center rounded-lg bg-blue-600 text-xs font-black text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-300"
>
{isSaving ? <Loader2 size={14} className="animate-spin" /> : '✓'}
</button>
<button
type="button"
onClick={onCancelInlineEdit}
disabled={isSaving}
title="Cancel"
className="inline-flex h-6 w-6 items-center justify-center rounded-lg border border-slate-200 bg-white text-xs font-black text-slate-600 hover:bg-slate-50 disabled:opacity-50"
>
</button>
</div>
);
};

useEffect(() => {
const node = instructionContainerRef.current;
Expand All @@ -26,9 +78,33 @@ export const AgentIdentificationTab: React.FC<AgentIdentificationTabProps> = ({
Identification & Role
</h3>

<p className="text-sm text-slate-600 leading-relaxed border-l-2 border-blue-200 pl-4 py-1 mb-4">
{agent.description}
</p>
{isEditing ? (
<textarea
value={editDescription ?? agent.description ?? ''}
onChange={e => onEditDescriptionChange?.(e.target.value)}
rows={3}
className="w-full text-sm text-slate-600 leading-relaxed border-l-2 border-blue-400 pl-4 py-1 mb-4 bg-blue-50/40 outline-none resize-none rounded-r-lg"
/>
) : isInlineDescription && inlineEdit ? (
<div className="flex items-start gap-2 mb-4">
<textarea
value={inlineEdit.value}
onChange={e => onInlineValueChange?.(e.target.value)}
rows={3}
className="w-full text-sm text-slate-600 leading-relaxed border-l-2 border-blue-400 pl-4 py-1 bg-blue-50/40 outline-none resize-none rounded-r-lg"
autoFocus
/>
{renderInlineActions('description')}
</div>
) : (
<p
onDoubleClick={() => onStartInlineEdit?.('description')}
title="Double-click to edit"
className="text-sm text-slate-600 leading-relaxed border-l-2 border-blue-200 pl-4 py-1 mb-4 cursor-text rounded-r-lg hover:bg-blue-50/40 transition-colors"
>
{agent.description || '-'}
</p>
)}

{(id?.owner || id?.tags) && (
<div className="flex flex-wrap gap-2 mb-4">
Expand All @@ -53,7 +129,7 @@ export const AgentIdentificationTab: React.FC<AgentIdentificationTabProps> = ({
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">System Instruction</span>
{instrOverflow && (
{instrOverflow && !isInlineInstruction && (
<button
onClick={() => setInstrOpen(o => !o)}
className="text-[10px] font-semibold text-blue-500 hover:text-blue-700 flex items-center gap-1"
Expand All @@ -62,14 +138,36 @@ export const AgentIdentificationTab: React.FC<AgentIdentificationTabProps> = ({
</button>
)}
</div>
<div
ref={instructionContainerRef}
className={`overflow-hidden transition-all duration-300 ease-in-out ${instrOverflow ? (instrOpen ? 'max-h-[2500px]' : 'max-h-64') : 'max-h-none'} overflow-y-auto pr-1`}
>
<pre className="text-xs font-mono text-slate-600 whitespace-pre-wrap leading-relaxed">
{id?.instruction || '-'}
</pre>
</div>
{isEditing ? (
<textarea
value={editInstruction ?? id?.instruction ?? ''}
onChange={e => onEditInstructionChange?.(e.target.value)}
rows={8}
className="w-full text-xs font-mono text-slate-600 leading-relaxed bg-white border border-blue-300 rounded-lg px-3 py-2 outline-none focus:ring-2 focus:ring-blue-400/20 resize-none mt-1"
/>
) : isInlineInstruction && inlineEdit ? (
<div className="flex items-start gap-2 mt-1">
<textarea
value={inlineEdit.value}
onChange={e => onInlineValueChange?.(e.target.value)}
rows={8}
className="w-full text-xs font-mono text-slate-600 leading-relaxed bg-white border border-blue-300 rounded-lg px-3 py-2 outline-none focus:ring-2 focus:ring-blue-400/20 resize-none"
autoFocus
/>
{renderInlineActions('instruction')}
</div>
) : (
<div
ref={instructionContainerRef}
onDoubleClick={() => onStartInlineEdit?.('instruction')}
title="Double-click to edit"
className={`overflow-hidden transition-all duration-300 ease-in-out ${instrOverflow ? (instrOpen ? 'max-h-[2500px]' : 'max-h-32') : 'max-h-none'} overflow-y-auto pr-1`}
>
<pre className="text-xs font-mono text-slate-600 whitespace-pre-wrap leading-relaxed cursor-text rounded-lg hover:bg-blue-50/50 transition-colors">
{id?.instruction || '-'}
</pre>
</div>
)}
</div>
</div>
</div>
Expand Down
54 changes: 51 additions & 3 deletions tavro_app/src/components/AgentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,24 @@ import AgentLineage from './AgentLineage';
import AgentRiskSummary from './AgentRiskSummary';
import AgentContextGraph from './AgentContextGraphRF';

type AgentInlineField = 'name' | 'description' | 'instruction';

interface AgentViewProps {
agent: AgentData;
onBusinessImpactChange?: (snapshot: AgentBusinessImpactSnapshot) => void;
isEditing?: boolean;
editName?: string;
onEditNameChange?: (v: string) => void;
editDescription?: string;
onEditDescriptionChange?: (v: string) => void;
editInstruction?: string;
onEditInstructionChange?: (v: string) => void;
inlineEdit?: { field: AgentInlineField; value: string } | null;
inlineSaving?: AgentInlineField | null;
onStartInlineEdit?: (field: AgentInlineField) => void;
onInlineValueChange?: (value: string) => void;
onSaveInlineEdit?: () => void;
onCancelInlineEdit?: () => void;
}

type TabType =
Expand All @@ -32,7 +47,14 @@ const BASE_TABS: { id: TabType; label: string }[] = [
{ id: 'CONTEXT', label: 'Context Graph' },
];

const AgentView: React.FC<AgentViewProps> = ({ agent, onBusinessImpactChange }) => {
const AgentView: React.FC<AgentViewProps> = ({
agent, onBusinessImpactChange,
isEditing, editName, onEditNameChange,
editDescription, onEditDescriptionChange,
editInstruction, onEditInstructionChange,
inlineEdit, inlineSaving, onStartInlineEdit,
onInlineValueChange, onSaveInlineEdit, onCancelInlineEdit,
}) => {
const [activeTab, setActiveTab] = useState<TabType>('IDENTIFICATION');
const agentId = agent.identification?.agent_id;

Expand All @@ -46,7 +68,18 @@ const AgentView: React.FC<AgentViewProps> = ({ agent, onBusinessImpactChange })

{/* Header (always visible) */}
<div className="-mt-6">
<AgentHeader agent={agent} />
<AgentHeader
agent={agent}
isEditing={isEditing}
editName={editName}
onEditNameChange={onEditNameChange}
inlineEdit={inlineEdit}
inlineSaving={inlineSaving}
onStartInlineEdit={onStartInlineEdit}
onInlineValueChange={onInlineValueChange}
onSaveInlineEdit={onSaveInlineEdit}
onCancelInlineEdit={onCancelInlineEdit}
/>
</div>

{/* Tab Navigation */}
Expand All @@ -67,7 +100,22 @@ const AgentView: React.FC<AgentViewProps> = ({ agent, onBusinessImpactChange })

{/* Tab Content */}
<div>
{activeTab === 'IDENTIFICATION' && <AgentIdentificationTab agent={agent} />}
{activeTab === 'IDENTIFICATION' && (
<AgentIdentificationTab
agent={agent}
isEditing={isEditing}
editDescription={editDescription}
onEditDescriptionChange={onEditDescriptionChange}
editInstruction={editInstruction}
onEditInstructionChange={onEditInstructionChange}
inlineEdit={inlineEdit}
inlineSaving={inlineSaving}
onStartInlineEdit={onStartInlineEdit}
onInlineValueChange={onInlineValueChange}
onSaveInlineEdit={onSaveInlineEdit}
onCancelInlineEdit={onCancelInlineEdit}
/>
)}

{activeTab === 'CONFIG' && <AgentTechConfigTab agent={agent} />}

Expand Down
1 change: 0 additions & 1 deletion tavro_app/src/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,6 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ onClose }) => {
<MessageCircle size={14} />
</div>
<div className="min-w-0">
<h2 className="font-semibold text-slate-800 text-sm leading-tight truncate">Tavro AI Assistant</h2>
{getContextBadge(viewType, viewData) && (
<span className="text-[10px] font-semibold text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 px-1.5 py-0.5 rounded-full leading-tight mt-0.5">
{getContextBadge(viewType, viewData)}
Expand Down
Loading