diff --git a/apps/whispering/src/lib/commands.browser.ts b/apps/whispering/src/lib/commands.browser.ts index 900ee5f8e7..be1232acae 100644 --- a/apps/whispering/src/lib/commands.browser.ts +++ b/apps/whispering/src/lib/commands.browser.ts @@ -1,10 +1,10 @@ import type { SatisfiedCommand } from '$lib/commands'; /** - * Browser builds contribute no extra commands. The transformation picker is + * Browser builds contribute no extra commands. The recipe picker is * desktop-only: it captures the selection in another app via a simulated system * copy and opens a separate Tauri window, neither of which a browser tab can do. - * The cross-platform `Run transformation on clipboard` command covers the web - * transformation path. See `commands.tauri.ts` for the desktop addition. + * The cross-platform `Run recipe on clipboard` command covers the web recipe + * path. See `commands.tauri.ts` for the desktop addition. */ export const platformCommands = [] as const satisfies SatisfiedCommand[]; diff --git a/apps/whispering/src/lib/commands.tauri.ts b/apps/whispering/src/lib/commands.tauri.ts index e5051553d0..01048b2800 100644 --- a/apps/whispering/src/lib/commands.tauri.ts +++ b/apps/whispering/src/lib/commands.tauri.ts @@ -1,5 +1,5 @@ import type { SatisfiedCommand, ShortcutEventState } from '$lib/commands'; -import { openTransformationPicker } from '$lib/operations/transformation-picker'; +import { openRecipePicker } from '$lib/operations/recipe-picker'; /** * Desktop-only commands, spread into the registry by the `#platform/commands` @@ -9,8 +9,8 @@ import { openTransformationPicker } from '$lib/operations/transformation-picker' */ export const platformCommands = [ { - id: 'openTransformationPicker', - title: 'Open transformation picker', + id: 'openRecipePicker', + title: 'Open recipe picker', // Fire on release, not press: the global accelerator carries a Cmd/Ctrl+Shift // chord, and capturing on press synthesizes Cmd/Ctrl+C while that chord is // still held, so the foreground app sees Cmd+Shift+C instead of a clean copy. @@ -19,8 +19,7 @@ export const platformCommands = [ // in-app shortcut would never fire. The callback guard runs once, on release. on: ['Pressed', 'Released'], callback: (state?: ShortcutEventState) => { - if (state === 'Released' || state === undefined) - openTransformationPicker(); + if (state === 'Released' || state === undefined) openRecipePicker(); }, }, ] as const satisfies SatisfiedCommand[]; diff --git a/apps/whispering/src/lib/commands.ts b/apps/whispering/src/lib/commands.ts index a42beb336b..8d27ae8125 100644 --- a/apps/whispering/src/lib/commands.ts +++ b/apps/whispering/src/lib/commands.ts @@ -1,4 +1,5 @@ import { platformCommands } from '#platform/commands'; +import { runRecipeOnClipboard } from '$lib/operations/recipe-clipboard'; import { cancelRecording, startManualRecording, @@ -6,7 +7,6 @@ import { toggleManualRecording, toggleVadRecording, } from '$lib/operations/recording'; -import { runTransformationOnClipboard } from '$lib/operations/transformation-clipboard'; /** * Registry of available commands in the application. @@ -18,9 +18,9 @@ import { runTransformationOnClipboard } from '$lib/operations/transformation-cli * command registry. * * Platform split: `sharedCommands` exist in every build. Desktop-only commands - * (the transformation picker, which captures a selection from another app and - * opens a Tauri window) come from the `#platform/commands` seam, so a browser - * build never imports their Tauri-only code and never offers them as shortcuts. + * (the recipe picker, which captures a selection from another app and opens a + * Tauri window) come from the `#platform/commands` seam, so a browser build + * never imports their Tauri-only code and never offers them as shortcuts. */ /** @@ -84,10 +84,10 @@ const sharedCommands = [ callback: () => toggleVadRecording(), }, { - id: 'runTransformationOnClipboard', - title: 'Run transformation on clipboard', + id: 'runRecipeOnClipboard', + title: 'Run recipe on clipboard', on: ['Pressed'], - callback: () => runTransformationOnClipboard(), + callback: () => runRecipeOnClipboard(), }, ] as const satisfies SatisfiedCommand[]; diff --git a/apps/whispering/src/lib/components/CandidateCards.svelte b/apps/whispering/src/lib/components/CandidateCards.svelte deleted file mode 100644 index 0552240299..0000000000 --- a/apps/whispering/src/lib/components/CandidateCards.svelte +++ /dev/null @@ -1,131 +0,0 @@ - - -{#snippet diffInline(segments: DiffSegment[])} -

- {#each segments as seg, i (i)} - {#if seg.type === 'equal'}{seg.text}{:else if seg.type === 'insert'}{seg.text}{:else}{seg.text}{/if} - {/each} -

-{/snippet} - -
-
- - Diff - -
- -
- {#each candidates as candidate, index (candidate.id)} - {@const selected = index === selectedIndex} - (selectedIndex = index)} - ondblclick={() => { - selectedIndex = index; - onaccept(); - }} - class={cn( - 'cursor-pointer gap-2 border py-3 transition-colors outline-none', - selected - ? 'border-l-2 border-l-foreground/40 bg-accent shadow-sm' - : 'bg-card hover:bg-accent/40', - )} - > - - - {candidate.transformation.title || 'Untitled transformation'} - - {#if selected} - - Enter to accept - - {/if} - - - {#await candidate.result} -
- - Generating -
- {:then result} - {#if result.error} -

{result.error.message}

- {:else if showDiff} - {@render diffInline(wordDiff(original, result.data))} - {:else} -

- {result.data} -

- {/if} - {/await} -
-
- {/each} -
-
diff --git a/apps/whispering/src/lib/components/RecipePicker.svelte b/apps/whispering/src/lib/components/RecipePicker.svelte new file mode 100644 index 0000000000..66bfe084ad --- /dev/null +++ b/apps/whispering/src/lib/components/RecipePicker.svelte @@ -0,0 +1,74 @@ + + + recipePicker.isOpen, (open) => { if (!open) recipePicker.close(); } + } +> + + Run a recipe + + Pick a recipe to run on your captured text. + + + + + No recipes found. + + {#each recipes.pickable as recipe (recipe.id)} + run(recipe)}> + {#if recipe.icon} + + {/if} + {recipe.name} + {#if isBuiltinRecipeId(recipe.id)} + Built-in + {/if} + + {/each} + + + + + diff --git a/apps/whispering/src/lib/components/TransformationPickerBody.svelte b/apps/whispering/src/lib/components/TransformationPickerBody.svelte deleted file mode 100644 index fbe540b1d1..0000000000 --- a/apps/whispering/src/lib/components/TransformationPickerBody.svelte +++ /dev/null @@ -1,112 +0,0 @@ - - -{#snippet renderTransformationIdTitle(transformation: Transformation)} -
- - {transformation.id} - - {transformation.title} -
-{/snippet} - - - - No transformation found. - - {#each sortedTransformations as transformation, index (transformation.id)} - onSelect(transformation)} - class="flex items-center justify-between gap-2 p-2" - > -
- {@render renderTransformationIdTitle(transformation)} - {#if transformation.description} - - {transformation.description} - - {/if} -
- {#if index < 10} - - {modifierKey}{index === 9 ? '0' : index + 1} - - {/if} -
- {/each} -
- - - Manage transformations - -
diff --git a/apps/whispering/src/lib/components/copyable/TextPreviewDialog.svelte b/apps/whispering/src/lib/components/copyable/TextPreviewDialog.svelte index a13552f50a..fd9d420f62 100644 --- a/apps/whispering/src/lib/components/copyable/TextPreviewDialog.svelte +++ b/apps/whispering/src/lib/components/copyable/TextPreviewDialog.svelte @@ -6,6 +6,7 @@ import { Spinner } from '@epicenter/ui/spinner'; import { Textarea } from '@epicenter/ui/textarea'; import TrashIcon from '@lucide/svelte/icons/trash'; + import type { Snippet } from 'svelte'; import { createCopyFn } from '$lib/utils/createCopyFn'; /** @@ -66,6 +67,8 @@ loading = false, /** Optional callback to delete the associated item. When provided, a delete button appears in the dialog footer. */ onDelete, + /** Optional extra controls rendered on the left of the dialog footer (e.g. a view toggle). */ + actions, }: { id: string; title: string; @@ -75,6 +78,7 @@ disabled?: boolean; loading?: boolean; onDelete?: () => void; + actions?: Snippet; } = $props(); let isDialogOpen = $state(false); @@ -126,6 +130,7 @@ Delete {/if} + {@render actions?.()}
+{/snippet} diff --git a/apps/whispering/src/lib/components/settings/ProviderConfigFields.svelte b/apps/whispering/src/lib/components/settings/ProviderConfigFields.svelte index 2e34872e69..c1580546c9 100644 --- a/apps/whispering/src/lib/components/settings/ProviderConfigFields.svelte +++ b/apps/whispering/src/lib/components/settings/ProviderConfigFields.svelte @@ -193,7 +193,7 @@ placeholder: 'e.g. http://localhost:11434/v1', configKey: 'providers.custom.endpoint', description: [ - 'URL for OpenAI-compatible endpoints (Ollama, LM Studio, llama.cpp, etc.). Every transformation that uses the Custom provider calls this endpoint.', + 'URL for OpenAI-compatible endpoints (Ollama, LM Studio, llama.cpp, etc.). Every AI pass (Polish and recipes) that uses the Custom provider calls this endpoint.', ], }, { diff --git a/apps/whispering/src/lib/components/settings/README.md b/apps/whispering/src/lib/components/settings/README.md index 74a5a425f0..7a9d3b4a71 100644 --- a/apps/whispering/src/lib/components/settings/README.md +++ b/apps/whispering/src/lib/components/settings/README.md @@ -33,7 +33,6 @@ settings/ ├── selectors/ # Various selector components │ ├── ManualDeviceSelector.svelte │ ├── VadDeviceSelector.svelte -│ ├── TransformationSelector.svelte │ ├── TranscriptionSelector.svelte │ └── CaptureSurfaceSelector.svelte ├── LocalModelSelector.svelte diff --git a/apps/whispering/src/lib/components/settings/TranscriptionRuntimeConfig.svelte b/apps/whispering/src/lib/components/settings/TranscriptionRuntimeConfig.svelte index d8f6f415fb..736d30c2fc 100644 --- a/apps/whispering/src/lib/components/settings/TranscriptionRuntimeConfig.svelte +++ b/apps/whispering/src/lib/components/settings/TranscriptionRuntimeConfig.svelte @@ -486,7 +486,7 @@ /> {currentServiceCapabilities.supportsPrompt - ? 'Helps services that support prompts recognize specific terms, names, or context during transcription. For rewriting or translation, use Transformations.' + ? 'Helps services that support prompts recognize specific terms, names, or context during transcription. For rewriting or translation, use recipes.' : 'This transcription service does not support prompts.'} diff --git a/apps/whispering/src/lib/components/settings/index.ts b/apps/whispering/src/lib/components/settings/index.ts index 9d52597e4f..59af6fa9f7 100644 --- a/apps/whispering/src/lib/components/settings/index.ts +++ b/apps/whispering/src/lib/components/settings/index.ts @@ -11,6 +11,5 @@ export { default as SettingSwitch } from './SettingSwitch.svelte'; export { default as CaptureSurfaceSelector } from './selectors/CaptureSurfaceSelector.svelte'; export { default as ManualDeviceSelector } from './selectors/ManualDeviceSelector.svelte'; export { default as TranscriptionSelector } from './selectors/TranscriptionSelector.svelte'; -export { default as TransformationSelector } from './selectors/TransformationSelector.svelte'; export { default as VadDeviceSelector } from './selectors/VadDeviceSelector.svelte'; export { default as TranscriptionRuntimeConfig } from './TranscriptionRuntimeConfig.svelte'; diff --git a/apps/whispering/src/lib/components/settings/selectors/TranscriptionSelector.svelte b/apps/whispering/src/lib/components/settings/selectors/TranscriptionSelector.svelte index 035313aa64..dfced2f546 100644 --- a/apps/whispering/src/lib/components/settings/selectors/TranscriptionSelector.svelte +++ b/apps/whispering/src/lib/components/settings/selectors/TranscriptionSelector.svelte @@ -62,7 +62,7 @@ ); // The pipeline pill already shows the model name, so its tooltip describes the - // action (parallel with the mic and transformation triggers) rather than + // action (parallel with the mic and post-processing triggers) rather than // echoing the visible value. The standalone switcher keeps the value, since // there it is the brand icon, not text, that is on screen. const triggerTooltip = $derived.by(() => { diff --git a/apps/whispering/src/lib/components/settings/selectors/TransformationSelector.svelte b/apps/whispering/src/lib/components/settings/selectors/TransformationSelector.svelte deleted file mode 100644 index a6e56e63d4..0000000000 --- a/apps/whispering/src/lib/components/settings/selectors/TransformationSelector.svelte +++ /dev/null @@ -1,137 +0,0 @@ - - -{#snippet renderTransformationIdTitle(transformation: Transformation)} -
- - {transformation.id} - - {transformation.title} -
-{/snippet} - - - - {#snippet child({ props })} - - {/snippet} - - - - - No transformation found. - - {#each sortedTransformations as transformation (transformation.id)} - {@const isSelectedTransformation = - settings.get('transformation.selectedId') === - transformation.id} - { - settings.set( - 'transformation.selectedId', - settings.get('transformation.selectedId') === - transformation.id - ? null - : transformation.id, - ); - combobox.closeAndFocusTrigger(); - }} - class="flex items-center gap-2 p-2" - > - -
- {@render renderTransformationIdTitle(transformation)} - {#if transformation.description} - - {transformation.description} - - {/if} -
-
- {/each} -
- { - goto('/transformations'); - combobox.closeAndFocusTrigger(); - }} - class="rounded-none p-2 bg-muted/50 text-muted-foreground" - > - - Manage transformations - -
-
-
diff --git a/apps/whispering/src/lib/components/transformations-editor/Configuration.svelte b/apps/whispering/src/lib/components/transformations-editor/Configuration.svelte deleted file mode 100644 index 175d0c1560..0000000000 --- a/apps/whispering/src/lib/components/transformations-editor/Configuration.svelte +++ /dev/null @@ -1,400 +0,0 @@ - - -{#snippet replacementSection( - phase: ReplacementPhase, - heading: string, - help: string, -)} - - {heading} - {help} - -
- {#each transformation[phase] as replacement, index (index)} - -
-
-
- - Find - - updateReplacement(phase, index, { - find: e.currentTarget.value, - })} - placeholder="Text or pattern to search for" - /> - - - Replace - - updateReplacement(phase, index, { - replace: e.currentTarget.value, - })} - placeholder="Text to use as the replacement" - /> - -
- -
- - - updateReplacement(phase, index, { useRegex: v })} - /> - - Use regex - - Match with a regular expression instead of plain text. - - - -
- {/each} -
- - -
-{/snippet} - -
- - Configuration - - A transformation applies deterministic find/replace and, optionally, sends - the text through one AI model. With the prompt on, replacements run both - before and after it. - - - - - -
- - Title - { - transformation = { - ...transformation, - title: e.currentTarget.value, - }; - }} - placeholder="e.g., Format Meeting Notes" - /> - - A clear, concise name that describes what this transformation does. - - - - Description -