generalize convert-to-mp3 into convert-audio with selectable target format

- rename task to convert-audio, support mp3/flac/wav/ogg/aac/opus targets
- add options input type with selectable pill buttons in UI
- extend seed script parser to handle YAML list properties (options)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 16:42:44 +00:00
co-authored by Claude Opus 4.6
parent 729ffb930b
commit 2caccc183c
4 changed files with 91 additions and 14 deletions
+32 -4
View File
@@ -48,24 +48,52 @@ function parseFrontmatter(content: string) {
}
meta.trigger = triggers.length > 0 ? triggers : undefined;
// Parse inputs (simplified — store as-is for now)
// Parse inputs
const inputsMatch = yaml.match(/^inputs:\s*\n((?:[ \t]+.+\n?)*)/m);
if (inputsMatch) {
const inputBlock = inputsMatch[1]!;
const inputEntries: Record<string, Record<string, string>> = {};
const inputEntries: Record<string, Record<string, string | string[]>> = {};
let currentInput: string | null = null;
let currentListKey: string | null = null;
let currentList: string[] = [];
const flushList = () => {
if (currentInput && currentListKey && currentList.length > 0) {
inputEntries[currentInput]![currentListKey] = currentList;
}
currentListKey = null;
currentList = [];
};
for (const line of inputBlock.split('\n')) {
const topMatch = line.match(/^\s{2}(\w[\w_-]*):\s*$/);
if (topMatch) {
flushList();
currentInput = topMatch[1]!;
inputEntries[currentInput] = {};
continue;
}
const propMatch = line.match(/^\s{4}(\w[\w_-]*):\s*(.+)$/);
// List item (6 spaces + dash)
const listItemMatch = line.match(/^\s{6}-\s*(.+)$/);
if (listItemMatch && currentInput && currentListKey) {
currentList.push(listItemMatch[1]!.trim());
continue;
}
// Property with value or start of list
const propMatch = line.match(/^\s{4}(\w[\w_-]*):\s*(.*)$/);
if (propMatch && currentInput) {
inputEntries[currentInput]![propMatch[1]!] = propMatch[2]!.trim();
flushList();
const value = propMatch[2]!.trim();
if (value === '') {
// Start of a list (e.g. "options:")
currentListKey = propMatch[1]!;
currentList = [];
} else {
inputEntries[currentInput]![propMatch[1]!] = value;
}
}
}
flushList();
if (Object.keys(inputEntries).length > 0) {
meta.inputs = inputEntries;
}
@@ -198,6 +198,7 @@ type TaskInputDef = {
type: string;
description?: string;
default?: string;
options?: string[];
};
type TaskInputFormProps = {
@@ -239,6 +240,26 @@ const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys }: TaskInpu
);
}
if (def.options && def.options.length > 0) {
const selected = values[key] ?? def.default ?? def.options[0]!;
return (
<div key={key} className="flex flex-col gap-1.5">
<span className="text-sm font-medium text-duck-dark dark:text-foreground">{def.description ?? key}</span>
<div className="flex flex-wrap gap-1.5">
{def.options.map((opt) => (
<button
key={opt}
onClick={() => onChange(key, opt)}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors cursor-pointer ${selected === opt ? 'bg-duck-teal text-white' : 'bg-duck-dark/5 dark:bg-foreground/5 text-duck-dark/60 dark:text-foreground/60 hover:text-duck-dark dark:hover:text-foreground hover:bg-duck-dark/10 dark:hover:bg-foreground/10'}`}
>
{opt}
</button>
))}
</div>
</div>
);
}
// Default: text input for string/number
return (
<div key={key} className="flex flex-col gap-1">