mirror of https://github.com/kortix-ai/suna.git
chore(ui): sync custom agents config with credentials profile
This commit is contained in:
parent
720e31ad78
commit
333777213e
|
@ -1,13 +1,15 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Loader2, Save, Sparkles } from 'lucide-react';
|
||||
import { Loader2, Save, Sparkles, AlertTriangle } from 'lucide-react';
|
||||
import { useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
|
||||
import { CredentialProfileSelector } from '@/components/workflows/CredentialProfileSelector';
|
||||
import { useCredentialProfilesForMcp, type CredentialProfile } from '@/hooks/react-query/mcp/use-credential-profiles';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { MCPConfiguration } from './types';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
interface ConfigDialogProps {
|
||||
server: any;
|
||||
|
@ -22,20 +24,27 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
|
|||
onSave,
|
||||
onCancel
|
||||
}) => {
|
||||
const [config, setConfig] = useState<Record<string, any>>(existingConfig?.config || {});
|
||||
const [selectedTools, setSelectedTools] = useState<Set<string>>(
|
||||
new Set(existingConfig?.enabledTools || [])
|
||||
);
|
||||
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(
|
||||
existingConfig?.selectedProfileId || null
|
||||
);
|
||||
|
||||
const { data: serverDetails, isLoading } = useMCPServerDetails(server.qualifiedName);
|
||||
|
||||
const requiresConfig = serverDetails?.connections?.[0]?.configSchema?.properties &&
|
||||
Object.keys(serverDetails.connections[0].configSchema.properties).length > 0;
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
const mcpConfig: MCPConfiguration = {
|
||||
name: server.displayName || server.name || server.qualifiedName,
|
||||
qualifiedName: server.qualifiedName,
|
||||
config,
|
||||
config: {}, // Always use empty config since we're using profiles
|
||||
enabledTools: Array.from(selectedTools),
|
||||
});
|
||||
selectedProfileId: selectedProfileId || undefined,
|
||||
};
|
||||
onSave(mcpConfig);
|
||||
};
|
||||
|
||||
const handleToolToggle = (toolName: string) => {
|
||||
|
@ -48,6 +57,10 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
|
|||
setSelectedTools(newTools);
|
||||
};
|
||||
|
||||
const handleProfileSelect = (profileId: string | null, profile: CredentialProfile | null) => {
|
||||
setSelectedProfileId(profileId);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent className="max-w-5xl max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
|
@ -86,32 +99,32 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
|
|||
<div className="w-1 h-4 bg-primary rounded-full" />
|
||||
Connection Settings
|
||||
</h3>
|
||||
{serverDetails?.connections?.[0]?.configSchema?.properties ? (
|
||||
<ScrollArea className="border bg-muted/30 rounded-lg p-4 flex-1 min-h-0">
|
||||
<div>
|
||||
{Object.entries(serverDetails.connections[0].configSchema.properties).map(([key, schema]: [string, any]) => (
|
||||
<div key={key} className="space-y-2">
|
||||
<Label htmlFor={key} className="text-sm font-medium">
|
||||
{schema.title || key}
|
||||
{serverDetails.connections[0].configSchema.required?.includes(key) && (
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id={key}
|
||||
type={schema.format === 'password' ? 'password' : 'text'}
|
||||
placeholder={schema.description || `Enter ${key}`}
|
||||
value={config[key] || ''}
|
||||
onChange={(e) => setConfig({ ...config, [key]: e.target.value })}
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{requiresConfig ? (
|
||||
<div className="space-y-4">
|
||||
<CredentialProfileSelector
|
||||
mcpQualifiedName={server.qualifiedName}
|
||||
mcpDisplayName={server.displayName || server.name}
|
||||
selectedProfileId={selectedProfileId || undefined}
|
||||
onProfileSelect={handleProfileSelect}
|
||||
/>
|
||||
|
||||
{!selectedProfileId && (
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Please select or create a credential profile to configure this MCP server.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||
<p className="text-sm">No configuration required</p>
|
||||
<Card className="border-dashed border-2 w-full">
|
||||
<CardContent className="p-6 text-center">
|
||||
<p className="text-sm">No configuration required for this MCP server</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -164,7 +177,11 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
|
|||
</ScrollArea>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||
<p className="text-sm">No tools available</p>
|
||||
<Card className="border-dashed border-2 w-full">
|
||||
<CardContent className="p-6 text-center">
|
||||
<p className="text-sm">No tools available for this MCP server</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -177,7 +194,7 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
|
|||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || (requiresConfig && !selectedProfileId)}
|
||||
className="bg-primary hover:bg-primary/90"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Settings, X, Sparkles } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Settings, X, Sparkles, Key, AlertTriangle } from 'lucide-react';
|
||||
import { MCPConfiguration } from './types';
|
||||
|
||||
import { useCredentialProfilesForMcp } from '@/hooks/react-query/mcp/use-credential-profiles';
|
||||
|
||||
interface ConfiguredMcpListProps {
|
||||
configuredMCPs: MCPConfiguration[];
|
||||
|
@ -11,6 +12,74 @@ interface ConfiguredMcpListProps {
|
|||
onRemove: (index: number) => void;
|
||||
}
|
||||
|
||||
const MCPConfigurationItem: React.FC<{
|
||||
mcp: MCPConfiguration;
|
||||
index: number;
|
||||
onEdit: (index: number) => void;
|
||||
onRemove: (index: number) => void;
|
||||
}> = ({ mcp, index, onEdit, onRemove }) => {
|
||||
const { data: profiles = [] } = useCredentialProfilesForMcp(mcp.qualifiedName);
|
||||
const selectedProfile = profiles.find(p => p.profile_id === mcp.selectedProfileId);
|
||||
|
||||
const hasCredentialProfile = !!mcp.selectedProfileId && !!selectedProfile;
|
||||
const needsConfiguration = !hasCredentialProfile;
|
||||
|
||||
return (
|
||||
<Card className="p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="font-medium text-sm truncate">{mcp.name}</div>
|
||||
{mcp.isCustom && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Custom
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span>{mcp.enabledTools?.length || 0} tools enabled</span>
|
||||
{hasCredentialProfile && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Key className="h-3 w-3 text-green-600" />
|
||||
<span className="text-green-600 font-medium truncate max-w-24">
|
||||
{selectedProfile.profile_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{needsConfiguration && !mcp.isCustom && (
|
||||
<div className="flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3 text-amber-600" />
|
||||
<span className="text-amber-600">Needs config</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onEdit(index)}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onRemove(index)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfiguredMcpList: React.FC<ConfiguredMcpListProps> = ({
|
||||
configuredMCPs,
|
||||
onEdit,
|
||||
|
@ -21,37 +90,13 @@ export const ConfiguredMcpList: React.FC<ConfiguredMcpListProps> = ({
|
|||
return (
|
||||
<div className="space-y-2">
|
||||
{configuredMCPs.map((mcp, index) => (
|
||||
<Card key={index} className="p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{mcp.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{mcp.enabledTools?.length || 0} tools enabled
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onEdit(index)}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onRemove(index)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<MCPConfigurationItem
|
||||
key={index}
|
||||
mcp={mcp}
|
||||
index={index}
|
||||
onEdit={onEdit}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Checkbox } from '@/components/ui/checkbox';
|
|||
import { cn } from '@/lib/utils';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { CredentialProfile } from '@/hooks/react-query/mcp/use-credential-profiles';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||
|
||||
|
@ -25,6 +26,7 @@ interface CustomMCPConfiguration {
|
|||
type: 'http' | 'sse';
|
||||
config: any;
|
||||
enabledTools: string[];
|
||||
selectedProfileId?: string;
|
||||
}
|
||||
|
||||
interface MCPTool {
|
||||
|
@ -119,6 +121,16 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleToolsNext = () => {
|
||||
if (selectedTools.size === 0) {
|
||||
setValidationError('Please select at least one tool to continue.');
|
||||
return;
|
||||
}
|
||||
setValidationError(null);
|
||||
// Custom MCPs don't need credentials, so save directly
|
||||
handleSave();
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (discoveredTools.length === 0 || selectedTools.size === 0) {
|
||||
setValidationError('Please select at least one tool to continue.');
|
||||
|
@ -137,7 +149,9 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
name: serverName,
|
||||
type: serverType,
|
||||
config: configToSave,
|
||||
enabledTools: Array.from(selectedTools)
|
||||
enabledTools: Array.from(selectedTools),
|
||||
// Custom MCPs don't need credential profiles since they're just URLs
|
||||
selectedProfileId: undefined
|
||||
});
|
||||
|
||||
setConfigText('');
|
||||
|
@ -146,6 +160,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
setSelectedTools(new Set());
|
||||
setServerName('');
|
||||
setProcessedConfig(null);
|
||||
|
||||
setValidationError(null);
|
||||
setStep('setup');
|
||||
onOpenChange(false);
|
||||
|
@ -165,7 +180,9 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setStep('setup');
|
||||
if (step === 'tools') {
|
||||
setStep('setup');
|
||||
}
|
||||
setValidationError(null);
|
||||
};
|
||||
|
||||
|
@ -176,6 +193,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
setSelectedTools(new Set());
|
||||
setServerName('');
|
||||
setProcessedConfig(null);
|
||||
|
||||
setValidationError(null);
|
||||
setStep('setup');
|
||||
};
|
||||
|
@ -325,7 +343,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
) : step === 'tools' ? (
|
||||
<div className="space-y-6 p-1 flex-1 flex flex-col">
|
||||
<Alert className="border-green-200 bg-green-50 text-green-800">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||
|
@ -409,7 +427,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-shrink-0 pt-4">
|
||||
|
@ -422,7 +440,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
onClick={handleToolsNext}
|
||||
disabled={selectedTools.size === 0}
|
||||
>
|
||||
Add Connection ({selectedTools.size} tools)
|
||||
|
|
|
@ -67,6 +67,7 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
qualifiedName: `custom_${customConfig.type}_${Date.now()}`,
|
||||
config: customConfig.config,
|
||||
enabledTools: customConfig.enabledTools,
|
||||
selectedProfileId: customConfig.selectedProfileId,
|
||||
isCustom: true,
|
||||
customType: customConfig.type as 'http' | 'sse'
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ export interface MCPConfiguration {
|
|||
qualifiedName: string;
|
||||
config: Record<string, any>;
|
||||
enabledTools?: string[];
|
||||
selectedProfileId?: string;
|
||||
isCustom?: boolean;
|
||||
customType?: 'http' | 'sse';
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus, FileText, Loader2 } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCreateAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { DEFAULT_AGENTPRESS_TOOLS } from '../_data/tools';
|
||||
import { generateRandomAvatar } from '../_utils/_avatar-generator';
|
||||
|
||||
interface ResultsInfoProps {
|
||||
isLoading: boolean;
|
||||
|
@ -22,6 +27,40 @@ export const ResultsInfo = ({
|
|||
currentPage,
|
||||
totalPages
|
||||
}: ResultsInfoProps) => {
|
||||
const router = useRouter();
|
||||
const createAgentMutation = useCreateAgent();
|
||||
|
||||
const handleCreateNewAgent = async () => {
|
||||
try {
|
||||
const { avatar, avatar_color } = generateRandomAvatar();
|
||||
|
||||
const defaultAgentData = {
|
||||
name: 'New Agent',
|
||||
description: 'A newly created agent',
|
||||
system_prompt: 'You are a helpful assistant. Provide clear, accurate, and helpful responses to user queries.',
|
||||
avatar,
|
||||
avatar_color,
|
||||
configured_mcps: [],
|
||||
agentpress_tools: Object.fromEntries(
|
||||
Object.entries(DEFAULT_AGENTPRESS_TOOLS).map(([key, value]) => [
|
||||
key,
|
||||
{ enabled: value.enabled, description: value.description }
|
||||
])
|
||||
),
|
||||
is_default: false,
|
||||
};
|
||||
|
||||
const newAgent = await createAgentMutation.mutateAsync(defaultAgentData);
|
||||
router.push(`/agents/new/${newAgent.agent_id}`);
|
||||
} catch (error) {
|
||||
console.error('Error creating agent:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMyTemplates = () => {
|
||||
router.push('/marketplace/my-templates');
|
||||
};
|
||||
|
||||
if (isLoading || totalAgents === 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -39,11 +78,31 @@ export const ResultsInfo = ({
|
|||
{showingText()}
|
||||
{searchQuery && ` for "${searchQuery}"`}
|
||||
</span>
|
||||
{activeFiltersCount > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={clearFilters} className="h-auto p-0">
|
||||
Clear all filters
|
||||
<div className="flex items-center gap-2">
|
||||
{activeFiltersCount > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={clearFilters} className="h-auto p-0">
|
||||
Clear all filters
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={handleMyTemplates}>
|
||||
<FileText className="h-4 w-4" />
|
||||
My Templates
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleCreateNewAgent}
|
||||
disabled={createAgentMutation.isPending}
|
||||
>
|
||||
{createAgentMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4" />
|
||||
New Agent
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -271,70 +271,32 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
}
|
||||
}, [item, open]);
|
||||
|
||||
// Add a function to refresh requirements when profiles are created
|
||||
const refreshRequirements = React.useCallback(() => {
|
||||
if (item && open) {
|
||||
setIsCheckingRequirements(true);
|
||||
checkRequirementsAndSetupSteps();
|
||||
}
|
||||
}, [item, open]);
|
||||
|
||||
const checkRequirementsAndSetupSteps = async () => {
|
||||
if (!item?.mcp_requirements) return;
|
||||
|
||||
const steps: SetupStep[] = [];
|
||||
const missing: MissingProfile[] = [];
|
||||
const customServers = item.mcp_requirements.filter(req => req.custom_type);
|
||||
const regularServices = item.mcp_requirements.filter(req => !req.custom_type);
|
||||
|
||||
// Check for credential profiles for regular services and create profile selection steps
|
||||
for (const req of regularServices) {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (!session) {
|
||||
missing.push({
|
||||
qualified_name: req.qualified_name,
|
||||
display_name: req.display_name,
|
||||
required_config: req.required_config
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'}/secure-mcp/credential-profiles/${encodeURIComponent(req.qualified_name)}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session.access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const profiles = await response.json();
|
||||
if (!profiles || profiles.length === 0) {
|
||||
missing.push({
|
||||
qualified_name: req.qualified_name,
|
||||
display_name: req.display_name,
|
||||
required_config: req.required_config
|
||||
});
|
||||
} else {
|
||||
steps.push({
|
||||
id: req.qualified_name,
|
||||
title: `Select Credential Profile for ${req.display_name}`,
|
||||
description: `Choose which credential profile to use for ${req.display_name}.`,
|
||||
type: 'credential_profile',
|
||||
service_name: req.display_name,
|
||||
qualified_name: req.qualified_name,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
missing.push({
|
||||
qualified_name: req.qualified_name,
|
||||
display_name: req.display_name,
|
||||
required_config: req.required_config
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
missing.push({
|
||||
qualified_name: req.qualified_name,
|
||||
display_name: req.display_name,
|
||||
required_config: req.required_config
|
||||
});
|
||||
}
|
||||
steps.push({
|
||||
id: req.qualified_name,
|
||||
title: `Select Credential Profile for ${req.display_name}`,
|
||||
description: `Choose or create a credential profile for ${req.display_name}.`,
|
||||
type: 'credential_profile',
|
||||
service_name: req.display_name,
|
||||
qualified_name: req.qualified_name,
|
||||
});
|
||||
}
|
||||
|
||||
// Add custom server configuration steps (just ask for URL, no credentials needed)
|
||||
for (const req of customServers) {
|
||||
steps.push({
|
||||
id: req.qualified_name,
|
||||
|
@ -355,7 +317,7 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
}
|
||||
|
||||
setSetupSteps(steps);
|
||||
setMissingProfiles(missing);
|
||||
setMissingProfiles([]); // Clear missing profiles since we handle them in setup steps
|
||||
setIsCheckingRequirements(false);
|
||||
};
|
||||
|
||||
|
@ -425,7 +387,6 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
const canInstall = () => {
|
||||
if (!item) return false;
|
||||
if (!instanceName.trim()) return false;
|
||||
if (missingProfiles.length > 0) return false;
|
||||
|
||||
// Check if all required profile mappings are selected
|
||||
const regularRequirements = item.mcp_requirements?.filter(req => !req.custom_type) || [];
|
||||
|
@ -458,41 +419,6 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">Checking requirements...</p>
|
||||
</div>
|
||||
) : missingProfiles.length > 0 ? (
|
||||
<div className="space-y-6">
|
||||
<Alert className="border-destructive/50 bg-destructive/10 dark:bg-destructive/5 text-destructive">
|
||||
<AlertTriangle className="h-4 w-4 text-destructive" />
|
||||
<AlertTitle className="text-destructive">Missing Credential Profiles</AlertTitle>
|
||||
<AlertDescription className="text-destructive/80">
|
||||
This agent requires profiles for the following services:
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-3">
|
||||
{missingProfiles.map((profile) => (
|
||||
<Card key={profile.qualified_name} className="py-0 border-destructive/20 shadow-none bg-transparent">
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-destructive/10">
|
||||
<AlertTriangle className="h-4 w-4 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">{profile.display_name}</span>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{profile.required_config.map((config) => (
|
||||
<Badge key={config} variant="outline" className="text-xs">
|
||||
{config}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="destructive">Missing</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : setupSteps.length === 0 ? (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
|
@ -527,9 +453,9 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{/* <p className="text-sm text-muted-foreground">
|
||||
{currentStepData.description}
|
||||
</p>
|
||||
</p> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -541,6 +467,9 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
selectedProfileId={profileMappings[currentStepData.qualified_name]}
|
||||
onProfileSelect={(profileId, profile) => {
|
||||
handleProfileSelect(currentStepData.qualified_name, profileId);
|
||||
if (profile && !profileMappings[currentStepData.qualified_name]) {
|
||||
refreshRequirements();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
@ -607,73 +536,56 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
</div>
|
||||
|
||||
<DialogFooter className="flex-col gap-3">
|
||||
{missingProfiles.length > 0 ? (
|
||||
<div className="flex gap-3 w-full justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
<div className="flex gap-3 w-full justify-end">
|
||||
{currentStep > 0 && (
|
||||
<Button variant="outline" onClick={handleBack} className="flex-1">
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{setupSteps.length === 0 ? (
|
||||
<Button
|
||||
onClick={() => window.open('/settings/credentials', '_blank')}
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || !canInstall()}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
Set Up Credential Profiles
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Install Agent
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-3 w-full justify-end">
|
||||
{currentStep > 0 && (
|
||||
<Button variant="outline" onClick={handleBack} className="flex-1">
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{setupSteps.length === 0 ? (
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || !canInstall()}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Install Agent
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
) : currentStep < setupSteps.length ? (
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!isCurrentStepComplete()}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || !canInstall()}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Install Agent
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : currentStep < setupSteps.length ? (
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!isCurrentStepComplete()}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || !canInstall()}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Install Agent
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -1099,32 +1011,26 @@ export default function MarketplacePage() {
|
|||
<User className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-foreground">Agents from Community</h2>
|
||||
<p className="text-sm text-muted-foreground">Templates created by the community</p>
|
||||
<h2 className="text-lg font-semibold text-foreground">Agents from Community</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{communityItems.map((item) => {
|
||||
const { avatar, color } = getItemStyling(item);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group flex flex-col h-full"
|
||||
onClick={() => handleItemClick(item)}
|
||||
>
|
||||
<div className={`h-50 flex items-center justify-center relative`} style={{ backgroundColor: color }}>
|
||||
<div className="text-4xl">
|
||||
{avatar}
|
||||
</div>
|
||||
<div className="absolute top-3 right-3 flex gap-2">
|
||||
<div className="flex items-center gap-1 bg-white/20 backdrop-blur-sm px-2 py-1 rounded-full">
|
||||
<Download className="h-3 w-3 text-white" />
|
||||
<span className="text-white text-xs font-medium">{item.download_count}</span>
|
||||
<div className='p-4'>
|
||||
<div className={`h-12 w-12 flex items-center justify-center rounded-lg`} style={{ backgroundColor: color }}>
|
||||
<div className="text-2xl">
|
||||
{avatar}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex flex-col flex-1">
|
||||
<div className="p-4 -mt-4 flex flex-col flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
|
||||
{item.name}
|
||||
|
@ -1153,19 +1059,24 @@ export default function MarketplacePage() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1 mb-4">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<User className="h-3 w-3" />
|
||||
<span>By {item.creator_name}</span>
|
||||
</div>
|
||||
{item.marketplace_published_at && (
|
||||
<div className="mb-4 w-full flex justify-between">
|
||||
<div className='space-y-1'>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>{new Date(item.marketplace_published_at).toLocaleDateString()}</span>
|
||||
<User className="h-3 w-3" />
|
||||
<span>By {item.creator_name}</span>
|
||||
</div>
|
||||
)}
|
||||
{item.marketplace_published_at && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>{new Date(item.marketplace_published_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-muted-foreground text-xs font-medium">{item.download_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={(e) => handleInstallClick(item, e)}
|
||||
disabled={installingItemId === item.id}
|
||||
|
@ -1180,7 +1091,7 @@ export default function MarketplacePage() {
|
|||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Install Template
|
||||
Add to Library
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
|
@ -636,7 +636,7 @@ export const EnhancedAddCredentialDialog: React.FC<EnhancedAddCredentialDialogPr
|
|||
{step === 'browse' ? 'Cancel' :
|
||||
createProfileMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : 'Create Profile'}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Loader2, Server, RefreshCw, AlertCircle } from 'lucide-react';
|
||||
import { Loader2, Server, RefreshCw, AlertCircle, Clock, Wrench } from 'lucide-react';
|
||||
import { useApiHealth } from '@/hooks/react-query';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { isLocalMode } from '@/lib/config';
|
||||
|
||||
export function MaintenancePage() {
|
||||
|
@ -31,58 +33,85 @@ export function MaintenancePage() {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen p-4 bg-background">
|
||||
<div className="w-full max-w-md space-y-8 text-center">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<div className="relative">
|
||||
<Server className="h-16 w-16 text-primary animate-pulse" />
|
||||
<div className="absolute inset-0 flex items-center justify-center"></div>
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-2xl">
|
||||
<Card className="border-none shadow-none backdrop-blur-sm bg-transparent">
|
||||
<CardContent className="p-8">
|
||||
<div className="text-center space-y-6">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="relative">
|
||||
<div className="relative p-4 rounded-full border-2 bg-primary/10">
|
||||
<Wrench className="h-10 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Badge variant="outline" className="px-3 py-1 bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-700 text-blue-800 dark:text-blue-200 font-medium">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
System Under Maintenance
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
|
||||
We'll Be Right Back
|
||||
</h1>
|
||||
<p className="text-base text-muted-foreground max-w-lg mx-auto leading-relaxed">
|
||||
{isLocalMode() ? (
|
||||
"The backend server appears to be offline. Please ensure your backend server is running and try again."
|
||||
) : (
|
||||
"We're performing scheduled maintenance to improve your experience. Our team is working diligently to restore all services."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 mt-6 md:px-4">
|
||||
<Card className="bg-muted-foreground/10 border-none shadow-none">
|
||||
<CardContent className="text-center">
|
||||
<div className="flex items-center justify-center mb-1">
|
||||
<div className="h-3 w-3 bg-red-500 dark:bg-red-400 rounded-full mr-2 animate-pulse"></div>
|
||||
<span className="font-medium text-red-700 dark:text-red-300">Services Offline</span>
|
||||
</div>
|
||||
<p className="text-sm text-red-600 dark:text-red-400">All agent executions are currently paused.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="space-y-4 pt-4">
|
||||
<Button
|
||||
onClick={checkHealth}
|
||||
disabled={isCheckingHealth}
|
||||
size="lg"
|
||||
className="w-full md:w-auto px-6 py-2 bg-primary hover:bg-primary/90 text-primary-foreground font-medium transition-all duration-200"
|
||||
>
|
||||
{isCheckingHealth ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Checking Status...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Check System Status
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{lastChecked && (
|
||||
<div className="flex items-center justify-center text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4 mr-2" />
|
||||
Last checked: {lastChecked.toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-4 border-t border-border/50">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isLocalMode() ? (
|
||||
"Need help? Check the documentation for setup instructions."
|
||||
) : (
|
||||
"For urgent matters, please contact our support team."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
System Maintenance
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
{isLocalMode() ? (
|
||||
"The backend server appears to be offline. Please check that your backend server is running."
|
||||
) : (
|
||||
"We're currently performing maintenance on our systems. Our team is working to get everything back up and running as soon as possible."
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Alert className="mt-6">
|
||||
<AlertTitle>Agent Executions Stopped</AlertTitle>
|
||||
<AlertDescription>
|
||||
{isLocalMode() ? (
|
||||
"The backend server needs to be running for agent executions to work. Please start the backend server and try again."
|
||||
) : (
|
||||
"Any running agent executions have been stopped during maintenance. You'll need to manually continue these executions once the system is back online."
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
onClick={checkHealth}
|
||||
disabled={isCheckingHealth}
|
||||
className="w-full"
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn('h-4 w-4', isCheckingHealth && 'animate-spin')}
|
||||
/>
|
||||
{isCheckingHealth ? 'Checking...' : 'Check Again'}
|
||||
</Button>
|
||||
|
||||
{lastChecked && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Last checked: {lastChecked.toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
getUserFriendlyToolName,
|
||||
safeJsonParse,
|
||||
} from '@/components/thread/utils';
|
||||
import { formatMCPToolDisplayName } from '@/components/thread/tool-views/mcp-tool/_utils';
|
||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||
import { AgentLoader } from './loader';
|
||||
import { parseXmlToolCalls, isNewXmlFormat, extractToolNameFromStream } from '@/components/thread/tool-views/xml-parser';
|
||||
|
@ -54,6 +55,22 @@ const HIDE_STREAMING_XML_TAGS = new Set([
|
|||
'execute-data-provider-endpoint',
|
||||
]);
|
||||
|
||||
function getEnhancedToolDisplayName(toolName: string, rawXml?: string): string {
|
||||
if (toolName === 'call-mcp-tool' && rawXml) {
|
||||
const toolNameMatch = rawXml.match(/tool_name="([^"]+)"/);
|
||||
if (toolNameMatch) {
|
||||
const fullToolName = toolNameMatch[1];
|
||||
const parts = fullToolName.split('_');
|
||||
if (parts.length >= 3 && fullToolName.startsWith('mcp_')) {
|
||||
const serverName = parts[1];
|
||||
const toolNamePart = parts.slice(2).join('_');
|
||||
return formatMCPToolDisplayName(serverName, toolNamePart);
|
||||
}
|
||||
}
|
||||
}
|
||||
return getUserFriendlyToolName(toolName);
|
||||
}
|
||||
|
||||
// Helper function to render attachments (keeping original implementation for now)
|
||||
export function renderAttachments(attachments: string[], fileViewerHandler?: (filePath?: string, filePathList?: string[]) => void, sandboxId?: string, project?: Project) {
|
||||
if (!attachments || attachments.length === 0) return null;
|
||||
|
@ -772,7 +789,10 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
<CircleDashed className="h-3.5 w-3.5 text-primary flex-shrink-0 animate-spin animation-duration-2000" />
|
||||
</div>
|
||||
<span className="font-mono text-xs text-primary">
|
||||
{extractToolNameFromStream(streamingTextContent) || 'Using Tool...'}
|
||||
{(() => {
|
||||
const extractedToolName = extractToolNameFromStream(streamingTextContent);
|
||||
return extractedToolName ? getUserFriendlyToolName(extractedToolName) : 'Using Tool...';
|
||||
})()}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -857,7 +877,13 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
>
|
||||
<CircleDashed className="h-3.5 w-3.5 text-primary flex-shrink-0 animate-spin animation-duration-2000" />
|
||||
<span className="font-mono text-xs text-primary">
|
||||
{detectedTag === 'function_calls' ? (extractToolNameFromStream(streamingText) || 'Using Tool...') : detectedTag}
|
||||
{detectedTag === 'function_calls' ?
|
||||
(() => {
|
||||
const extractedToolName = extractToolNameFromStream(streamingText);
|
||||
return extractedToolName ? getUserFriendlyToolName(extractedToolName) : 'Using Tool...';
|
||||
})() :
|
||||
getUserFriendlyToolName(detectedTag)
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
parseMCPToolCall,
|
||||
getMCPServerIcon,
|
||||
getMCPServerColor,
|
||||
formatMCPToolDisplayName,
|
||||
MCPResult,
|
||||
ParsedMCPTool
|
||||
} from './_utils';
|
||||
|
@ -52,7 +53,7 @@ export function McpToolView({
|
|||
const argumentsCount = result?.mcp_metadata?.arguments_count ?? Object.keys(parsedTool.arguments).length;
|
||||
|
||||
const displayName = result?.mcp_metadata ?
|
||||
`${serverName.charAt(0).toUpperCase() + serverName.slice(1)}: ${toolName.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}` :
|
||||
formatMCPToolDisplayName(serverName, toolName) :
|
||||
parsedTool.displayName;
|
||||
|
||||
const ServerIcon = getMCPServerIcon(serverName);
|
||||
|
|
|
@ -24,6 +24,84 @@ export interface ParsedMCPTool {
|
|||
arguments: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced MCP tool name formatter that handles various naming conventions
|
||||
*/
|
||||
export function formatMCPToolDisplayName(serverName: string, toolName: string): string {
|
||||
// Handle server name formatting
|
||||
const formattedServerName = formatServerName(serverName);
|
||||
|
||||
// Handle tool name formatting
|
||||
const formattedToolName = formatToolName(toolName);
|
||||
|
||||
return `${formattedServerName}: ${formattedToolName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format server names with special handling for known services
|
||||
*/
|
||||
function formatServerName(serverName: string): string {
|
||||
const serverMappings: Record<string, string> = {
|
||||
'exa': 'Exa Search',
|
||||
'github': 'GitHub',
|
||||
'notion': 'Notion',
|
||||
'slack': 'Slack',
|
||||
'filesystem': 'File System',
|
||||
'memory': 'Memory',
|
||||
'anthropic': 'Anthropic',
|
||||
'openai': 'OpenAI',
|
||||
'composio': 'Composio',
|
||||
'langchain': 'LangChain',
|
||||
'llamaindex': 'LlamaIndex'
|
||||
};
|
||||
|
||||
const lowerName = serverName.toLowerCase();
|
||||
return serverMappings[lowerName] || capitalizeWords(serverName);
|
||||
}
|
||||
|
||||
function formatToolName(toolName: string): string {
|
||||
if (toolName.includes('-')) {
|
||||
return toolName
|
||||
.split('-')
|
||||
.map(word => capitalizeWord(word))
|
||||
.join(' ');
|
||||
}
|
||||
if (toolName.includes('_')) {
|
||||
return toolName
|
||||
.split('_')
|
||||
.map(word => capitalizeWord(word))
|
||||
.join(' ');
|
||||
}
|
||||
if (/[a-z][A-Z]/.test(toolName)) {
|
||||
return toolName
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.split(' ')
|
||||
.map(word => capitalizeWord(word))
|
||||
.join(' ');
|
||||
}
|
||||
return capitalizeWord(toolName);
|
||||
}
|
||||
|
||||
function capitalizeWord(word: string): string {
|
||||
if (!word) return '';
|
||||
const upperCaseWords = new Set([
|
||||
'api', 'url', 'http', 'https', 'json', 'xml', 'csv', 'pdf', 'id', 'uuid',
|
||||
'oauth', 'jwt', 'sql', 'html', 'css', 'js', 'ts', 'ai', 'ml', 'ui', 'ux'
|
||||
]);
|
||||
const lowerWord = word.toLowerCase();
|
||||
if (upperCaseWords.has(lowerWord)) {
|
||||
return word.toUpperCase();
|
||||
}
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
function capitalizeWords(text: string): string {
|
||||
return text
|
||||
.split(/[\s_-]+/)
|
||||
.map(word => capitalizeWord(word))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function extractFromNewFormat(toolContent: string | object): MCPResult | null {
|
||||
try {
|
||||
let parsed: any;
|
||||
|
@ -220,16 +298,14 @@ export function parseMCPToolCall(assistantContent: string): ParsedMCPTool {
|
|||
const serverName = parts.length > 1 ? parts[1] : 'unknown';
|
||||
const toolName = parts.length > 2 ? parts.slice(2).join('_') : fullToolName;
|
||||
|
||||
const serverDisplayName = serverName.charAt(0).toUpperCase() + serverName.slice(1);
|
||||
const toolDisplayName = toolName.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ');
|
||||
// Use the enhanced formatting function
|
||||
const displayName = formatMCPToolDisplayName(serverName, toolName);
|
||||
|
||||
return {
|
||||
serverName,
|
||||
toolName,
|
||||
fullToolName,
|
||||
displayName: `${serverDisplayName}: ${toolDisplayName}`,
|
||||
displayName,
|
||||
arguments: args
|
||||
};
|
||||
} catch (e) {
|
||||
|
|
|
@ -114,6 +114,32 @@ export function extractToolNameFromStream(content: string): string | null {
|
|||
}
|
||||
|
||||
export function formatToolNameForDisplay(toolName: string): string {
|
||||
if (toolName.startsWith('mcp_')) {
|
||||
const parts = toolName.split('_');
|
||||
if (parts.length >= 3) {
|
||||
const serverName = parts[1];
|
||||
const toolNamePart = parts.slice(2).join('_');
|
||||
const formattedServerName = serverName.charAt(0).toUpperCase() + serverName.slice(1);
|
||||
|
||||
let formattedToolName = toolNamePart;
|
||||
if (toolNamePart.includes('-')) {
|
||||
formattedToolName = toolNamePart
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
} else if (toolNamePart.includes('_')) {
|
||||
formattedToolName = toolNamePart
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
} else {
|
||||
formattedToolName = toolNamePart.charAt(0).toUpperCase() + toolNamePart.slice(1);
|
||||
}
|
||||
|
||||
return `${formattedServerName}: ${formattedToolName}`;
|
||||
}
|
||||
}
|
||||
|
||||
return toolName
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
|
|
|
@ -399,37 +399,68 @@ const MCP_SERVER_NAMES = new Map([
|
|||
['memory', 'Memory'],
|
||||
]);
|
||||
|
||||
const MCP_TOOL_MAPPINGS = new Map([
|
||||
['web_search_exa', 'Web Search'],
|
||||
['research_paper_search', 'Research Papers'],
|
||||
['search', 'Search'],
|
||||
['find_content', 'Find Content'],
|
||||
['get_content', 'Get Content'],
|
||||
['read_file', 'Read File'],
|
||||
['write_file', 'Write File'],
|
||||
['list_files', 'List Files'],
|
||||
]);
|
||||
function formatMCPToolName(serverName: string, toolName: string): string {
|
||||
const serverMappings: Record<string, string> = {
|
||||
'exa': 'Exa Search',
|
||||
'github': 'GitHub',
|
||||
'notion': 'Notion',
|
||||
'slack': 'Slack',
|
||||
'filesystem': 'File System',
|
||||
'memory': 'Memory',
|
||||
'anthropic': 'Anthropic',
|
||||
'openai': 'OpenAI',
|
||||
'composio': 'Composio',
|
||||
'langchain': 'LangChain',
|
||||
'llamaindex': 'LlamaIndex'
|
||||
};
|
||||
|
||||
const formattedServerName = serverMappings[serverName.toLowerCase()] ||
|
||||
serverName.charAt(0).toUpperCase() + serverName.slice(1);
|
||||
|
||||
let formattedToolName = toolName;
|
||||
|
||||
if (toolName.includes('-')) {
|
||||
formattedToolName = toolName
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
else if (toolName.includes('_')) {
|
||||
formattedToolName = toolName
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
else if (/[a-z][A-Z]/.test(toolName)) {
|
||||
formattedToolName = toolName
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
else {
|
||||
formattedToolName = toolName.charAt(0).toUpperCase() + toolName.slice(1);
|
||||
}
|
||||
|
||||
return `${formattedServerName}: ${formattedToolName}`;
|
||||
}
|
||||
|
||||
export function getUserFriendlyToolName(toolName: string): string {
|
||||
if (toolName?.startsWith('mcp_')) {
|
||||
if (toolName.startsWith('mcp_')) {
|
||||
const parts = toolName.split('_');
|
||||
if (parts.length >= 3) {
|
||||
const serverName = parts[1];
|
||||
const toolNamePart = parts.slice(2).join('_');
|
||||
|
||||
// Get friendly server name
|
||||
const friendlyServerName = MCP_SERVER_NAMES.get(serverName) ||
|
||||
serverName.charAt(0).toUpperCase() + serverName.slice(1);
|
||||
|
||||
// Get friendly tool name
|
||||
const friendlyToolName = MCP_TOOL_MAPPINGS.get(toolNamePart) ||
|
||||
toolNamePart.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ');
|
||||
|
||||
return `${friendlyServerName}: ${friendlyToolName}`;
|
||||
return formatMCPToolName(serverName, toolNamePart);
|
||||
}
|
||||
}
|
||||
if (toolName.includes('-') && !TOOL_DISPLAY_NAMES.has(toolName)) {
|
||||
const parts = toolName.split('-');
|
||||
if (parts.length >= 2) {
|
||||
const serverName = parts[0];
|
||||
const toolNamePart = parts.slice(1).join('-');
|
||||
return formatMCPToolName(serverName, toolNamePart);
|
||||
}
|
||||
}
|
||||
|
||||
return TOOL_DISPLAY_NAMES.get(toolName) || toolName;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
|
@ -10,21 +10,32 @@ import {
|
|||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import {
|
||||
Plus,
|
||||
Settings,
|
||||
Star,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertCircle
|
||||
AlertCircle,
|
||||
Key,
|
||||
Shield,
|
||||
Loader2,
|
||||
ExternalLink
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
useCredentialProfilesForMcp,
|
||||
useSetDefaultProfile,
|
||||
type CredentialProfile
|
||||
useCreateCredentialProfile,
|
||||
type CredentialProfile,
|
||||
type CreateCredentialProfileRequest
|
||||
} from '@/hooks/react-query/mcp/use-credential-profiles';
|
||||
import { useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CredentialProfileSelectorProps {
|
||||
mcpQualifiedName: string;
|
||||
|
@ -34,6 +45,213 @@ interface CredentialProfileSelectorProps {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface InlineCreateProfileDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
mcpQualifiedName: string;
|
||||
mcpDisplayName: string;
|
||||
onSuccess: (profile: CredentialProfile) => void;
|
||||
}
|
||||
|
||||
const InlineCreateProfileDialog: React.FC<InlineCreateProfileDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
mcpQualifiedName,
|
||||
mcpDisplayName,
|
||||
onSuccess
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<{
|
||||
profile_name: string;
|
||||
display_name: string;
|
||||
config: Record<string, string>;
|
||||
is_default: boolean;
|
||||
}>({
|
||||
profile_name: `${mcpDisplayName} Profile`,
|
||||
display_name: mcpDisplayName,
|
||||
config: {},
|
||||
is_default: false
|
||||
});
|
||||
|
||||
const { data: serverDetails, isLoading: isLoadingDetails } = useMCPServerDetails(mcpQualifiedName);
|
||||
const createProfileMutation = useCreateCredentialProfile();
|
||||
|
||||
const getConfigProperties = () => {
|
||||
const schema = serverDetails?.connections?.[0]?.configSchema;
|
||||
return schema?.properties || {};
|
||||
};
|
||||
|
||||
const getRequiredFields = () => {
|
||||
const schema = serverDetails?.connections?.[0]?.configSchema;
|
||||
return schema?.required || [];
|
||||
};
|
||||
|
||||
const isFieldRequired = (fieldName: string) => {
|
||||
return getRequiredFields().includes(fieldName);
|
||||
};
|
||||
|
||||
const handleConfigChange = (key: string, value: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
[key]: value
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const request: CreateCredentialProfileRequest = {
|
||||
mcp_qualified_name: mcpQualifiedName,
|
||||
profile_name: formData.profile_name,
|
||||
display_name: formData.display_name,
|
||||
config: formData.config,
|
||||
is_default: formData.is_default
|
||||
};
|
||||
|
||||
const response = await createProfileMutation.mutateAsync(request);
|
||||
toast.success('Credential profile created successfully!');
|
||||
|
||||
// Create a profile object to return
|
||||
const newProfile: CredentialProfile = {
|
||||
profile_id: response.profile_id || 'new-profile',
|
||||
mcp_qualified_name: mcpQualifiedName,
|
||||
profile_name: formData.profile_name,
|
||||
display_name: formData.display_name,
|
||||
config_keys: Object.keys(formData.config),
|
||||
is_active: true,
|
||||
is_default: formData.is_default,
|
||||
last_used_at: null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
onSuccess(newProfile);
|
||||
onOpenChange(false);
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
profile_name: `${mcpDisplayName} Profile`,
|
||||
display_name: mcpDisplayName,
|
||||
config: {},
|
||||
is_default: false
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to create credential profile');
|
||||
}
|
||||
};
|
||||
|
||||
const configProperties = getConfigProperties();
|
||||
const hasConfigFields = Object.keys(configProperties).length > 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Key className="h-5 w-5 text-primary" />
|
||||
Create Credential Profile
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new credential profile for <strong>{mcpDisplayName}</strong>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoadingDetails ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
<span>Loading server configuration...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6 flex-1 overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profile_name">Profile Name *</Label>
|
||||
<Input
|
||||
id="profile_name"
|
||||
value={formData.profile_name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, profile_name: e.target.value }))}
|
||||
placeholder="Enter a name for this profile"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This helps you identify different configurations for the same MCP server
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasConfigFields ? (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
Connection Settings
|
||||
</h3>
|
||||
{Object.entries(configProperties).map(([key, schema]: [string, any]) => (
|
||||
<div key={key} className="space-y-2">
|
||||
<Label htmlFor={key}>
|
||||
{schema.title || key}
|
||||
{isFieldRequired(key) && (
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id={key}
|
||||
type={schema.format === 'password' ? 'password' : 'text'}
|
||||
placeholder={schema.description || `Enter ${key}`}
|
||||
value={formData.config[key] || ''}
|
||||
onChange={(e) => handleConfigChange(key, e.target.value)}
|
||||
/>
|
||||
{schema.description && (
|
||||
<p className="text-xs text-muted-foreground">{schema.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Alert>
|
||||
<Key className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
This MCP server doesn't require any API credentials to use.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Alert>
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Your credentials will be encrypted and stored securely. You can create multiple profiles for the same MCP server to handle different use cases.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!formData.profile_name.trim() || createProfileMutation.isPending}
|
||||
>
|
||||
{createProfileMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Profile
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export function CredentialProfileSelector({
|
||||
mcpQualifiedName,
|
||||
mcpDisplayName,
|
||||
|
@ -41,10 +259,13 @@ export function CredentialProfileSelector({
|
|||
onProfileSelect,
|
||||
disabled = false
|
||||
}: CredentialProfileSelectorProps) {
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
|
||||
const {
|
||||
data: profiles = [],
|
||||
isLoading,
|
||||
error
|
||||
error,
|
||||
refetch
|
||||
} = useCredentialProfilesForMcp(mcpQualifiedName);
|
||||
|
||||
const setDefaultMutation = useSetDefaultProfile();
|
||||
|
@ -60,7 +281,15 @@ export function CredentialProfileSelector({
|
|||
};
|
||||
|
||||
const handleCreateNewProfile = () => {
|
||||
window.open('/settings/credentials', '_blank');
|
||||
setShowCreateDialog(true);
|
||||
};
|
||||
|
||||
const handleCreateSuccess = (newProfile: CredentialProfile) => {
|
||||
// Refetch profiles to get the updated list
|
||||
refetch();
|
||||
// Auto-select the newly created profile
|
||||
onProfileSelect(newProfile.profile_id, newProfile);
|
||||
toast.success(`Profile "${newProfile.profile_name}" created and selected!`);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -85,100 +314,110 @@ export function CredentialProfileSelector({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Credential Profile</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={handleCreateNewProfile}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
New Profile
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{profiles.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-6 text-center">
|
||||
<Settings className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
No credential profiles found for {mcpDisplayName}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateNewProfile}
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Credential Profile</Label>
|
||||
{/* <Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={handleCreateNewProfile}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
New Profile
|
||||
</Button> */}
|
||||
</div>
|
||||
|
||||
{profiles.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-6 text-center">
|
||||
<Settings className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
No credential profiles found for {mcpDisplayName}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateNewProfile}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Profile
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<Select
|
||||
value={selectedProfileId || ''}
|
||||
onValueChange={(value) => {
|
||||
const profile = profiles.find(p => p.profile_id === value);
|
||||
onProfileSelect(value || null, profile || null);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Create First Profile
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<Select
|
||||
value={selectedProfileId || ''}
|
||||
onValueChange={(value) => {
|
||||
const profile = profiles.find(p => p.profile_id === value);
|
||||
onProfileSelect(value || null, profile || null);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a credential profile..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles.map((profile) => (
|
||||
<SelectItem key={profile.profile_id} value={profile.profile_id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{profile.profile_name}</span>
|
||||
{profile.is_default && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Default
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selectedProfile && (
|
||||
<Card className="bg-muted/30 py-0">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a credential profile..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles.map((profile) => (
|
||||
<SelectItem key={profile.profile_id} value={profile.profile_id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium">{selectedProfile.profile_name}</h4>
|
||||
{selectedProfile.is_default && (
|
||||
<span>{profile.profile_name}</span>
|
||||
{profile.is_default && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Default
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedProfile.display_name}
|
||||
</p>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selectedProfile && (
|
||||
<Card className="bg-muted/30 py-0">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium">{selectedProfile.profile_name}</h4>
|
||||
{selectedProfile.is_default && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Default
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedProfile.display_name}
|
||||
</p>
|
||||
</div>
|
||||
{!selectedProfile.is_default && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleSetDefault(selectedProfile.profile_id)}
|
||||
disabled={setDefaultMutation.isPending}
|
||||
>
|
||||
<Star className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!selectedProfile.is_default && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleSetDefault(selectedProfile.profile_id)}
|
||||
disabled={setDefaultMutation.isPending}
|
||||
>
|
||||
<Star className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<InlineCreateProfileDialog
|
||||
open={showCreateDialog}
|
||||
onOpenChange={setShowCreateDialog}
|
||||
mcpQualifiedName={mcpQualifiedName}
|
||||
mcpDisplayName={mcpDisplayName}
|
||||
onSuccess={handleCreateSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue