Implement OAuth client and token management with settings updates (#464)

This commit is contained in:
samanhappy
2025-12-01 16:02:55 +08:00
committed by GitHub
parent b5dff990e5
commit 764959eaca
34 changed files with 2306 additions and 1051 deletions

View File

@@ -1,152 +1,174 @@
import { useState, useCallback, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Prompt } from '@/types'
import { ChevronDown, ChevronRight, Play, Loader, Edit, Check } from '@/components/icons/LucideIcons'
import { Switch } from './ToggleGroup'
import { getPrompt, PromptCallResult } from '@/services/promptService'
import { useSettingsData } from '@/hooks/useSettingsData'
import DynamicForm from './DynamicForm'
import PromptResult from './PromptResult'
import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Prompt } from '@/types';
import {
ChevronDown,
ChevronRight,
Play,
Loader,
Edit,
Check,
} from '@/components/icons/LucideIcons';
import { Switch } from './ToggleGroup';
import { getPrompt, updatePromptDescription, PromptCallResult } from '@/services/promptService';
import { useSettingsData } from '@/hooks/useSettingsData';
import DynamicForm from './DynamicForm';
import PromptResult from './PromptResult';
import { useToast } from '@/contexts/ToastContext';
interface PromptCardProps {
server: string
prompt: Prompt
onToggle?: (promptName: string, enabled: boolean) => void
onDescriptionUpdate?: (promptName: string, description: string) => void
server: string;
prompt: Prompt;
onToggle?: (promptName: string, enabled: boolean) => void;
onDescriptionUpdate?: (promptName: string, description: string) => void;
}
const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCardProps) => {
const { t } = useTranslation()
const { nameSeparator } = useSettingsData()
const [isExpanded, setIsExpanded] = useState(false)
const [showRunForm, setShowRunForm] = useState(false)
const [isRunning, setIsRunning] = useState(false)
const [result, setResult] = useState<PromptCallResult | null>(null)
const [isEditingDescription, setIsEditingDescription] = useState(false)
const [customDescription, setCustomDescription] = useState(prompt.description || '')
const descriptionInputRef = useRef<HTMLInputElement>(null)
const descriptionTextRef = useRef<HTMLSpanElement>(null)
const [textWidth, setTextWidth] = useState<number>(0)
const { t } = useTranslation();
const { showToast } = useToast();
const { nameSeparator } = useSettingsData();
const [isExpanded, setIsExpanded] = useState(false);
const [showRunForm, setShowRunForm] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const [result, setResult] = useState<PromptCallResult | null>(null);
const [isEditingDescription, setIsEditingDescription] = useState(false);
const [customDescription, setCustomDescription] = useState(prompt.description || '');
const descriptionInputRef = useRef<HTMLInputElement>(null);
const descriptionTextRef = useRef<HTMLSpanElement>(null);
const [textWidth, setTextWidth] = useState<number>(0);
// Focus the input when editing mode is activated
useEffect(() => {
if (isEditingDescription && descriptionInputRef.current) {
descriptionInputRef.current.focus()
descriptionInputRef.current.focus();
// Set input width to match text width
if (textWidth > 0) {
descriptionInputRef.current.style.width = `${textWidth + 20}px` // Add some padding
descriptionInputRef.current.style.width = `${textWidth + 20}px`; // Add some padding
}
}
}, [isEditingDescription, textWidth])
}, [isEditingDescription, textWidth]);
// Measure text width when not editing
useEffect(() => {
if (!isEditingDescription && descriptionTextRef.current) {
setTextWidth(descriptionTextRef.current.offsetWidth)
setTextWidth(descriptionTextRef.current.offsetWidth);
}
}, [isEditingDescription, customDescription])
}, [isEditingDescription, customDescription]);
// Generate a unique key for localStorage based on prompt name and server
const getStorageKey = useCallback(() => {
return `mcphub_prompt_form_${server ? `${server}_` : ''}${prompt.name}`
}, [prompt.name, server])
return `mcphub_prompt_form_${server ? `${server}_` : ''}${prompt.name}`;
}, [prompt.name, server]);
// Clear form data from localStorage
const clearStoredFormData = useCallback(() => {
localStorage.removeItem(getStorageKey())
}, [getStorageKey])
localStorage.removeItem(getStorageKey());
}, [getStorageKey]);
const handleToggle = (enabled: boolean) => {
if (onToggle) {
onToggle(prompt.name, enabled)
onToggle(prompt.name, enabled);
}
}
};
const handleDescriptionEdit = () => {
setIsEditingDescription(true)
}
setIsEditingDescription(true);
};
const handleDescriptionSave = async () => {
// For now, we'll just update the local state
// In a real implementation, you would call an API to update the description
setIsEditingDescription(false)
if (onDescriptionUpdate) {
onDescriptionUpdate(prompt.name, customDescription)
setIsEditingDescription(false);
try {
const result = await updatePromptDescription(server, prompt.name, customDescription);
if (result.success) {
showToast(t('prompt.descriptionUpdateSuccess'), 'success');
if (onDescriptionUpdate) {
onDescriptionUpdate(prompt.name, customDescription);
}
} else {
showToast(result.error || t('prompt.descriptionUpdateFailed'), 'error');
// Revert to original description on failure
setCustomDescription(prompt.description || '');
}
} catch (error) {
console.error('Error updating prompt description:', error);
showToast(t('prompt.descriptionUpdateFailed'), 'error');
// Revert to original description on failure
setCustomDescription(prompt.description || '');
}
}
};
const handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCustomDescription(e.target.value)
}
setCustomDescription(e.target.value);
};
const handleDescriptionKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleDescriptionSave()
handleDescriptionSave();
} else if (e.key === 'Escape') {
setCustomDescription(prompt.description || '')
setIsEditingDescription(false)
setCustomDescription(prompt.description || '');
setIsEditingDescription(false);
}
}
};
const handleGetPrompt = async (arguments_: Record<string, any>) => {
setIsRunning(true)
setIsRunning(true);
try {
const result = await getPrompt({ promptName: prompt.name, arguments: arguments_ }, server)
console.log('GetPrompt result:', result)
const result = await getPrompt({ promptName: prompt.name, arguments: arguments_ }, server);
console.log('GetPrompt result:', result);
setResult({
success: result.success,
data: result.data,
error: result.error
})
error: result.error,
});
// Clear form data on successful submission
// clearStoredFormData()
} catch (error) {
setResult({
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
})
});
} finally {
setIsRunning(false)
setIsRunning(false);
}
}
};
const handleCancelRun = () => {
setShowRunForm(false)
setShowRunForm(false);
// Clear form data when cancelled
clearStoredFormData()
setResult(null)
}
clearStoredFormData();
setResult(null);
};
const handleCloseResult = () => {
setResult(null)
}
setResult(null);
};
// Convert prompt arguments to ToolInputSchema format for DynamicForm
const convertToSchema = () => {
if (!prompt.arguments || prompt.arguments.length === 0) {
return { type: 'object', properties: {}, required: [] }
return { type: 'object', properties: {}, required: [] };
}
const properties: Record<string, any> = {}
const required: string[] = []
const properties: Record<string, any> = {};
const required: string[] = [];
prompt.arguments.forEach(arg => {
prompt.arguments.forEach((arg) => {
properties[arg.name] = {
type: 'string', // Default to string for prompts
description: arg.description || ''
}
description: arg.description || '',
};
if (arg.required) {
required.push(arg.name)
required.push(arg.name);
}
})
});
return {
type: 'object',
properties,
required
}
}
required,
};
};
return (
<div className="bg-white border border-gray-200 shadow rounded-lg p-4 mb-4">
@@ -158,9 +180,7 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar
<h3 className="text-lg font-medium text-gray-900">
{prompt.name.replace(server + nameSeparator, '')}
{prompt.title && (
<span className="ml-2 text-sm font-normal text-gray-600">
{prompt.title}
</span>
<span className="ml-2 text-sm font-normal text-gray-600">{prompt.title}</span>
)}
<span className="ml-2 text-sm font-normal text-gray-500 inline-flex items-center">
{isEditingDescription ? (
@@ -175,14 +195,14 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar
onClick={(e) => e.stopPropagation()}
style={{
minWidth: '100px',
width: textWidth > 0 ? `${textWidth + 20}px` : 'auto'
width: textWidth > 0 ? `${textWidth + 20}px` : 'auto',
}}
/>
<button
className="ml-2 p-1 text-green-600 hover:text-green-800 cursor-pointer transition-colors"
onClick={(e) => {
e.stopPropagation()
handleDescriptionSave()
e.stopPropagation();
handleDescriptionSave();
}}
>
<Check size={16} />
@@ -190,12 +210,14 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar
</>
) : (
<>
<span ref={descriptionTextRef}>{customDescription || t('tool.noDescription')}</span>
<span ref={descriptionTextRef}>
{customDescription || t('tool.noDescription')}
</span>
<button
className="ml-2 p-1 text-gray-500 hover:text-blue-600 cursor-pointer transition-colors"
onClick={(e) => {
e.stopPropagation()
handleDescriptionEdit()
e.stopPropagation();
handleDescriptionEdit();
}}
>
<Edit size={14} />
@@ -206,10 +228,7 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar
</h3>
</div>
<div className="flex items-center space-x-2">
<div
className="flex items-center space-x-2"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center space-x-2" onClick={(e) => e.stopPropagation()}>
{prompt.enabled !== undefined && (
<Switch
checked={prompt.enabled}
@@ -220,18 +239,14 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar
</div>
<button
onClick={(e) => {
e.stopPropagation()
setIsExpanded(true) // Ensure card is expanded when showing run form
setShowRunForm(true)
e.stopPropagation();
setIsExpanded(true); // Ensure card is expanded when showing run form
setShowRunForm(true);
}}
className="flex items-center space-x-1 px-3 py-1 text-sm text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors btn-primary"
disabled={isRunning || !prompt.enabled}
>
{isRunning ? (
<Loader size={14} className="animate-spin" />
) : (
<Play size={14} />
)}
{isRunning ? <Loader size={14} className="animate-spin" /> : <Play size={14} />}
<span>{isRunning ? t('tool.running') : t('tool.run')}</span>
</button>
<button className="text-gray-400 hover:text-gray-600">
@@ -251,7 +266,9 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar
onCancel={handleCancelRun}
loading={isRunning}
storageKey={getStorageKey()}
title={t('prompt.runPromptWithName', { name: prompt.name.replace(server + nameSeparator, '') })}
title={t('prompt.runPromptWithName', {
name: prompt.name.replace(server + nameSeparator, ''),
})}
/>
{/* Prompt Result */}
{result && (
@@ -278,9 +295,7 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar
<p className="text-sm text-gray-600 mt-1">{arg.description}</p>
)}
</div>
<div className="text-xs text-gray-500 ml-2">
{arg.title || ''}
</div>
<div className="text-xs text-gray-500 ml-2">{arg.title || ''}</div>
</div>
))}
</div>
@@ -296,7 +311,7 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar
</div>
)}
</div>
)
}
);
};
export default PromptCard
export default PromptCard;