feat: 添加触发词组件,支持动态保存和反馈机制
This commit is contained in:
5
src/lib/components/icons/TriangleRight.svelte
Normal file
5
src/lib/components/icons/TriangleRight.svelte
Normal 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 |
@@ -3,13 +3,15 @@ import DatasetName from './meta-form/DatasetName.svelte';
|
|||||||
import SelectLoraType from './meta-form/SelectLoraType.svelte';
|
import SelectLoraType from './meta-form/SelectLoraType.svelte';
|
||||||
import SelectModel from './meta-form/SelectModel.svelte';
|
import SelectModel from './meta-form/SelectModel.svelte';
|
||||||
import TargetSize from './meta-form/TargetSize.svelte';
|
import TargetSize from './meta-form/TargetSize.svelte';
|
||||||
|
import TriggerWord from './meta-form/TriggerWord.svelte';
|
||||||
import UnifiedSizeChoice from './meta-form/UnifiedSizeChoice.svelte';
|
import UnifiedSizeChoice from './meta-form/UnifiedSizeChoice.svelte';
|
||||||
</script>
|
</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 />
|
<DatasetName />
|
||||||
<SelectModel />
|
<SelectModel />
|
||||||
<SelectLoraType />
|
<SelectLoraType />
|
||||||
<UnifiedSizeChoice />
|
<UnifiedSizeChoice />
|
||||||
<TargetSize />
|
<TargetSize />
|
||||||
|
<TriggerWord />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
141
src/routes/dataset/meta-form/TriggerWord.svelte
Normal file
141
src/routes/dataset/meta-form/TriggerWord.svelte
Normal 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>
|
||||||
Reference in New Issue
Block a user