mirror of
https://github.com/arc53/DocsGPT.git
synced 2026-03-04 12:54:54 +00:00
feat: enhance API tool with body serialization and content type handling (#2192)
* feat: enhance API tool with body serialization and content type handling * feat: enhance ToolConfig with import functionality and user action management - Added ImportSpecModal to allow importing actions into the tool configuration. - Implemented search functionality for user actions with expandable action details. - Introduced method colors for better visual distinction of HTTP methods. - Updated APIActionType and ParameterGroupType to include optional 'required' field. - Refactored action rendering to improve usability and maintainability. * feat: add base URL input to ImportSpecModal for action URL customization * feat: update TestBaseAgentTools to include 'required' field for parameters * feat: standardize API call timeout to DEFAULT_TIMEOUT constant * feat: add import specification functionality and related translations for multiple languages --------- Co-authored-by: Alex <a@tushynski.me>
This commit is contained in:
@@ -40,6 +40,7 @@ const endpoints = {
|
||||
UPDATE_TOOL_STATUS: '/api/update_tool_status',
|
||||
UPDATE_TOOL: '/api/update_tool',
|
||||
DELETE_TOOL: '/api/delete_tool',
|
||||
PARSE_SPEC: '/api/parse_spec',
|
||||
SYNC_CONNECTOR: '/api/connectors/sync',
|
||||
GET_CHUNKS: (
|
||||
docId: string,
|
||||
|
||||
@@ -84,6 +84,11 @@ const userService = {
|
||||
apiClient.post(endpoints.USER.UPDATE_TOOL, data, token),
|
||||
deleteTool: (data: any, token: string | null): Promise<any> =>
|
||||
apiClient.post(endpoints.USER.DELETE_TOOL, data, token),
|
||||
parseSpec: (file: File, token: string | null): Promise<any> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return apiClient.postFormData(endpoints.USER.PARSE_SPEC, formData, token);
|
||||
},
|
||||
getDocumentChunks: (
|
||||
docId: string,
|
||||
page: number,
|
||||
|
||||
@@ -162,12 +162,17 @@
|
||||
"authentication": "Authentifizierung",
|
||||
"actions": "Aktionen",
|
||||
"addAction": "Aktion hinzufügen",
|
||||
"importSpec": "Spezifikation importieren",
|
||||
"searchActions": "Aktionen suchen...",
|
||||
"noActionsMatch": "Keine Aktionen passen zu deiner Suche",
|
||||
"actionAlreadyExists": "Eine Aktion mit diesem Namen existiert bereits",
|
||||
"noActionsFound": "Keine Aktionen gefunden",
|
||||
"url": "URL",
|
||||
"urlPlaceholder": "URL eingeben",
|
||||
"method": "Methode",
|
||||
"description": "Beschreibung",
|
||||
"descriptionPlaceholder": "Beschreibung eingeben",
|
||||
"bodyContentType": "Body-Inhaltstyp",
|
||||
"headers": "Header",
|
||||
"queryParameters": "Abfrageparameter",
|
||||
"body": "Body",
|
||||
@@ -441,6 +446,22 @@
|
||||
"generate": "Generieren",
|
||||
"test": "Testen",
|
||||
"learnMore": "Mehr erfahren"
|
||||
},
|
||||
"importSpec": {
|
||||
"title": "API-Spezifikation importieren",
|
||||
"description": "Lade eine OpenAPI 3.x- oder Swagger 2.0-Spezifikationsdatei hoch, um automatisch Aktionen zu generieren.",
|
||||
"dropzoneText": "Zum Hochladen klicken oder per Drag & Drop",
|
||||
"supportedFormats": "JSON- oder YAML-Format",
|
||||
"invalidFileType": "Ungültiger Dateityp. Bitte eine JSON- oder YAML-Datei hochladen.",
|
||||
"parseError": "Spezifikation konnte nicht geparst werden. Bitte Dateiformat prüfen.",
|
||||
"version": "Version",
|
||||
"baseUrl": "Basis-URL",
|
||||
"actionsFound": "{{count}} Aktionen gefunden",
|
||||
"selectAll": "Alle auswählen",
|
||||
"deselectAll": "Alle abwählen",
|
||||
"cancel": "Abbrechen",
|
||||
"parse": "Parsen",
|
||||
"import": "Importieren ({{count}})"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -162,12 +162,17 @@
|
||||
"authentication": "Authentication",
|
||||
"actions": "Actions",
|
||||
"addAction": "Add action",
|
||||
"importSpec": "Import Spec",
|
||||
"searchActions": "Search actions...",
|
||||
"noActionsMatch": "No actions match your search",
|
||||
"actionAlreadyExists": "An action with this name already exists",
|
||||
"noActionsFound": "No actions found",
|
||||
"url": "URL",
|
||||
"urlPlaceholder": "Enter URL",
|
||||
"method": "Method",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Enter description",
|
||||
"bodyContentType": "Body Content Type",
|
||||
"headers": "Headers",
|
||||
"queryParameters": "Query Parameters",
|
||||
"body": "Body",
|
||||
@@ -441,6 +446,22 @@
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
},
|
||||
"importSpec": {
|
||||
"title": "Import API Specification",
|
||||
"description": "Upload an OpenAPI 3.x or Swagger 2.0 specification file to automatically generate actions.",
|
||||
"dropzoneText": "Click to upload or drag and drop",
|
||||
"supportedFormats": "JSON or YAML format",
|
||||
"invalidFileType": "Invalid file type. Please upload a JSON or YAML file.",
|
||||
"parseError": "Failed to parse the specification. Please check the file format.",
|
||||
"version": "Version",
|
||||
"baseUrl": "Base URL",
|
||||
"actionsFound": "{{count}} actions found",
|
||||
"selectAll": "Select all",
|
||||
"deselectAll": "Deselect all",
|
||||
"cancel": "Cancel",
|
||||
"parse": "Parse",
|
||||
"import": "Import ({{count}})"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -162,12 +162,17 @@
|
||||
"authentication": "Autenticación",
|
||||
"actions": "Acciones",
|
||||
"addAction": "Agregar acción",
|
||||
"importSpec": "Importar especificación",
|
||||
"searchActions": "Buscar acciones...",
|
||||
"noActionsMatch": "No hay acciones que coincidan con tu búsqueda",
|
||||
"actionAlreadyExists": "Ya existe una acción con este nombre",
|
||||
"noActionsFound": "No se encontraron acciones",
|
||||
"url": "URL",
|
||||
"urlPlaceholder": "Ingresa url",
|
||||
"method": "Método",
|
||||
"description": "Descripción",
|
||||
"descriptionPlaceholder": "Ingresa descripción",
|
||||
"bodyContentType": "Tipo de contenido del cuerpo",
|
||||
"headers": "Encabezados",
|
||||
"queryParameters": "Parámetros de Consulta",
|
||||
"body": "Cuerpo",
|
||||
@@ -441,6 +446,22 @@
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
},
|
||||
"importSpec": {
|
||||
"title": "Importar especificación de API",
|
||||
"description": "Sube un archivo de especificación OpenAPI 3.x o Swagger 2.0 para generar acciones automáticamente.",
|
||||
"dropzoneText": "Haz clic para subir o arrastra y suelta",
|
||||
"supportedFormats": "Formato JSON o YAML",
|
||||
"invalidFileType": "Tipo de archivo no válido. Sube un archivo JSON o YAML.",
|
||||
"parseError": "No se pudo analizar la especificación. Verifica el formato del archivo.",
|
||||
"version": "Versión",
|
||||
"baseUrl": "URL base",
|
||||
"actionsFound": "{{count}} acciones encontradas",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"deselectAll": "Deseleccionar todo",
|
||||
"cancel": "Cancelar",
|
||||
"parse": "Analizar",
|
||||
"import": "Importar ({{count}})"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -162,12 +162,17 @@
|
||||
"authentication": "認証",
|
||||
"actions": "アクション",
|
||||
"addAction": "アクションを追加",
|
||||
"importSpec": "仕様をインポート",
|
||||
"searchActions": "アクションを検索...",
|
||||
"noActionsMatch": "検索に一致するアクションがありません",
|
||||
"actionAlreadyExists": "この名前のアクションは既に存在します",
|
||||
"noActionsFound": "アクションが見つかりません",
|
||||
"url": "URL",
|
||||
"urlPlaceholder": "URLを入力",
|
||||
"method": "メソッド",
|
||||
"description": "説明",
|
||||
"descriptionPlaceholder": "説明を入力",
|
||||
"bodyContentType": "ボディのコンテンツタイプ",
|
||||
"headers": "ヘッダー",
|
||||
"queryParameters": "クエリパラメータ",
|
||||
"body": "ボディ",
|
||||
@@ -441,6 +446,22 @@
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
},
|
||||
"importSpec": {
|
||||
"title": "API仕様のインポート",
|
||||
"description": "OpenAPI 3.x または Swagger 2.0 の仕様ファイルをアップロードして、アクションを自動生成します。",
|
||||
"dropzoneText": "クリックしてアップロード、またはドラッグ&ドロップ",
|
||||
"supportedFormats": "JSON または YAML 形式",
|
||||
"invalidFileType": "無効なファイル形式です。JSON または YAML ファイルをアップロードしてください。",
|
||||
"parseError": "仕様の解析に失敗しました。ファイル形式を確認してください。",
|
||||
"version": "バージョン",
|
||||
"baseUrl": "ベースURL",
|
||||
"actionsFound": "{{count}} 件のアクションが見つかりました",
|
||||
"selectAll": "すべて選択",
|
||||
"deselectAll": "すべて解除",
|
||||
"cancel": "キャンセル",
|
||||
"parse": "解析",
|
||||
"import": "インポート ({{count}})"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -162,12 +162,17 @@
|
||||
"authentication": "Аутентификация",
|
||||
"actions": "Действия",
|
||||
"addAction": "Добавить действие",
|
||||
"importSpec": "Импорт спецификации",
|
||||
"searchActions": "Поиск действий...",
|
||||
"noActionsMatch": "Нет действий, соответствующих вашему поиску",
|
||||
"actionAlreadyExists": "Действие с таким именем уже существует",
|
||||
"noActionsFound": "Действия не найдены",
|
||||
"url": "URL",
|
||||
"urlPlaceholder": "Введите URL",
|
||||
"method": "Метод",
|
||||
"description": "Описание",
|
||||
"descriptionPlaceholder": "Введите описание",
|
||||
"bodyContentType": "Тип содержимого тела",
|
||||
"headers": "Заголовки",
|
||||
"queryParameters": "Параметры запроса",
|
||||
"body": "Тело запроса",
|
||||
@@ -441,6 +446,22 @@
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
},
|
||||
"importSpec": {
|
||||
"title": "Импорт спецификации API",
|
||||
"description": "Загрузите файл спецификации OpenAPI 3.x или Swagger 2.0 для автоматического создания действий.",
|
||||
"dropzoneText": "Нажмите для загрузки или перетащите файл",
|
||||
"supportedFormats": "Формат JSON или YAML",
|
||||
"invalidFileType": "Неверный тип файла. Пожалуйста, загрузите файл JSON или YAML.",
|
||||
"parseError": "Не удалось разобрать спецификацию. Проверьте формат файла.",
|
||||
"version": "Версия",
|
||||
"baseUrl": "Базовый URL",
|
||||
"actionsFound": "{{count}} действий найдено",
|
||||
"selectAll": "Выбрать все",
|
||||
"deselectAll": "Снять выделение со всех",
|
||||
"cancel": "Отмена",
|
||||
"parse": "Разобрать",
|
||||
"import": "Импорт ({{count}})"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -162,12 +162,17 @@
|
||||
"authentication": "認證",
|
||||
"actions": "操作",
|
||||
"addAction": "新增操作",
|
||||
"importSpec": "匯入規格",
|
||||
"searchActions": "搜尋操作...",
|
||||
"noActionsMatch": "沒有符合搜尋的操作",
|
||||
"actionAlreadyExists": "已存在同名操作",
|
||||
"noActionsFound": "找不到操作",
|
||||
"url": "URL",
|
||||
"urlPlaceholder": "輸入url",
|
||||
"method": "方法",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "輸入描述",
|
||||
"bodyContentType": "主體內容類型",
|
||||
"headers": "標頭",
|
||||
"queryParameters": "查詢參數",
|
||||
"body": "主體",
|
||||
@@ -441,6 +446,22 @@
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
},
|
||||
"importSpec": {
|
||||
"title": "匯入 API 規格",
|
||||
"description": "上傳 OpenAPI 3.x 或 Swagger 2.0 規格檔以自動產生操作。",
|
||||
"dropzoneText": "點擊上傳或拖放",
|
||||
"supportedFormats": "JSON 或 YAML 格式",
|
||||
"invalidFileType": "無效的檔案類型。請上傳 JSON 或 YAML 檔案。",
|
||||
"parseError": "解析規格失敗。請檢查檔案格式。",
|
||||
"version": "版本",
|
||||
"baseUrl": "基礎 URL",
|
||||
"actionsFound": "找到 {{count}} 個操作",
|
||||
"selectAll": "全選",
|
||||
"deselectAll": "取消全選",
|
||||
"cancel": "取消",
|
||||
"parse": "解析",
|
||||
"import": "匯入 ({{count}})"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -162,12 +162,17 @@
|
||||
"authentication": "认证",
|
||||
"actions": "操作",
|
||||
"addAction": "添加操作",
|
||||
"importSpec": "导入规范",
|
||||
"searchActions": "搜索操作...",
|
||||
"noActionsMatch": "没有与搜索匹配的操作",
|
||||
"actionAlreadyExists": "已存在同名操作",
|
||||
"noActionsFound": "未找到操作",
|
||||
"url": "URL",
|
||||
"urlPlaceholder": "输入url",
|
||||
"method": "方法",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "输入描述",
|
||||
"bodyContentType": "请求体内容类型",
|
||||
"headers": "请求头",
|
||||
"queryParameters": "查询参数",
|
||||
"body": "请求体",
|
||||
@@ -441,6 +446,22 @@
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
},
|
||||
"importSpec": {
|
||||
"title": "导入 API 规范",
|
||||
"description": "上传 OpenAPI 3.x 或 Swagger 2.0 规范文件以自动生成操作。",
|
||||
"dropzoneText": "点击上传或拖拽到此处",
|
||||
"supportedFormats": "JSON 或 YAML 格式",
|
||||
"invalidFileType": "文件类型无效。请上传 JSON 或 YAML 文件。",
|
||||
"parseError": "解析规范失败。请检查文件格式。",
|
||||
"version": "版本",
|
||||
"baseUrl": "基础 URL",
|
||||
"actionsFound": "找到 {{count}} 个操作",
|
||||
"selectAll": "全选",
|
||||
"deselectAll": "取消全选",
|
||||
"cancel": "取消",
|
||||
"parse": "解析",
|
||||
"import": "导入 ({{count}})"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
321
frontend/src/modals/ImportSpecModal.tsx
Normal file
321
frontend/src/modals/ImportSpecModal.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import Upload from '../assets/upload.svg';
|
||||
import Spinner from '../components/Spinner';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { APIActionType } from '../settings/types';
|
||||
import WrapperModal from './WrapperModal';
|
||||
|
||||
interface ImportSpecModalProps {
|
||||
modalState: ActiveState;
|
||||
setModalState: (state: ActiveState) => void;
|
||||
onImport: (actions: APIActionType[]) => void;
|
||||
}
|
||||
|
||||
interface ParsedResult {
|
||||
metadata: {
|
||||
title: string;
|
||||
description: string;
|
||||
version: string;
|
||||
base_url: string;
|
||||
};
|
||||
actions: APIActionType[];
|
||||
}
|
||||
|
||||
const METHOD_COLORS: Record<string, string> = {
|
||||
GET: 'bg-[#D1FAE5] text-[#065F46] dark:bg-[#064E3B]/60 dark:text-[#6EE7B7]',
|
||||
POST: 'bg-[#DBEAFE] text-[#1E40AF] dark:bg-[#1E3A8A]/60 dark:text-[#93C5FD]',
|
||||
PUT: 'bg-[#FEF3C7] text-[#92400E] dark:bg-[#78350F]/60 dark:text-[#FCD34D]',
|
||||
DELETE:
|
||||
'bg-[#FEE2E2] text-[#991B1B] dark:bg-[#7F1D1D]/60 dark:text-[#FCA5A5]',
|
||||
PATCH: 'bg-[#EDE9FE] text-[#5B21B6] dark:bg-[#4C1D95]/60 dark:text-[#C4B5FD]',
|
||||
HEAD: 'bg-[#F3F4F6] text-[#374151] dark:bg-[#374151]/60 dark:text-[#D1D5DB]',
|
||||
OPTIONS:
|
||||
'bg-[#F3F4F6] text-[#374151] dark:bg-[#374151]/60 dark:text-[#D1D5DB]',
|
||||
};
|
||||
|
||||
export default function ImportSpecModal({
|
||||
modalState,
|
||||
setModalState,
|
||||
onImport,
|
||||
}: ImportSpecModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const token = useSelector(selectToken);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [parsedResult, setParsedResult] = useState<ParsedResult | null>(null);
|
||||
const [selectedActions, setSelectedActions] = useState<Set<number>>(
|
||||
new Set(),
|
||||
);
|
||||
const [baseUrl, setBaseUrl] = useState<string>('');
|
||||
|
||||
const handleClose = () => {
|
||||
setModalState('INACTIVE');
|
||||
setFile(null);
|
||||
setLoading(false);
|
||||
setError(null);
|
||||
setParsedResult(null);
|
||||
setSelectedActions(new Set());
|
||||
setBaseUrl('');
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = e.target.files?.[0];
|
||||
if (!selectedFile) return;
|
||||
|
||||
const validExtensions = ['.json', '.yaml', '.yml'];
|
||||
const hasValidExtension = validExtensions.some((ext) =>
|
||||
selectedFile.name.toLowerCase().endsWith(ext),
|
||||
);
|
||||
|
||||
if (!hasValidExtension) {
|
||||
setError(t('modals.importSpec.invalidFileType'));
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
setError(null);
|
||||
setParsedResult(null);
|
||||
};
|
||||
|
||||
const handleParse = async () => {
|
||||
if (!file) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await userService.parseSpec(file, token);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
setError(
|
||||
errorData.error ||
|
||||
errorData.message ||
|
||||
t('modals.importSpec.parseError'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setParsedResult(result);
|
||||
setBaseUrl(result.metadata.base_url || '');
|
||||
setSelectedActions(
|
||||
new Set<number>(
|
||||
result.actions.map((_: APIActionType, i: number) => i),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setError(
|
||||
result.error || result.message || t('modals.importSpec.parseError'),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
setError(t('modals.importSpec.parseError'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAction = (index: number) => {
|
||||
setSelectedActions((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(index)) {
|
||||
next.delete(index);
|
||||
} else {
|
||||
next.add(index);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleAll = () => {
|
||||
if (!parsedResult) return;
|
||||
if (selectedActions.size === parsedResult.actions.length) {
|
||||
setSelectedActions(new Set());
|
||||
} else {
|
||||
setSelectedActions(new Set(parsedResult.actions.map((_, i) => i)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (!parsedResult) return;
|
||||
const actionsToImport = parsedResult.actions
|
||||
.filter((_, i) => selectedActions.has(i))
|
||||
.map((action) => ({
|
||||
...action,
|
||||
url: action.url.replace(parsedResult.metadata.base_url, baseUrl.trim()),
|
||||
}));
|
||||
onImport(actionsToImport);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
if (modalState !== 'ACTIVE') return null;
|
||||
|
||||
return (
|
||||
<WrapperModal
|
||||
close={handleClose}
|
||||
className="w-full max-w-2xl"
|
||||
contentClassName="max-h-[70vh]"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="text-jet dark:text-bright-gray text-xl font-semibold">
|
||||
{t('modals.importSpec.title')}
|
||||
</h2>
|
||||
|
||||
{!parsedResult ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('modals.importSpec.description')}
|
||||
</p>
|
||||
|
||||
<div
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="border-silver dark:border-silver/40 hover:border-purple-30 dark:hover:border-purple-30 flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 transition-colors"
|
||||
>
|
||||
<img
|
||||
src={Upload}
|
||||
alt="Upload"
|
||||
className="mb-3 h-10 w-10 opacity-60 dark:invert"
|
||||
/>
|
||||
<p className="text-jet dark:text-bright-gray text-sm font-medium">
|
||||
{file ? file.name : t('modals.importSpec.dropzoneText')}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t('modals.importSpec.supportedFormats')}
|
||||
</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json,.yaml,.yml"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-red-500 dark:text-red-400">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-xl bg-[#F9F9F9] p-4 dark:bg-[#28292D]">
|
||||
<h3 className="text-jet dark:text-bright-gray font-medium">
|
||||
{parsedResult.metadata.title}
|
||||
</h3>
|
||||
{parsedResult.metadata.description && (
|
||||
<p className="mt-1 line-clamp-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
{parsedResult.metadata.description}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
{t('modals.importSpec.version')}:{' '}
|
||||
{parsedResult.metadata.version}
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
{t('modals.importSpec.baseUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
className="border-silver dark:border-silver/40 text-jet dark:text-bright-gray w-full rounded-lg border bg-white px-3 py-2 text-sm outline-hidden dark:bg-[#2C2C2C]"
|
||||
placeholder={
|
||||
parsedResult.metadata.base_url || 'https://api.example.com'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<p className="text-jet dark:text-bright-gray text-sm font-medium">
|
||||
{t('modals.importSpec.actionsFound', {
|
||||
count: parsedResult.actions.length,
|
||||
})}
|
||||
</p>
|
||||
<button
|
||||
onClick={toggleAll}
|
||||
className="text-purple-30 hover:text-violets-are-blue text-sm"
|
||||
>
|
||||
{selectedActions.size === parsedResult.actions.length
|
||||
? t('modals.importSpec.deselectAll')
|
||||
: t('modals.importSpec.selectAll')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-72 space-y-2 overflow-y-auto px-1">
|
||||
{parsedResult.actions.map((action, index) => (
|
||||
<label
|
||||
key={index}
|
||||
className="border-silver dark:border-silver/40 flex cursor-pointer items-start gap-3 rounded-xl border p-3 transition-colors hover:bg-[#F9F9F9] dark:hover:bg-[#28292D]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedActions.has(index)}
|
||||
onChange={() => toggleAction(index)}
|
||||
className="text-purple-30 focus:ring-purple-30 mt-1 h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs font-medium ${METHOD_COLORS[action.method.toUpperCase()] || METHOD_COLORS.GET}`}
|
||||
>
|
||||
{action.method.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-jet dark:text-bright-gray truncate font-medium">
|
||||
{action.name}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 truncate text-sm text-gray-500 dark:text-gray-400">
|
||||
{action.url}
|
||||
</p>
|
||||
{action.description && (
|
||||
<p className="mt-1 line-clamp-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{action.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex flex-row-reverse gap-2">
|
||||
{!parsedResult ? (
|
||||
<button
|
||||
onClick={handleParse}
|
||||
disabled={!file || loading}
|
||||
className="bg-purple-30 hover:bg-violets-are-blue flex w-20 items-center justify-center gap-2 rounded-3xl px-5 py-2 text-sm text-white transition-all disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{loading && <Spinner size="small" color="white" />}
|
||||
{!loading && t('modals.importSpec.parse')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={selectedActions.size === 0}
|
||||
className="bg-purple-30 hover:bg-violets-are-blue rounded-3xl px-5 py-2 text-sm text-white transition-all disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{t('modals.importSpec.import', { count: selectedActions.size })}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="dark:text-light-gray cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:hover:bg-[#767183]/50"
|
||||
>
|
||||
{t('modals.importSpec.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</WrapperModal>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ export type ParameterGroupType = {
|
||||
description: string;
|
||||
value: string | number;
|
||||
filled_by_llm: boolean;
|
||||
required?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -57,6 +58,7 @@ export type UserToolType = {
|
||||
description: string;
|
||||
filled_by_llm: boolean;
|
||||
value: string;
|
||||
required?: boolean;
|
||||
};
|
||||
};
|
||||
additionalProperties: boolean;
|
||||
@@ -71,11 +73,24 @@ export type APIActionType = {
|
||||
name: string;
|
||||
url: string;
|
||||
description: string;
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||
query_params: ParameterGroupType;
|
||||
headers: ParameterGroupType;
|
||||
body: ParameterGroupType;
|
||||
active: boolean;
|
||||
body_content_type?:
|
||||
| 'application/json'
|
||||
| 'application/x-www-form-urlencoded'
|
||||
| 'multipart/form-data'
|
||||
| 'text/plain'
|
||||
| 'application/xml'
|
||||
| 'application/octet-stream';
|
||||
body_encoding_rules?: {
|
||||
[key: string]: {
|
||||
style?: 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject';
|
||||
explode?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type APIToolType = {
|
||||
|
||||
Reference in New Issue
Block a user