add task input form and improve script runner output
- fetch task input definitions from API and render configurable inputs - boolean inputs render as No/Yes toggle (e.g. delete_source) - auto-filled inputs (file_path) are hidden from the form - wrap script output in dark pre/code block with copy button - fix ffmpeg -nostdin for batch directory processing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,10 @@ inputs:
|
||||
file_path:
|
||||
type: string
|
||||
description: Path to an audio file or directory to convert.
|
||||
delete_source:
|
||||
type: boolean
|
||||
description: Delete source files after successful conversion.
|
||||
default: false
|
||||
args: [file_path]
|
||||
---
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ set -euo pipefail
|
||||
# ── Task: Convert To MP3 ──
|
||||
|
||||
EXTENSIONS="flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff"
|
||||
DELETE_SOURCE="${INPUT_DELETE_SOURCE:-false}"
|
||||
|
||||
process_file() {
|
||||
local input="$1"
|
||||
@@ -25,8 +26,12 @@ process_file() {
|
||||
fi
|
||||
|
||||
echo "Converting: $input → $output"
|
||||
if ffmpeg -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null; then
|
||||
if ffmpeg -nostdin -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null; then
|
||||
echo " Done"
|
||||
if [[ "$DELETE_SOURCE" == "true" ]]; then
|
||||
rm "$input"
|
||||
echo " Deleted source"
|
||||
fi
|
||||
else
|
||||
echo " Failed" >&2
|
||||
return 1
|
||||
|
||||
+128
-10
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { X, Play, Square, CircleCheck, CircleX } from 'lucide-react';
|
||||
import { X, Play, Square, CircleCheck, CircleX, Copy, Check } from 'lucide-react';
|
||||
import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { cardStyle } from '@/components/Card';
|
||||
@@ -7,6 +7,7 @@ import type { TaskInfo, ChatMessage } from '../../../Chat';
|
||||
import { usePiChat, MessageBubble, StreamingBubble, ModelSelector } from '../../../Chat';
|
||||
import { useSettings } from 'state/useSettings';
|
||||
import { useUserVisibleModels } from 'state/useModels';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import type { TaskSummary } from '../../useTasks';
|
||||
import { useTaskRunner } from './useTaskRunner';
|
||||
|
||||
@@ -191,17 +192,104 @@ const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo, sandboxed }: P
|
||||
);
|
||||
};
|
||||
|
||||
// ── Task input definitions ──
|
||||
|
||||
type TaskInputDef = {
|
||||
type: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
};
|
||||
|
||||
type TaskInputFormProps = {
|
||||
inputDefs: Record<string, TaskInputDef>;
|
||||
values: Record<string, string>;
|
||||
onChange: (key: string, value: string) => void;
|
||||
autoFilledKeys: Set<string>;
|
||||
};
|
||||
|
||||
const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys }: TaskInputFormProps) => {
|
||||
const configurableInputs = Object.entries(inputDefs).filter(([key]) => !autoFilledKeys.has(key));
|
||||
if (configurableInputs.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="px-5 py-3 border-b border-duck-dark/10 flex flex-col gap-3">
|
||||
{configurableInputs.map(([key, def]) => {
|
||||
if (def.type === 'boolean') {
|
||||
const isTrue = values[key] === 'true';
|
||||
return (
|
||||
<div key={key} className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground">{def.description ?? key}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0 bg-duck-dark/5 dark:bg-foreground/5 rounded-lg p-0.5">
|
||||
<button
|
||||
onClick={() => onChange(key, 'false')}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors cursor-pointer ${!isTrue ? 'bg-background shadow-sm text-duck-dark dark:text-foreground' : 'text-duck-dark/50 dark:text-foreground/50 hover:text-duck-dark dark:hover:text-foreground'}`}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onChange(key, 'true')}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors cursor-pointer ${isTrue ? 'bg-background shadow-sm text-duck-dark dark:text-foreground' : 'text-duck-dark/50 dark:text-foreground/50 hover:text-duck-dark dark:hover:text-foreground'}`}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: text input for string/number
|
||||
return (
|
||||
<div key={key} className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-duck-dark/70 dark:text-foreground/70">{def.description ?? key}</label>
|
||||
<input
|
||||
type={def.type === 'number' ? 'number' : 'text'}
|
||||
value={values[key] ?? ''}
|
||||
onChange={(e) => onChange(key, e.target.value)}
|
||||
className="px-3 py-1.5 text-sm rounded-lg border border-duck-dark/15 dark:border-foreground/15 bg-background focus:outline-none focus:ring-1 focus:ring-duck-teal"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Script-mode runner ──
|
||||
|
||||
type ScriptRunnerProps = {
|
||||
taskDirName: string;
|
||||
inputs: Record<string, string>;
|
||||
autoInputs: Record<string, string>;
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
const ScriptRunner = ({ taskDirName, inputs, cwd }: ScriptRunnerProps) => {
|
||||
const ScriptRunner = ({ taskDirName, autoInputs, cwd }: ScriptRunnerProps) => {
|
||||
const runner = useTaskRunner();
|
||||
const client = useClient();
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null);
|
||||
const [inputDefs, setInputDefs] = useState<Record<string, TaskInputDef> | null>(null);
|
||||
const [formValues, setFormValues] = useState<Record<string, string>>({});
|
||||
|
||||
// Fetch task detail to get input definitions
|
||||
useEffect(() => {
|
||||
client.get<{ inputs?: Record<string, TaskInputDef> }>(`/tasks/${taskDirName}`).then((task) => {
|
||||
const defs = task.inputs ?? {};
|
||||
setInputDefs(defs);
|
||||
// Initialize form values from defaults
|
||||
const defaults: Record<string, string> = {};
|
||||
for (const [key, def] of Object.entries(defs)) {
|
||||
if (def.default !== undefined) defaults[key] = def.default;
|
||||
}
|
||||
setFormValues(defaults);
|
||||
});
|
||||
}, [taskDirName]);
|
||||
|
||||
const autoFilledKeys = new Set(Object.keys(autoInputs));
|
||||
|
||||
const handleInputChange = (key: string, value: string) => {
|
||||
setFormValues((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
// Auto-scroll
|
||||
useEffect(() => {
|
||||
@@ -217,29 +305,57 @@ const ScriptRunner = ({ taskDirName, inputs, cwd }: ScriptRunnerProps) => {
|
||||
prevPhaseRef.current = runner.phase;
|
||||
}, [runner.phase]);
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const handleCopy = () => {
|
||||
const text = runner.output.map((line) => line.text).join('');
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const handleRun = () => {
|
||||
runner.run(taskDirName, inputs, cwd);
|
||||
const allInputs = { ...formValues, ...autoInputs };
|
||||
runner.run(taskDirName, allInputs, cwd);
|
||||
};
|
||||
|
||||
if (runner.phase === 'ready') {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col">
|
||||
{inputDefs && (
|
||||
<TaskInputForm
|
||||
inputDefs={inputDefs}
|
||||
values={formValues}
|
||||
onChange={handleInputChange}
|
||||
autoFilledKeys={autoFilledKeys}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<button
|
||||
onClick={handleRun}
|
||||
disabled={!runner.isConnected}
|
||||
disabled={!runner.isConnected || !inputDefs}
|
||||
className="flex items-center gap-2 px-6 py-2.5 rounded-lg bg-duck-teal text-white font-medium text-sm hover:bg-duck-teal/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
Run
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto px-4 py-3">
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap break-words">
|
||||
<div className="relative group">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 p-1.5 rounded-md bg-white/10 text-white/40 hover:text-white hover:bg-white/20 transition-colors opacity-0 group-hover:opacity-100 cursor-pointer z-10"
|
||||
title="Copy output"
|
||||
>
|
||||
{copied ? <Check className="h-3.5 w-3.5 text-green-400" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap break-words bg-[#0d1117] text-gray-200 rounded-lg p-4 pr-10">
|
||||
<code>
|
||||
{runner.output.map((line, i) => (
|
||||
<span
|
||||
key={i}
|
||||
@@ -254,7 +370,9 @@ const ScriptRunner = ({ taskDirName, inputs, cwd }: ScriptRunnerProps) => {
|
||||
{line.text}
|
||||
</span>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
<div className="shrink-0 flex justify-center py-3 border-t border-duck-dark/10">
|
||||
@@ -308,9 +426,9 @@ export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFull
|
||||
: `Execute the task "${task.name}" (${task.dirName})`);
|
||||
const taskInfo: TaskInfo = { taskName: task.name, taskDirName: task.dirName, entryName: entryName ?? '', entryType: entryType ?? 'file' };
|
||||
|
||||
// Script mode inputs — for now, map the entry path to file_path
|
||||
const scriptInputs: Record<string, string> = {};
|
||||
if (entryFullPath) scriptInputs.file_path = entryFullPath;
|
||||
// Script mode: auto-filled inputs from context (e.g. file_path from file browser)
|
||||
const autoInputs: Record<string, string> = {};
|
||||
if (entryFullPath) autoInputs.file_path = entryFullPath;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -342,7 +460,7 @@ export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFull
|
||||
<ScriptRunner
|
||||
key="script"
|
||||
taskDirName={task.dirName}
|
||||
inputs={scriptInputs}
|
||||
autoInputs={autoInputs}
|
||||
cwd={cwd.path || undefined}
|
||||
/>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user