diff --git a/agentex-ui/components/primary-content/prompt-input.tsx b/agentex-ui/components/primary-content/prompt-input.tsx index 5d872c4..aab272d 100644 --- a/agentex-ui/components/primary-content/prompt-input.tsx +++ b/agentex-ui/components/primary-content/prompt-input.tsx @@ -11,6 +11,7 @@ import { DataContent, TextContent } from 'agentex/resources'; import { ArrowUp } from 'lucide-react'; import { useAgentexClient } from '@/components/providers'; +import { Button } from '@/components/ui/button'; import { IconButton } from '@/components/ui/icon-button'; import { Switch } from '@/components/ui/switch'; import { toast } from '@/components/ui/toast'; @@ -21,6 +22,7 @@ import { } from '@/hooks/use-safe-search-params'; import { useSendMessage } from '@/hooks/use-task-messages'; import { useTask } from '@/hooks/use-tasks'; +import { parseOptionalJsonObject } from '@/lib/json-utils'; import { TaskStatusEnum } from '@/lib/types'; type PromptInputProps = { @@ -49,6 +51,8 @@ export function PromptInput({ prompt, setPrompt }: PromptInputProps) { const { taskID, agentName, updateParams } = useSafeSearchParams(); const [isClient, setIsClient] = useState(false); const [isSendingJSON, setIsSendingJSON] = useState(false); + const [isTaskParamsEnabled, setIsTaskParamsEnabled] = useState(false); + const [taskParamsPrompt, setTaskParamsPrompt] = useState(''); const { agentexClient } = useAgentexClient(); @@ -58,6 +62,7 @@ export function PromptInput({ prompt, setPrompt }: PromptInputProps) { const textInputRef = useRef(null); const codeMirrorViewRef = useRef(null); + const taskParamsCodeMirrorViewRef = useRef(null); const isTaskTerminal = useMemo(() => { if (!taskID || !task) return false; @@ -116,18 +121,34 @@ export function PromptInput({ prompt, setPrompt }: PromptInputProps) { } } + let taskParams: Record | undefined; + if (!currentTaskId && isTaskParamsEnabled) { + try { + taskParams = parseOptionalJsonObject(taskParamsPrompt); + } catch (error) { + toast.error({ + title: 'Invalid task creation params', + message: + error instanceof Error ? error.message : 'Invalid JSON object', + }); + return; + } + } + setPrompt(''); if (!currentTaskId) { const task = await createTaskMutation.mutateAsync({ agentName: agentName, - params: { + params: taskParams ?? { description: prompt, content: currentPrompt, }, }); currentTaskId = task.id; updateParams({ [SearchParamKey.TASK_ID]: currentTaskId }); + setIsTaskParamsEnabled(false); + setTaskParamsPrompt(''); } const content: TextContent | DataContent = isSendingJSON @@ -159,10 +180,22 @@ export function PromptInput({ prompt, setPrompt }: PromptInputProps) { sendMessageMutation, setPrompt, isSendingJSON, + isTaskParamsEnabled, + taskParamsPrompt, ]); return (
+ {!taskID && ( + + )}
@@ -211,6 +244,100 @@ export function PromptInput({ prompt, setPrompt }: PromptInputProps) { ); } +const TaskCreationParamsEditor = ({ + value, + setValue, + isEnabled, + setIsEnabled, + isDisabled, + codeMirrorViewRef, +}: { + value: string; + setValue: (value: string) => void; + isEnabled: boolean; + setIsEnabled: (isEnabled: boolean) => void; + isDisabled: boolean; + codeMirrorViewRef: React.MutableRefObject; +}) => { + const handleAdd = useCallback(() => { + setIsEnabled(true); + }, [setIsEnabled]); + + const handleRemove = useCallback(() => { + setIsEnabled(false); + setValue(''); + }, [setIsEnabled, setValue]); + + useEffect(() => { + if (!isEnabled) return; + + requestAnimationFrame(() => { + codeMirrorViewRef.current?.focus(); + }); + }, [codeMirrorViewRef, isEnabled]); + + return ( +
+ {isEnabled ? ( +
+
+ Task creation params + +
+ setValue(nextValue)} + onCreateEditor={view => { + codeMirrorViewRef.current = view; + }} + extensions={[json(), noOutlineTheme, closeBrackets()]} + placeholder='{ "container_id": "..." }' + basicSetup={{ + lineNumbers: false, + foldGutter: false, + highlightActiveLineGutter: false, + highlightActiveLine: false, + }} + editable={!isDisabled} + theme="none" + maxHeight="180px" + /> +

+ Optional JSON object sent only when this GUI starts a new task. +

+
+ ) : ( + + )} +
+ ); +}; + const TextInput = ({ prompt, setPrompt, diff --git a/agentex-ui/lib/json-utils.test.ts b/agentex-ui/lib/json-utils.test.ts index 0256692..0676b12 100644 --- a/agentex-ui/lib/json-utils.test.ts +++ b/agentex-ui/lib/json-utils.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { serializeValue } from '@/lib/json-utils'; +import { parseOptionalJsonObject, serializeValue } from '@/lib/json-utils'; import type { JsonValue } from '@/lib/types'; describe('serializeValue', () => { @@ -77,3 +77,34 @@ describe('serializeValue', () => { expect(result).toBe('0'); }); }); + +describe('parseOptionalJsonObject', () => { + it('returns undefined for empty input', () => { + expect(parseOptionalJsonObject('')).toBeUndefined(); + expect(parseOptionalJsonObject(' ')).toBeUndefined(); + }); + + it('parses a valid JSON object', () => { + expect(parseOptionalJsonObject('{ "container_id": "abc123" }')).toEqual({ + container_id: 'abc123', + }); + }); + + it('rejects invalid JSON', () => { + expect(() => parseOptionalJsonObject('{ bad json }')).toThrow( + 'Invalid JSON' + ); + }); + + it('rejects arrays', () => { + expect(() => parseOptionalJsonObject('[]')).toThrow( + 'Expected a JSON object' + ); + }); + + it('rejects null', () => { + expect(() => parseOptionalJsonObject('null')).toThrow( + 'Expected a JSON object' + ); + }); +}); diff --git a/agentex-ui/lib/json-utils.ts b/agentex-ui/lib/json-utils.ts index ee17338..d4b08e3 100644 --- a/agentex-ui/lib/json-utils.ts +++ b/agentex-ui/lib/json-utils.ts @@ -1,5 +1,7 @@ import type { JsonValue } from '@/lib/types'; +export type JsonObject = { [key: string]: JsonValue }; + export function serializeValue(data: JsonValue): string { if (typeof data === 'object' && data !== null) { return JSON.stringify(data, null, 2); @@ -9,3 +11,30 @@ export function serializeValue(data: JsonValue): string { } return String(data); } + +export function parseJsonObject(input: string): JsonObject { + let parsedValue: unknown; + try { + parsedValue = JSON.parse(input) as unknown; + } catch (error) { + throw new Error('Invalid JSON', { cause: error }); + } + + if ( + parsedValue === null || + typeof parsedValue !== 'object' || + Array.isArray(parsedValue) + ) { + throw new Error('Expected a JSON object'); + } + + return parsedValue as JsonObject; +} + +export function parseOptionalJsonObject(input: string): JsonObject | undefined { + if (!input.trim()) { + return undefined; + } + + return parseJsonObject(input); +}