feat: 添加触发词组件,支持动态保存和反馈机制

This commit is contained in:
Vixalie
2026-03-29 08:31:24 +08:00
parent 109f77014b
commit d5c1d2bdc2
3 changed files with 149 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...$$props}>
<path
fill="currentColor"
d="M10.91 15.242q-.168 0-.289-.11q-.121-.112-.121-.293V9.162q0-.182.124-.293t.288-.111q.042 0 .284.13l2.677 2.678q.093.092.143.199t.05.235t-.05.235t-.143.2l-2.677 2.677q-.055.055-.129.093q-.073.037-.157.037" />
</svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@@ -3,13 +3,15 @@ import DatasetName from './meta-form/DatasetName.svelte';
import SelectLoraType from './meta-form/SelectLoraType.svelte';
import SelectModel from './meta-form/SelectModel.svelte';
import TargetSize from './meta-form/TargetSize.svelte';
import TriggerWord from './meta-form/TriggerWord.svelte';
import UnifiedSizeChoice from './meta-form/UnifiedSizeChoice.svelte';
</script>
<div class="flex flex-col gap-2 px-4 py-4">
<div class="grow flex flex-col gap-2 px-4 py-4">
<DatasetName />
<SelectModel />
<SelectLoraType />
<UnifiedSizeChoice />
<TargetSize />
<TriggerWord />
</div>

View File

@@ -0,0 +1,141 @@
<script lang="ts">
import TriangleRight from '$lib/components/icons/TriangleRight.svelte';
import { activeDatasetMeta, updateActiveDatasetMeta } from '$lib/stores/dataset';
import {
createDebouncedTrigger,
createSaveFeedbackController,
type SaveFeedbackState,
} from '$lib/utils/form-save';
import { onDestroy, onMount, tick } from 'svelte';
import { get } from 'svelte/store';
let triggerWords = $state<string[]>([]);
let saveFeedback = $state<SaveFeedbackState>('idle');
let rowFeedback = $state<Record<number, SaveFeedbackState>>({});
let editingRowIndex: number | null = null;
const inputRefs: Array<HTMLInputElement | null> = [];
const rowFeedbackTimers = new Map<number, ReturnType<typeof setTimeout>>();
const feedback = createSaveFeedbackController((state) => {
saveFeedback = state;
});
onMount(() => {
triggerWords = get(activeDatasetMeta)?.triggerWords ?? [];
});
onDestroy(() => {
saveLater.cancel();
feedback.dispose();
for (const timer of rowFeedbackTimers.values()) {
clearTimeout(timer);
}
rowFeedbackTimers.clear();
});
function markRowFeedback(index: number, state: SaveFeedbackState) {
const previous = rowFeedbackTimers.get(index);
if (previous) {
clearTimeout(previous);
}
rowFeedback = { ...rowFeedback, [index]: state };
if (state !== 'idle') {
const timer = setTimeout(() => {
rowFeedback = { ...rowFeedback, [index]: 'idle' };
rowFeedbackTimers.delete(index);
}, 2000);
rowFeedbackTimers.set(index, timer);
}
}
function clearAllRowFeedback() {
for (const timer of rowFeedbackTimers.values()) {
clearTimeout(timer);
}
rowFeedbackTimers.clear();
rowFeedback = {};
}
async function persistTriggerWords() {
const nextTriggerWords = triggerWords.filter((word) => word.trim() !== '');
const removedEmptyRows = nextTriggerWords.length !== triggerWords.length;
if (removedEmptyRows) {
triggerWords = nextTriggerWords;
clearAllRowFeedback();
editingRowIndex = null;
}
try {
await updateActiveDatasetMeta({ triggerWords: [...nextTriggerWords] });
feedback.markUpdated();
if (editingRowIndex !== null) {
markRowFeedback(editingRowIndex, 'updated');
}
} catch (error) {
console.error('Failed to save trigger words:', error);
feedback.markNotUpdated();
if (editingRowIndex !== null) {
markRowFeedback(editingRowIndex, 'not-updated');
}
}
}
const saveLater = createDebouncedTrigger(() => {
void persistTriggerWords();
}, 3000);
function scheduleSave(index: number | null) {
editingRowIndex = index;
saveLater.trigger();
}
function onWordInput(index: number, event: Event) {
const target = event.currentTarget as HTMLInputElement;
triggerWords = triggerWords.map((word, i) => (i === index ? target.value : word));
scheduleSave(index);
}
async function addTriggerWord() {
const nextIndex = triggerWords.length;
triggerWords = [...triggerWords, ''];
scheduleSave(nextIndex);
await tick();
inputRefs[nextIndex]?.focus();
}
</script>
<fieldset
class={[
'fieldset transition-colors duration-300 ease-out',
saveFeedback === 'updated' && 'text-green-600',
saveFeedback === 'not-updated' && 'text-red-600',
]}>
<legend class="fieldset-legend">Trigger Word</legend>
<ul class="list overflow-y-auto">
{#each triggerWords as word, index}
<li class="list-row py-1 gap-0">
<div class="flex justify-center"><TriangleRight width="20" /></div>
<input
type="text"
class={[
'input input-ghost input-sm list-col-grow w-full focus:outline-none transition-colors duration-300 ease-out',
rowFeedback[index] === 'updated' && 'text-green-600',
rowFeedback[index] === 'not-updated' && 'text-red-600',
]}
bind:value={triggerWords[index]}
bind:this={inputRefs[index]}
oninput={(event) => onWordInput(index, event)} />
</li>
{/each}
</ul>
<div class="flex flex-row gap-2">
<button type="button" class="btn btn-sm self-auto" onclick={addTriggerWord}
>Add Trigger Word</button>
</div>
</fieldset>