mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-17 06:14:25 +00:00
Fix a tool's parameters from the connector drawer
Under Customize, each action now opens its parameters. Each one is either left to the AI or set to Always use a value, which is sent on every call and never shown to the AI. Actions with fixed values carry a small count.
This commit is contained in:
1 parent
28b56e8b03
commit
744cfde565
13 files changed
+529
-51
No files matched your search
@@ -123,6 +123,8 @@ const endpoints = {
|
|||||||
`/api/connections/${encodeURIComponent(id)}/tools/${encodeURIComponent(toolId)}/permissions`,
|
`/api/connections/${encodeURIComponent(id)}/tools/${encodeURIComponent(toolId)}/permissions`,
|
||||||
CONNECTION_REPOSITORIES: (id: string) =>
|
CONNECTION_REPOSITORIES: (id: string) =>
|
||||||
`/api/connections/${encodeURIComponent(id)}/repositories`,
|
`/api/connections/${encodeURIComponent(id)}/repositories`,
|
||||||
|
CONNECTION_TOOL_PARAMETERS: (id: string, toolId: string) =>
|
||||||
|
`/api/connections/${encodeURIComponent(id)}/tools/${encodeURIComponent(toolId)}/parameters`,
|
||||||
CONNECTIONS_CLAIM: '/api/connections/claim',
|
CONNECTIONS_CLAIM: '/api/connections/claim',
|
||||||
TOOL_CREDENTIAL_MODE: (toolId: string) =>
|
TOOL_CREDENTIAL_MODE: (toolId: string) =>
|
||||||
`/api/connections/tools/${encodeURIComponent(toolId)}/credential-mode`,
|
`/api/connections/tools/${encodeURIComponent(toolId)}/credential-mode`,
|
||||||
|
|||||||
@@ -111,6 +111,21 @@ const connectorsService = {
|
|||||||
token,
|
token,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
/** Fix parameters to a value (`null` lets the AI decide again). */
|
||||||
|
setToolParameters: async (
|
||||||
|
id: string,
|
||||||
|
toolId: string,
|
||||||
|
action: string,
|
||||||
|
parameters: Record<string, string | number | boolean | null>,
|
||||||
|
token: string | null,
|
||||||
|
) =>
|
||||||
|
json(
|
||||||
|
await apiClient.put(
|
||||||
|
endpoints.USER.CONNECTION_TOOL_PARAMETERS(id, toolId),
|
||||||
|
{ action, parameters },
|
||||||
|
token,
|
||||||
|
),
|
||||||
|
),
|
||||||
setCredentialMode: async (
|
setCredentialMode: async (
|
||||||
toolId: string,
|
toolId: string,
|
||||||
mode: 'owner' | 'member',
|
mode: 'owner' | 'member',
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Button } from '../components/ui/button';
|
||||||
|
import { Input } from '../components/ui/input';
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from '../components/ui/toggle-group';
|
||||||
|
import type { ActionParameter } from './types';
|
||||||
|
|
||||||
|
type Mode = 'ai' | 'fixed';
|
||||||
|
export type ParameterValue = string | number | boolean | null;
|
||||||
|
|
||||||
|
const shown = (parameter: ActionParameter) =>
|
||||||
|
parameter.fixed && parameter.value !== null ? String(parameter.value) : '';
|
||||||
|
|
||||||
|
function ParameterRow({
|
||||||
|
parameter,
|
||||||
|
readOnly,
|
||||||
|
onSave,
|
||||||
|
}: {
|
||||||
|
parameter: ActionParameter;
|
||||||
|
readOnly: boolean;
|
||||||
|
onSave: (value: ParameterValue) => Promise<boolean>;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const saved = shown(parameter);
|
||||||
|
const [mode, setMode] = useState<Mode>(parameter.fixed ? 'fixed' : 'ai');
|
||||||
|
const [draft, setDraft] = useState(saved);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
// A save (here or elsewhere) brings the stored value back.
|
||||||
|
useEffect(() => {
|
||||||
|
setMode(parameter.fixed ? 'fixed' : 'ai');
|
||||||
|
setDraft(shown(parameter));
|
||||||
|
}, [parameter]);
|
||||||
|
|
||||||
|
const save = async (value: ParameterValue) => {
|
||||||
|
setSaving(true);
|
||||||
|
const ok = await onSave(value);
|
||||||
|
setSaving(false);
|
||||||
|
if (!ok) {
|
||||||
|
setMode(parameter.fixed ? 'fixed' : 'ai');
|
||||||
|
setDraft(saved);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const choiceLabel = t('settings.connectors.parameters.choiceLabel', {
|
||||||
|
parameter: parameter.name,
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li data-parameter={parameter.name} className="flex flex-col gap-2">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-foreground font-mono text-xs wrap-anywhere">
|
||||||
|
{parameter.name}
|
||||||
|
</p>
|
||||||
|
{parameter.description && (
|
||||||
|
<p
|
||||||
|
className="text-muted-foreground line-clamp-2 text-xs"
|
||||||
|
title={parameter.description}
|
||||||
|
>
|
||||||
|
{parameter.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted rounded-full p-1">
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
size="xs"
|
||||||
|
value={mode}
|
||||||
|
disabled={readOnly || saving}
|
||||||
|
aria-label={choiceLabel}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (!value) return;
|
||||||
|
setMode(value as Mode);
|
||||||
|
// Releasing a fixed value saves at once; fixing one waits for it.
|
||||||
|
if (value === 'ai' && parameter.fixed) save(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value="ai" data-mode="ai">
|
||||||
|
{t('settings.connectors.parameters.ai')}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="fixed" data-mode="fixed">
|
||||||
|
{t('settings.connectors.parameters.fixed')}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{mode === 'fixed' && (
|
||||||
|
<form
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (draft.trim() && draft !== saved) save(draft);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
aria-label={t('settings.connectors.parameters.valueLabel', {
|
||||||
|
parameter: parameter.name,
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
})}
|
||||||
|
value={draft}
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={(event) => setDraft(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
shape="pill"
|
||||||
|
variant="outline"
|
||||||
|
loading={saving}
|
||||||
|
disabled={readOnly || !draft.trim() || draft === saved}
|
||||||
|
>
|
||||||
|
{t('settings.connectors.parameters.save')}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One action's parameters, each left to the AI or fixed to a value that is
|
||||||
|
* sent on every call and never shown to the AI (a Telegram chat, a project
|
||||||
|
* key). Letting the AI decide saves at once; a fixed value saves with Save.
|
||||||
|
*/
|
||||||
|
export default function ActionParameters({
|
||||||
|
parameters,
|
||||||
|
readOnly = false,
|
||||||
|
onSave,
|
||||||
|
}: {
|
||||||
|
parameters: ActionParameter[];
|
||||||
|
readOnly?: boolean;
|
||||||
|
/** Saves `{name: value}`; resolves false when the save failed. */
|
||||||
|
onSave: (changes: Record<string, ParameterValue>) => Promise<boolean>;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<ul className="flex flex-col gap-4">
|
||||||
|
{parameters.map((parameter) => (
|
||||||
|
<ParameterRow
|
||||||
|
key={parameter.name}
|
||||||
|
parameter={parameter}
|
||||||
|
readOnly={readOnly}
|
||||||
|
onSave={(value) => onSave({ [parameter.name]: value })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{t('settings.connectors.parameters.hint')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,12 +10,15 @@ vi.mock('react-i18next', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const connectors = vi.hoisted(() => ({ setToolPermissions: vi.fn() }));
|
const connectors = vi.hoisted(() => ({
|
||||||
|
setToolPermissions: vi.fn(),
|
||||||
|
setToolParameters: vi.fn(),
|
||||||
|
}));
|
||||||
vi.mock('../api/services/connectorsService', () => ({ default: connectors }));
|
vi.mock('../api/services/connectorsService', () => ({ default: connectors }));
|
||||||
|
|
||||||
import actionToastReducer from '../notifications/actionToastSlice';
|
import actionToastReducer from '../notifications/actionToastSlice';
|
||||||
import ToolPermissions from './ToolPermissions';
|
import ToolPermissions from './ToolPermissions';
|
||||||
import type { ConnectionTool } from './types';
|
import type { ActionParameter, ConnectionTool } from './types';
|
||||||
|
|
||||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
||||||
|
|
||||||
@@ -23,7 +26,21 @@ const action = (
|
|||||||
name: string,
|
name: string,
|
||||||
access: 'read' | 'write',
|
access: 'read' | 'write',
|
||||||
permission: 'always' | 'ask' | 'off',
|
permission: 'always' | 'ask' | 'off',
|
||||||
) => ({ name, access, permission, description: `Does ${name}.` });
|
parameters: ActionParameter[] = [],
|
||||||
|
) => ({ name, access, permission, description: `Does ${name}.`, parameters });
|
||||||
|
|
||||||
|
const param = (
|
||||||
|
name: string,
|
||||||
|
fixed: boolean,
|
||||||
|
value: ActionParameter['value'] = null,
|
||||||
|
): ActionParameter => ({
|
||||||
|
name,
|
||||||
|
description: `The ${name}.`,
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
fixed,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
const tool = (actions: ReturnType<typeof action>[]): ConnectionTool =>
|
const tool = (actions: ReturnType<typeof action>[]): ConnectionTool =>
|
||||||
({
|
({
|
||||||
@@ -127,6 +144,105 @@ describe('ToolPermissions', () => {
|
|||||||
expect(container.textContent).not.toContain('Old one');
|
expect(container.textContent).not.toContain('Old one');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const button = (text: string) =>
|
||||||
|
Array.from(container.querySelectorAll('button')).find(
|
||||||
|
(b) => b.textContent === text,
|
||||||
|
)!;
|
||||||
|
const typeInto = (input: HTMLInputElement, value: string) => {
|
||||||
|
const setter = Object.getOwnPropertyDescriptor(
|
||||||
|
HTMLInputElement.prototype,
|
||||||
|
'value',
|
||||||
|
)!.set!;
|
||||||
|
setter.call(input, value);
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
it('marks an action that has fixed values', async () => {
|
||||||
|
await render(
|
||||||
|
tool([
|
||||||
|
action('telegram_send_message', 'write', 'ask', [
|
||||||
|
param('text', false),
|
||||||
|
param('chat_id', true, '-100'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(container.textContent).toContain(
|
||||||
|
'settings.connectors.parameters.fixedCount:1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fixes a parameter to a value the AI cannot change', async () => {
|
||||||
|
const send = action('telegram_send_message', 'write', 'ask', [
|
||||||
|
param('text', false),
|
||||||
|
param('chat_id', false),
|
||||||
|
]);
|
||||||
|
connectors.setToolParameters.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
tool: tool([
|
||||||
|
{
|
||||||
|
...send,
|
||||||
|
parameters: [param('text', false), param('chat_id', true, '-100')],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
await render(tool([send]));
|
||||||
|
await act(async () =>
|
||||||
|
button('settings.connectors.parameters.show').click(),
|
||||||
|
);
|
||||||
|
const row = container.querySelector<HTMLElement>(
|
||||||
|
'[data-parameter="chat_id"]',
|
||||||
|
)!;
|
||||||
|
await act(async () =>
|
||||||
|
row.querySelector<HTMLButtonElement>('[data-mode="fixed"]')!.click(),
|
||||||
|
);
|
||||||
|
const input = row.querySelector<HTMLInputElement>('input')!;
|
||||||
|
await act(async () => typeInto(input, '-100'));
|
||||||
|
await act(async () =>
|
||||||
|
button('settings.connectors.parameters.save').click(),
|
||||||
|
);
|
||||||
|
expect(connectors.setToolParameters).toHaveBeenCalledWith(
|
||||||
|
'conn-1',
|
||||||
|
'tool-1',
|
||||||
|
'telegram_send_message',
|
||||||
|
{ chat_id: '-100' },
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
expect(container.textContent).toContain(
|
||||||
|
'settings.connectors.parameters.fixedCount:1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets the AI decide a fixed parameter again', async () => {
|
||||||
|
connectors.setToolParameters.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
tool: tool([]),
|
||||||
|
});
|
||||||
|
await render(
|
||||||
|
tool([
|
||||||
|
action('telegram_send_message', 'write', 'ask', [
|
||||||
|
param('chat_id', true, '-100'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
await act(async () =>
|
||||||
|
button('settings.connectors.parameters.show').click(),
|
||||||
|
);
|
||||||
|
const row = container.querySelector<HTMLElement>(
|
||||||
|
'[data-parameter="chat_id"]',
|
||||||
|
)!;
|
||||||
|
expect(row.querySelector<HTMLInputElement>('input')!.value).toBe('-100');
|
||||||
|
await act(async () =>
|
||||||
|
row.querySelector<HTMLButtonElement>('[data-mode="ai"]')!.click(),
|
||||||
|
);
|
||||||
|
expect(connectors.setToolParameters).toHaveBeenCalledWith(
|
||||||
|
'conn-1',
|
||||||
|
'tool-1',
|
||||||
|
'telegram_send_message',
|
||||||
|
{ chat_id: null },
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('says what Ask first means outside chat', async () => {
|
it('says what Ask first means outside chat', async () => {
|
||||||
await render(tool([action('create_issue', 'write', 'ask')]));
|
await render(tool([action('create_issue', 'write', 'ask')]));
|
||||||
expect(container.textContent).toContain(
|
expect(container.textContent).toContain(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import connectorsService from '../api/services/connectorsService';
|
import connectorsService from '../api/services/connectorsService';
|
||||||
|
import { Badge } from '../components/ui/badge';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
import { Card } from '../components/ui/card';
|
import { Card } from '../components/ui/card';
|
||||||
import { SectionHeader } from '../components/ui/section-header';
|
import { SectionHeader } from '../components/ui/section-header';
|
||||||
@@ -17,6 +18,7 @@ import { ToggleGroup, ToggleGroupItem } from '../components/ui/toggle-group';
|
|||||||
import { showActionToast } from '../notifications/actionToastSlice';
|
import { showActionToast } from '../notifications/actionToastSlice';
|
||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import { formatCount } from '../utils/dateTimeUtils';
|
import { formatCount } from '../utils/dateTimeUtils';
|
||||||
|
import ActionParameters, { type ParameterValue } from './ActionParameters';
|
||||||
import { actionTitle } from './i18n';
|
import { actionTitle } from './i18n';
|
||||||
import type {
|
import type {
|
||||||
ActionPermission,
|
ActionPermission,
|
||||||
@@ -29,11 +31,105 @@ const PERMISSIONS: ActionPermission[] = ['always', 'ask', 'off'];
|
|||||||
/** Longer groups start folded to their one group choice. */
|
/** Longer groups start folded to their one group choice. */
|
||||||
const FOLD_AFTER = 5;
|
const FOLD_AFTER = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One action under Customize: its name, what it does and its permission,
|
||||||
|
* with its parameters one click away and a count of the values fixed there.
|
||||||
|
*/
|
||||||
|
function ActionRow({
|
||||||
|
action,
|
||||||
|
readOnly,
|
||||||
|
onPermission,
|
||||||
|
onParameters,
|
||||||
|
}: {
|
||||||
|
action: ConnectionToolAction;
|
||||||
|
readOnly: boolean;
|
||||||
|
onPermission: (permission: ActionPermission) => void;
|
||||||
|
onParameters: (changes: Record<string, ParameterValue>) => Promise<boolean>;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [showParameters, setShowParameters] = useState(false);
|
||||||
|
const parameters = action.parameters ?? [];
|
||||||
|
const fixedCount = parameters.filter((parameter) => parameter.fixed).length;
|
||||||
|
return (
|
||||||
|
<li className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex min-w-0 flex-col items-start">
|
||||||
|
<div className="flex max-w-full items-center gap-2">
|
||||||
|
<p className="text-foreground truncate text-sm" title={action.name}>
|
||||||
|
{actionTitle(action.name)}
|
||||||
|
</p>
|
||||||
|
{fixedCount > 0 && (
|
||||||
|
<Badge variant="neutral">
|
||||||
|
{t('settings.connectors.parameters.fixedCount', {
|
||||||
|
count: fixedCount,
|
||||||
|
formatted: formatCount(fixedCount),
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action.description && (
|
||||||
|
<p
|
||||||
|
className="text-muted-foreground line-clamp-2 text-xs"
|
||||||
|
title={action.description}
|
||||||
|
>
|
||||||
|
{action.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{parameters.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="link"
|
||||||
|
size="inline"
|
||||||
|
className="mt-1"
|
||||||
|
aria-expanded={showParameters}
|
||||||
|
onClick={() => setShowParameters(!showParameters)}
|
||||||
|
>
|
||||||
|
{showParameters
|
||||||
|
? t('settings.connectors.parameters.hide')
|
||||||
|
: t('settings.connectors.parameters.show')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={action.permission}
|
||||||
|
disabled={readOnly}
|
||||||
|
onValueChange={(value) => onPermission(value as ActionPermission)}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
size="sm"
|
||||||
|
className="w-32 shrink-0"
|
||||||
|
aria-label={t('settings.connectors.permission.label', {
|
||||||
|
action: actionTitle(action.name),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PERMISSIONS.map((permission) => (
|
||||||
|
<SelectItem key={permission} value={permission}>
|
||||||
|
{t(`settings.connectors.permission.${permission}`)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{showParameters && (
|
||||||
|
<ActionParameters
|
||||||
|
parameters={parameters}
|
||||||
|
readOnly={readOnly}
|
||||||
|
onSave={onParameters}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One tool's actions in two groups, what it looks up and what it does, each
|
* One tool's actions in two groups, what it looks up and what it does, each
|
||||||
* set at once to Allow / Ask first / Off; single actions can differ under
|
* set at once to Allow / Ask first / Off; single actions can differ under
|
||||||
* Customize. Changes save immediately; a failed save puts the previous
|
* Customize, where each action also opens its parameters to fix values the
|
||||||
* choices back and says so.
|
* AI must always use. Changes save immediately; a failed save puts the
|
||||||
|
* previous choices back and says so.
|
||||||
*/
|
*/
|
||||||
export default function ToolPermissions({
|
export default function ToolPermissions({
|
||||||
connectionId,
|
connectionId,
|
||||||
@@ -83,6 +179,33 @@ export default function ToolPermissions({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setParameters = async (
|
||||||
|
actionName: string,
|
||||||
|
changes: Record<string, ParameterValue>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const data = await connectorsService.setToolParameters(
|
||||||
|
connectionId,
|
||||||
|
tool.id,
|
||||||
|
actionName,
|
||||||
|
changes,
|
||||||
|
token,
|
||||||
|
);
|
||||||
|
if (!data?.success) throw new Error('save failed');
|
||||||
|
setActions(data.tool.actions);
|
||||||
|
onChange?.(data.tool);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
dispatch(
|
||||||
|
showActionToast({
|
||||||
|
variant: 'destructive',
|
||||||
|
message: t('settings.connectors.parameters.saveFailed'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const groups = (['read', 'write'] as const)
|
const groups = (['read', 'write'] as const)
|
||||||
.map((access) => ({
|
.map((access) => ({
|
||||||
access,
|
access,
|
||||||
@@ -177,53 +300,17 @@ export default function ToolPermissions({
|
|||||||
{open && (
|
{open && (
|
||||||
<ul className="flex flex-col gap-3">
|
<ul className="flex flex-col gap-3">
|
||||||
{group.actions.map((action) => (
|
{group.actions.map((action) => (
|
||||||
<li
|
<ActionRow
|
||||||
key={action.name}
|
key={action.name}
|
||||||
className="flex items-start justify-between gap-3"
|
action={action}
|
||||||
>
|
readOnly={readOnly}
|
||||||
<div className="min-w-0">
|
onPermission={(permission) =>
|
||||||
<p
|
setPermissions({ [action.name]: permission })
|
||||||
className="text-foreground truncate text-sm"
|
}
|
||||||
title={action.name}
|
onParameters={(changes) =>
|
||||||
>
|
setParameters(action.name, changes)
|
||||||
{actionTitle(action.name)}
|
}
|
||||||
</p>
|
/>
|
||||||
{action.description && (
|
|
||||||
<p
|
|
||||||
className="text-muted-foreground line-clamp-2 text-xs"
|
|
||||||
title={action.description}
|
|
||||||
>
|
|
||||||
{action.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
value={action.permission}
|
|
||||||
disabled={readOnly}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setPermissions({
|
|
||||||
[action.name]: value as ActionPermission,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
size="sm"
|
|
||||||
className="w-32 shrink-0"
|
|
||||||
aria-label={t('settings.connectors.permission.label', {
|
|
||||||
action: actionTitle(action.name),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{PERMISSIONS.map((permission) => (
|
|
||||||
<SelectItem key={permission} value={permission}>
|
|
||||||
{t(`settings.connectors.permission.${permission}`)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -92,11 +92,22 @@ export type Connection = {
|
|||||||
export type ActionAccess = 'read' | 'write';
|
export type ActionAccess = 'read' | 'write';
|
||||||
export type ActionPermission = 'always' | 'ask' | 'off';
|
export type ActionPermission = 'always' | 'ask' | 'off';
|
||||||
|
|
||||||
|
export type ActionParameter = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
required: boolean;
|
||||||
|
/** Sent with `value` on every call; the AI never sees it. */
|
||||||
|
fixed: boolean;
|
||||||
|
value: string | number | boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConnectionToolAction = {
|
export type ConnectionToolAction = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
access: ActionAccess;
|
access: ActionAccess;
|
||||||
permission: ActionPermission;
|
permission: ActionPermission;
|
||||||
|
parameters?: ActionParameter[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConnectionTool = {
|
export type ConnectionTool = {
|
||||||
|
|||||||
@@ -1344,6 +1344,19 @@
|
|||||||
"repositories": "Repositorys",
|
"repositories": "Repositorys",
|
||||||
"updated": "Aktualisiert {{date}}",
|
"updated": "Aktualisiert {{date}}",
|
||||||
"loadFailed": "Deine Repositorys konnten nicht geladen werden."
|
"loadFailed": "Deine Repositorys konnten nicht geladen werden."
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"show": "Parameter",
|
||||||
|
"hide": "Parameter ausblenden",
|
||||||
|
"fixedCount_one": "{{formatted}} festgelegt",
|
||||||
|
"fixedCount_other": "{{formatted}} festgelegt",
|
||||||
|
"ai": "KI entscheidet",
|
||||||
|
"fixed": "Immer verwenden",
|
||||||
|
"choiceLabel": "Wie {{parameter}} ausgefüllt wird",
|
||||||
|
"valueLabel": "Wert für {{parameter}}",
|
||||||
|
"save": "Speichern",
|
||||||
|
"saveFailed": "Der Wert konnte nicht gespeichert werden. Versuche es erneut.",
|
||||||
|
"hint": "Ein Wert mit „Immer verwenden“ wird bei jedem Aufruf gesendet. Die KI sieht ihn nicht und kann ihn nicht ändern."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1350,6 +1350,19 @@
|
|||||||
"repositories": "Repositories",
|
"repositories": "Repositories",
|
||||||
"updated": "Updated {{date}}",
|
"updated": "Updated {{date}}",
|
||||||
"loadFailed": "Could not load your repositories."
|
"loadFailed": "Could not load your repositories."
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"show": "Parameters",
|
||||||
|
"hide": "Hide parameters",
|
||||||
|
"fixedCount_one": "{{formatted}} fixed",
|
||||||
|
"fixedCount_other": "{{formatted}} fixed",
|
||||||
|
"ai": "Let AI decide",
|
||||||
|
"fixed": "Always use",
|
||||||
|
"choiceLabel": "How {{parameter}} is filled in",
|
||||||
|
"valueLabel": "Value for {{parameter}}",
|
||||||
|
"save": "Save",
|
||||||
|
"saveFailed": "Could not save the value. Try again.",
|
||||||
|
"hint": "A value set to Always use is sent on every call. The AI never sees it and cannot change it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1344,6 +1344,19 @@
|
|||||||
"repositories": "Repositorios",
|
"repositories": "Repositorios",
|
||||||
"updated": "Actualizado {{date}}",
|
"updated": "Actualizado {{date}}",
|
||||||
"loadFailed": "No se pudieron cargar tus repositorios."
|
"loadFailed": "No se pudieron cargar tus repositorios."
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"show": "Parámetros",
|
||||||
|
"hide": "Ocultar parámetros",
|
||||||
|
"fixedCount_one": "{{formatted}} fijo",
|
||||||
|
"fixedCount_other": "{{formatted}} fijos",
|
||||||
|
"ai": "Que decida la IA",
|
||||||
|
"fixed": "Usar siempre",
|
||||||
|
"choiceLabel": "Cómo se rellena {{parameter}}",
|
||||||
|
"valueLabel": "Valor de {{parameter}}",
|
||||||
|
"save": "Guardar",
|
||||||
|
"saveFailed": "No se pudo guardar el valor. Inténtalo de nuevo.",
|
||||||
|
"hint": "Un valor con «Usar siempre» se envía en cada llamada. La IA nunca lo ve ni puede cambiarlo."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1326,6 +1326,18 @@
|
|||||||
"repositories": "リポジトリ",
|
"repositories": "リポジトリ",
|
||||||
"updated": "{{date}} に更新",
|
"updated": "{{date}} に更新",
|
||||||
"loadFailed": "リポジトリを読み込めませんでした。"
|
"loadFailed": "リポジトリを読み込めませんでした。"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"show": "パラメーター",
|
||||||
|
"hide": "パラメーターを隠す",
|
||||||
|
"fixedCount_other": "{{formatted}} 件固定",
|
||||||
|
"ai": "AI に任せる",
|
||||||
|
"fixed": "常に使用",
|
||||||
|
"choiceLabel": "{{parameter}} の入力方法",
|
||||||
|
"valueLabel": "{{parameter}} の値",
|
||||||
|
"save": "保存",
|
||||||
|
"saveFailed": "値を保存できませんでした。もう一度お試しください。",
|
||||||
|
"hint": "「常に使用」の値は毎回の呼び出しで送信されます。AI はこの値を見ることも変更することもできません。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1418,6 +1418,21 @@
|
|||||||
"repositories": "Репозитории",
|
"repositories": "Репозитории",
|
||||||
"updated": "Обновлён {{date}}",
|
"updated": "Обновлён {{date}}",
|
||||||
"loadFailed": "Не удалось загрузить ваши репозитории."
|
"loadFailed": "Не удалось загрузить ваши репозитории."
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"show": "Параметры",
|
||||||
|
"hide": "Скрыть параметры",
|
||||||
|
"fixedCount_one": "{{formatted}} задан",
|
||||||
|
"fixedCount_few": "{{formatted}} задано",
|
||||||
|
"fixedCount_many": "{{formatted}} задано",
|
||||||
|
"fixedCount_other": "{{formatted}} задано",
|
||||||
|
"ai": "Решает ИИ",
|
||||||
|
"fixed": "Всегда использовать",
|
||||||
|
"choiceLabel": "Как заполняется {{parameter}}",
|
||||||
|
"valueLabel": "Значение {{parameter}}",
|
||||||
|
"save": "Сохранить",
|
||||||
|
"saveFailed": "Не удалось сохранить значение. Попробуйте ещё раз.",
|
||||||
|
"hint": "Значение «Всегда использовать» отправляется при каждом вызове. ИИ его не видит и не может изменить."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1326,6 +1326,18 @@
|
|||||||
"repositories": "儲存庫",
|
"repositories": "儲存庫",
|
||||||
"updated": "更新於 {{date}}",
|
"updated": "更新於 {{date}}",
|
||||||
"loadFailed": "無法載入你的儲存庫。"
|
"loadFailed": "無法載入你的儲存庫。"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"show": "參數",
|
||||||
|
"hide": "隱藏參數",
|
||||||
|
"fixedCount_other": "已固定 {{formatted}} 個",
|
||||||
|
"ai": "由 AI 決定",
|
||||||
|
"fixed": "一律使用",
|
||||||
|
"choiceLabel": "{{parameter}} 的填寫方式",
|
||||||
|
"valueLabel": "{{parameter}} 的值",
|
||||||
|
"save": "儲存",
|
||||||
|
"saveFailed": "無法儲存此值。請再試一次。",
|
||||||
|
"hint": "設為「一律使用」的值會在每次呼叫時送出。AI 看不到它,也無法變更。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1326,6 +1326,18 @@
|
|||||||
"repositories": "仓库",
|
"repositories": "仓库",
|
||||||
"updated": "更新于 {{date}}",
|
"updated": "更新于 {{date}}",
|
||||||
"loadFailed": "无法加载你的仓库。"
|
"loadFailed": "无法加载你的仓库。"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"show": "参数",
|
||||||
|
"hide": "隐藏参数",
|
||||||
|
"fixedCount_other": "已固定 {{formatted}} 个",
|
||||||
|
"ai": "由 AI 决定",
|
||||||
|
"fixed": "始终使用",
|
||||||
|
"choiceLabel": "{{parameter}} 的填写方式",
|
||||||
|
"valueLabel": "{{parameter}} 的值",
|
||||||
|
"save": "保存",
|
||||||
|
"saveFailed": "无法保存该值。请重试。",
|
||||||
|
"hint": "设为“始终使用”的值会在每次调用时发送。AI 看不到它,也无法更改。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user