Improved prompt function add add ctx.* functions (#301)

This commit is contained in:
Gregory Schier
2025-11-15 08:19:58 -08:00
committed by GitHub
parent 7ced183b11
commit 84219571e8
29 changed files with 454 additions and 150 deletions

View File

@@ -30,7 +30,6 @@ export const createFolder = createFastMutation<
label: 'Name',
defaultValue: 'Folder',
title: 'New Folder',
required: true,
confirmText: 'Create',
placeholder: 'Name',
});

View File

@@ -26,7 +26,7 @@ import { IconButton } from './core/IconButton';
import { Input } from './core/Input';
import { Label } from './core/Label';
import { Select } from './core/Select';
import { HStack, VStack } from './core/Stacks';
import { VStack } from './core/Stacks';
import { Markdown } from './Markdown';
import { SelectFile } from './SelectFile';
@@ -216,7 +216,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
);
case 'h_stack':
return (
<HStack key={i + stateKey} alignItems="end" space={3}>
<div className="flex flex-wrap sm:flex-nowrap gap-3 items-end" key={i + stateKey}>
<FormInputs
data={data}
disabled={disabled}
@@ -226,7 +226,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
autocompleteFunctions={autocompleteFunctions || false}
autocompleteVariables={autocompleteVariables}
/>
</HStack>
</div>
);
case 'banner':
return (

View File

@@ -50,7 +50,6 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
const newMethod = await showPrompt({
id: 'custom-method',
label: 'Http Method',
defaultValue: '',
title: 'Custom Method',
confirmText: 'Save',
description: 'Enter a custom method name',

View File

@@ -54,7 +54,6 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
initial.value = await convertTemplateToInsecure(template);
}
console.log('INITIAL', initial);
setInitialArgValues(initial);
})().catch(console.error);
}, [
@@ -78,7 +77,7 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
}
function InitializedTemplateFunctionDialog({
templateFunction: { name },
templateFunction: { name, previewType: ogPreviewType },
initialArgValues,
hide,
onChange,
@@ -86,7 +85,7 @@ function InitializedTemplateFunctionDialog({
}: Omit<Props, 'initialTokens'> & {
initialArgValues: Record<string, string | boolean>;
}) {
const enablePreview = name !== 'secure';
const previewType = ogPreviewType == null ? 'live' : ogPreviewType;
const [showSecretsInPreview, toggleShowSecretsInPreview] = useToggle(false);
const [argValues, setArgValues] = useState<Record<string, string | boolean>>(initialArgValues);
@@ -126,7 +125,14 @@ function InitializedTemplateFunctionDialog({
};
const debouncedTagText = useDebouncedValue(tagText.data ?? '', 400);
const rendered = useRenderTemplate(debouncedTagText);
const [renderKey, setRenderKey] = useState<string | null>(null);
const rendered = useRenderTemplate(
debouncedTagText,
previewType !== 'none',
previewType === 'click' ? 'send' : 'preview',
previewType === 'live' ? renderKey + debouncedTagText : renderKey,
);
const tooLarge = rendered.data ? rendered.data.length > 10000 : false;
const dataContainsSecrets = useMemo(() => {
for (const [name, value] of Object.entries(argValues)) {
@@ -174,12 +180,12 @@ function InitializedTemplateFunctionDialog({
)}
</div>
<div className="px-6 border-t border-t-border py-3 bg-surface-highlight w-full flex flex-col gap-4">
{enablePreview ? (
{previewType !== 'none' ? (
<VStack className="w-full">
<HStack space={0.5}>
<HStack className="text-sm text-text-subtle" space={1.5}>
Rendered Preview
{rendered.isPending && <LoadingIcon size="xs" />}
{rendered.isLoading && <LoadingIcon size="xs" />}
</HStack>
<IconButton
size="xs"
@@ -195,6 +201,7 @@ function InitializedTemplateFunctionDialog({
</HStack>
<InlineCode
className={classNames(
'relative',
'whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars !border-text-subtlest',
tooLarge && 'italic text-danger',
)}
@@ -212,6 +219,18 @@ function InitializedTemplateFunctionDialog({
) : (
rendered.data || <>&nbsp;</>
)}
<div className="absolute right-0 top-0 bottom-0 flex items-center">
<IconButton
size="xs"
icon="refresh"
className="text-text-subtle"
title="Refresh preview"
spin={rendered.isLoading}
onClick={() => {
setRenderKey(new Date().toISOString());
}}
/>
</div>
</InlineCode>
</VStack>
) : (
@@ -250,8 +269,16 @@ function collectArgumentValues(initialTokens: Tokens, templateFunction: Template
if (!('name' in arg)) return;
const initialArg = initialArgs.find((a) => a.name === arg.name);
const initialArgValue = initialArg?.value.type === 'str' ? initialArg?.value.text : undefined;
initial[arg.name] = initialArgValue ?? arg.defaultValue ?? DYNAMIC_FORM_NULL_ARG;
const initialArgValue =
initialArg?.value.type === 'str'
? initialArg?.value.text
: initialArg?.value.type === 'bool'
? initialArg.value.value
: undefined;
const value = initialArgValue ?? arg.defaultValue;
if (value != null) {
initial[arg.name] = value;
}
};
templateFunction.args.forEach(processArg);

View File

@@ -1,71 +0,0 @@
import type { Tokens } from '@yaakapp-internal/templates';
import { useCallback, useMemo, useState } from 'react';
import { useActiveEnvironmentVariables } from '../hooks/useActiveEnvironmentVariables';
import { useRenderTemplate } from '../hooks/useRenderTemplate';
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import { Select } from './core/Select';
import { VStack } from './core/Stacks';
interface Props {
initialTokens: Tokens;
hide: () => void;
onChange: (rawTag: string) => void;
}
export function TemplateVariableDialog({ hide, onChange, initialTokens }: Props) {
const variables = useActiveEnvironmentVariables();
const [selectedVariableName, setSelectedVariableName] = useState<string>(() => {
return initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'var'
? initialTokens.tokens[0]?.val.name
: ''; // Should never happen
});
const tokens: Tokens = useMemo(() => {
const selectedVariable = variables.find((v) => v.name === selectedVariableName);
return {
tokens: [
{
type: 'tag',
val: {
type: 'var',
name: selectedVariable?.name ?? '',
},
},
],
};
}, [selectedVariableName, variables]);
const tagText = useTemplateTokensToString(tokens);
const handleDone = useCallback(async () => {
if (tagText.data != null) {
onChange(tagText.data);
}
hide();
}, [hide, onChange, tagText.data]);
const rendered = useRenderTemplate(tagText.data ?? '');
return (
<VStack className="pb-3" space={4}>
<VStack space={2}>
<Select
name="variable"
label="Select Variable"
value={selectedVariableName}
options={variables.map((v) => ({ label: v.name, value: v.name }))}
onChange={setSelectedVariableName}
/>
</VStack>
<VStack>
<div className="text-sm text-text-subtle">Preview</div>
<InlineCode className="select-text cursor-text">{rendered.data}</InlineCode>
</VStack>
<Button color="primary" onClick={handleDone}>
Done
</Button>
</VStack>
);
}

View File

@@ -9,7 +9,7 @@ export interface BannerProps {
export function Banner({ children, className, color }: BannerProps) {
return (
<div className="w-full mb-auto grid grid-rows-1 max-h-full">
<div className="w-auto grid grid-rows-1 max-h-full flex-0">
<div
className={classNames(
className,
@@ -18,6 +18,7 @@ export function Banner({ children, className, color }: BannerProps) {
'border border-border border-dashed',
'px-4 py-2 rounded-lg select-auto',
'overflow-auto text-text',
'mb-auto', // Don't stretch all the way down if the parent is in grid or flexbox
)}
>
{children}

View File

@@ -723,6 +723,7 @@ function FileActionsDropdown({
id: 'content-type',
title: 'Override Content-Type',
label: 'Content-Type',
required: false,
placeholder: 'text/plain',
defaultValue: pair.contentType ?? '',
confirmText: 'Set',

View File

@@ -16,6 +16,7 @@ export function Prompt({
label,
defaultValue,
placeholder,
password,
onResult,
required,
confirmText,
@@ -36,10 +37,10 @@ export function Prompt({
onSubmit={handleSubmit}
>
<PlainInput
hideLabel
autoSelect
required={required}
placeholder={placeholder ?? 'Enter text'}
type={password ? 'password' : 'text'}
label={label}
defaultValue={defaultValue}
onChange={setValue}

View File

@@ -1,17 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import type { RenderPurpose } from '@yaakapp-internal/plugins';
import { useAtomValue } from 'jotai';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironment } from './useActiveEnvironment';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useRenderTemplate(template: string) {
export function useRenderTemplate(
template: string,
enabled: boolean,
purpose: RenderPurpose,
refreshKey: string | null,
) {
const workspaceId = useAtomValue(activeWorkspaceIdAtom) ?? 'n/a';
const environmentId = useActiveEnvironment()?.id ?? null;
return useQuery<string>({
refetchOnWindowFocus: false,
queryKey: ['render_template', template, workspaceId, environmentId],
queryFn: () => minPromiseMillis(renderTemplate({ template, workspaceId, environmentId }), 200),
enabled,
queryKey: ['render_template', workspaceId, environmentId, refreshKey, purpose],
queryFn: () =>
minPromiseMillis(renderTemplate({ template, workspaceId, environmentId, purpose }), 300),
});
}
@@ -19,12 +27,14 @@ export async function renderTemplate({
template,
workspaceId,
environmentId,
purpose,
}: {
template: string;
workspaceId: string;
environmentId: string | null;
purpose: RenderPurpose;
}): Promise<string> {
return invokeCmd('cmd_render_template', { template, workspaceId, environmentId });
return invokeCmd('cmd_render_template', { template, workspaceId, environmentId, purpose });
}
export async function decryptTemplate({

View File

@@ -6,7 +6,13 @@ import { showDialog } from './dialog';
type PromptArgs = Pick<DialogProps, 'title' | 'description'> &
Omit<PromptProps, 'onClose' | 'onCancel' | 'onResult'> & { id: string };
export async function showPrompt({ id, title, description, ...props }: PromptArgs) {
export async function showPrompt({
id,
title,
description,
required = true,
...props
}: PromptArgs) {
return new Promise((resolve: PromptProps['onResult']) => {
showDialog({
id,
@@ -21,6 +27,7 @@ export async function showPrompt({ id, title, description, ...props }: PromptArg
},
render: ({ hide }) =>
Prompt({
required,
onCancel: () => {
// Click cancel button within dialog
resolve(null);

View File

@@ -11,6 +11,7 @@ export async function renameModelWithPrompt(model: Extract<AnyModel, { name: str
const name = await showPrompt({
id: 'rename-request',
title: 'Rename Request',
required: false,
description:
model.name === '' ? (
'Enter a new name'

View File

@@ -20,7 +20,13 @@ export function setWorkspaceSearchParams(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
search: (prev: any) => {
// console.log('Navigating to', { prev, search });
return { ...prev, ...search };
const o = { ...prev, ...search };
for (const k of Object.keys(o)) {
if (o[k] == null) {
delete o[k];
}
}
return o;
},
});
}