初始化
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { loadInitialDataset } from '../utils/yamlLoader';
|
||||
import type { PromptDataset, PromptCategory, PromptGroup, PromptTag, LangCode, ExportBundle, CustomDiff, PromptPreset } from '../types';
|
||||
|
||||
const LS_KEY = 'ops.prompt.dataset.v1';
|
||||
let saveTimer: number | null = null; // 非响应式计时器,避免递归更新
|
||||
let baseline: PromptDataset | null = null; // 基线词库(从 public/sd 加载)
|
||||
|
||||
function deepClone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
export const usePromptStore = defineStore('promptStore', {
|
||||
state: () => ({
|
||||
dataset: null as PromptDataset | null,
|
||||
selectedLang: 'zh_CN' as LangCode,
|
||||
selectedCategoryIndex: 0,
|
||||
selectedGroupIndex: 0,
|
||||
searchQuery: '',
|
||||
// 编辑器相关
|
||||
promptText: '',
|
||||
presets: [] as PromptPreset[],
|
||||
}),
|
||||
getters: {
|
||||
categories: (s) => s.dataset?.categories ?? [],
|
||||
currentCategory: (s) => s.dataset?.categories[s.selectedCategoryIndex] ?? null,
|
||||
currentGroup(): PromptGroup | null {
|
||||
return this.currentCategory?.groups[this.selectedGroupIndex] ?? null;
|
||||
},
|
||||
languages: (s) => s.dataset?.languages ?? ['en'],
|
||||
filteredTags(): PromptTag[] {
|
||||
const grp = this.currentGroup;
|
||||
if (!grp) return [] as PromptTag[];
|
||||
const q = this.searchQuery.trim().toLowerCase();
|
||||
if (!q) return grp.tags;
|
||||
return grp.tags.filter((t) => {
|
||||
const trans = t.translation?.[this.selectedLang] ?? '';
|
||||
return (
|
||||
t.key.toLowerCase().includes(q) ||
|
||||
trans.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
},
|
||||
tokens(s): string[] {
|
||||
return splitTokens(s.promptText);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async initialize() {
|
||||
// 先加载基线词库
|
||||
baseline = await loadInitialDataset();
|
||||
const raw = localStorage.getItem(LS_KEY);
|
||||
if (raw) {
|
||||
try {
|
||||
const bundle = JSON.parse(raw) as ExportBundle;
|
||||
if (bundle.dataset) {
|
||||
this.dataset = bundle.dataset;
|
||||
} else if (bundle.customDiff) {
|
||||
this.dataset = this.applyDiff(deepClone(baseline!), bundle.customDiff);
|
||||
} else {
|
||||
this.dataset = deepClone(baseline!);
|
||||
}
|
||||
this.presets = bundle.presets || [];
|
||||
// 恢复编辑器内容与语言
|
||||
if (typeof bundle.promptText === 'string') {
|
||||
this.promptText = bundle.promptText;
|
||||
}
|
||||
if (bundle.selectedLang) {
|
||||
this.selectedLang = bundle.selectedLang as LangCode;
|
||||
}
|
||||
} catch {
|
||||
this.dataset = deepClone(baseline!);
|
||||
}
|
||||
} else {
|
||||
this.dataset = deepClone(baseline!);
|
||||
}
|
||||
// 若无恢复语言,则按数据集进行推断
|
||||
if (!this.selectedLang) {
|
||||
const guessLang: LangCode = (this.dataset.languages.includes('zh_CN') ? 'zh_CN' : 'en') as LangCode;
|
||||
this.selectedLang = guessLang;
|
||||
}
|
||||
this.autoPersist();
|
||||
},
|
||||
autoPersist() {
|
||||
// 订阅状态变化并延迟保存(不写入 store,避免递归)
|
||||
this.$subscribe(() => {
|
||||
if (saveTimer) window.clearTimeout(saveTimer);
|
||||
saveTimer = window.setTimeout(() => {
|
||||
this.save();
|
||||
}, 400);
|
||||
}, { detached: true });
|
||||
},
|
||||
save() {
|
||||
if (!this.dataset) return;
|
||||
const bundle: ExportBundle = {
|
||||
version: 1,
|
||||
savedAt: new Date().toISOString(),
|
||||
dataset: deepClone(this.dataset),
|
||||
presets: deepClone(this.presets),
|
||||
promptText: this.promptText,
|
||||
selectedLang: this.selectedLang,
|
||||
};
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(bundle));
|
||||
},
|
||||
resetToDefault() {
|
||||
this.importFromBundle(null);
|
||||
},
|
||||
async importFromBundle(bundle: ExportBundle | null) {
|
||||
if (!bundle) {
|
||||
if (!baseline) baseline = await loadInitialDataset();
|
||||
this.dataset = deepClone(baseline!);
|
||||
} else {
|
||||
if (bundle.dataset) {
|
||||
this.dataset = bundle.dataset;
|
||||
} else if (bundle.customDiff) {
|
||||
if (!baseline) baseline = await loadInitialDataset();
|
||||
this.dataset = this.applyDiff(deepClone(baseline!), bundle.customDiff);
|
||||
}
|
||||
this.presets = bundle.presets || [];
|
||||
}
|
||||
this.selectedCategoryIndex = 0;
|
||||
this.selectedGroupIndex = 0;
|
||||
this.save();
|
||||
},
|
||||
exportToJson(): string {
|
||||
// 导出仅包含自定义差异(不包含公共词库)
|
||||
const diff = this.buildDiff(baseline!, this.dataset!);
|
||||
const bundle: ExportBundle = {
|
||||
version: 1,
|
||||
savedAt: new Date().toISOString(),
|
||||
customDiff: diff,
|
||||
presets: deepClone(this.presets),
|
||||
};
|
||||
return JSON.stringify(bundle, null, 2);
|
||||
},
|
||||
setLanguage(lang: LangCode) {
|
||||
this.selectedLang = lang;
|
||||
},
|
||||
selectCategory(idx: number) {
|
||||
this.selectedCategoryIndex = idx;
|
||||
this.selectedGroupIndex = 0;
|
||||
},
|
||||
selectGroup(idx: number) {
|
||||
this.selectedGroupIndex = idx;
|
||||
},
|
||||
setSearch(q: string) {
|
||||
this.searchQuery = q;
|
||||
},
|
||||
addTag(groupId: string, key = 'new_tag') {
|
||||
const grp = this.findGroupById(groupId);
|
||||
if (!grp) return;
|
||||
grp.tags.push({ key, translation: { en: key, [this.selectedLang]: key } });
|
||||
},
|
||||
removeTag(groupId: string, key: string) {
|
||||
const grp = this.findGroupById(groupId);
|
||||
if (!grp) return;
|
||||
grp.tags = grp.tags.filter((t) => t.key !== key);
|
||||
},
|
||||
updateTagKey(groupId: string, oldKey: string, newKey: string) {
|
||||
const grp = this.findGroupById(groupId);
|
||||
if (!grp) return;
|
||||
const tag = grp.tags.find((t) => t.key === oldKey);
|
||||
if (!tag) return;
|
||||
tag.key = newKey;
|
||||
if (!tag.translation) tag.translation = {};
|
||||
tag.translation.en = newKey;
|
||||
},
|
||||
setTranslation(groupId: string, key: string, lang: LangCode, val: string) {
|
||||
const grp = this.findGroupById(groupId);
|
||||
if (!grp) return;
|
||||
const tag = grp.tags.find((t) => t.key === key);
|
||||
if (!tag) return;
|
||||
if (!tag.translation) tag.translation = {};
|
||||
tag.translation[lang] = val;
|
||||
},
|
||||
toggleHidden(groupId: string, key: string) {
|
||||
const grp = this.findGroupById(groupId);
|
||||
if (!grp) return;
|
||||
const tag = grp.tags.find((t) => t.key === key);
|
||||
if (!tag) return;
|
||||
tag.hidden = !tag.hidden;
|
||||
},
|
||||
reorderTags(groupId: string, fromIndex: number, toIndex: number) {
|
||||
const grp = this.findGroupById(groupId);
|
||||
if (!grp) return;
|
||||
const list = grp.tags;
|
||||
if (fromIndex < 0 || toIndex < 0 || fromIndex >= list.length || toIndex >= list.length) return;
|
||||
const [item] = list.splice(fromIndex, 1);
|
||||
if (item) list.splice(toIndex, 0, item);
|
||||
},
|
||||
// —— 编辑器核心:基于左侧文本的 token 操作 ——
|
||||
setPromptText(text: string) {
|
||||
this.promptText = normalizePrompt(text);
|
||||
},
|
||||
setPromptTextRaw(text: string) {
|
||||
// 原始赋值,不做格式化,保留光标与撤回体验
|
||||
this.promptText = text;
|
||||
},
|
||||
replaceChineseComma() {
|
||||
this.promptText = this.promptText.replace(/,/g, ',');
|
||||
},
|
||||
formatPrompt() {
|
||||
this.promptText = normalizePrompt(this.promptText);
|
||||
},
|
||||
updateToken(index: number, newToken: string) {
|
||||
const tokens = splitTokens(this.promptText);
|
||||
if (index < 0 || index >= tokens.length) return;
|
||||
tokens[index] = normalizeToken(newToken);
|
||||
this.promptText = tokens.join(', ');
|
||||
},
|
||||
reorderTokens(fromIndex: number, toIndex: number) {
|
||||
const tokens = splitTokens(this.promptText);
|
||||
if (fromIndex < 0 || toIndex < 0 || fromIndex >= tokens.length || toIndex >= tokens.length) return;
|
||||
const [item] = tokens.splice(fromIndex, 1);
|
||||
tokens.splice(toIndex, 0, item!);
|
||||
this.promptText = tokens.join(', ');
|
||||
},
|
||||
removeToken(index: number) {
|
||||
const tokens = splitTokens(this.promptText);
|
||||
if (index < 0 || index >= tokens.length) return;
|
||||
tokens.splice(index, 1);
|
||||
this.promptText = tokens.join(', ');
|
||||
},
|
||||
addTokenAfter(index: number, token: string) {
|
||||
const tokens = splitTokens(this.promptText);
|
||||
tokens.splice(index + 1, 0, normalizeToken(token));
|
||||
this.promptText = tokens.join(', ');
|
||||
},
|
||||
getTagByKey(key: string): PromptTag | null {
|
||||
for (const cat of this.dataset?.categories || []) {
|
||||
for (const g of cat.groups) {
|
||||
const t = g.tags.find((x) => x.key === key);
|
||||
if (t) return t;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getTranslation(key: string, lang: LangCode): string | null {
|
||||
const tag = this.getTagByKey(key);
|
||||
if (!tag) return null;
|
||||
return tag.translation?.[lang] ?? tag.key;
|
||||
},
|
||||
getSuggestions(prefix: string, limit = 8): string[] {
|
||||
const list: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const p = prefix.trim().toLowerCase();
|
||||
if (!p) return [];
|
||||
for (const cat of this.dataset?.categories || []) {
|
||||
for (const g of cat.groups) {
|
||||
for (const t of g.tags) {
|
||||
const k = t.key;
|
||||
if (seen.has(k)) continue;
|
||||
if (k.toLowerCase().includes(p)) {
|
||||
list.push(k);
|
||||
seen.add(k);
|
||||
if (list.length >= limit) return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
},
|
||||
addMapping(key: string, lang: LangCode, val: string) {
|
||||
// 若 key 已存在则更新翻译;否则在自定义分组增加映射
|
||||
const exist = this.getTagByKey(key);
|
||||
if (exist) {
|
||||
if (!exist.translation) exist.translation = {};
|
||||
exist.translation[lang] = val;
|
||||
return;
|
||||
}
|
||||
const grp = this.ensureCustomGroup();
|
||||
grp.tags.push({ key, translation: { en: key, [lang]: val } });
|
||||
},
|
||||
ensureCustomGroup(): PromptGroup {
|
||||
const catName = 'Custom';
|
||||
let cat = this.dataset?.categories.find((c) => c.name === catName);
|
||||
if (!cat) {
|
||||
cat = { id: `cat_${Math.random().toString(36).slice(2, 9)}`, name: catName, groups: [] };
|
||||
this.dataset?.categories.push(cat);
|
||||
}
|
||||
let grp = cat.groups.find((g) => g.name === 'User Mapping');
|
||||
if (!grp) {
|
||||
grp = { id: `grp_${Math.random().toString(36).slice(2, 9)}`, name: 'User Mapping', color: '#6366f1', tags: [] };
|
||||
cat.groups.push(grp);
|
||||
}
|
||||
return grp;
|
||||
},
|
||||
savePreset(name: string) {
|
||||
const now = new Date().toISOString();
|
||||
const exist = this.presets.find((p) => p.name === name);
|
||||
if (exist) {
|
||||
exist.text = this.promptText;
|
||||
exist.updatedAt = now;
|
||||
} else {
|
||||
this.presets.push({ name, text: this.promptText, updatedAt: now });
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
loadPreset(name: string) {
|
||||
const p = this.presets.find((x) => x.name === name);
|
||||
if (!p) return;
|
||||
this.promptText = p.text;
|
||||
},
|
||||
deletePreset(name: string) {
|
||||
this.presets = this.presets.filter((p) => p.name !== name);
|
||||
this.save();
|
||||
},
|
||||
renamePreset(oldName: string, newName: string) {
|
||||
const target = this.presets.find((p) => p.name === oldName);
|
||||
if (!target) return;
|
||||
const conflict = this.presets.find((p) => p.name === newName);
|
||||
const now = new Date().toISOString();
|
||||
if (conflict && conflict !== target) {
|
||||
// 合并到冲突项:用新名覆盖旧名文本
|
||||
conflict.text = target.text;
|
||||
conflict.updatedAt = now;
|
||||
this.deletePreset(oldName);
|
||||
} else {
|
||||
target.name = newName;
|
||||
target.updatedAt = now;
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
findGroupById(id: string): PromptGroup | null {
|
||||
for (const cat of this.dataset?.categories || []) {
|
||||
const grp = cat.groups.find((g) => g.id === id);
|
||||
if (grp) return grp;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// 工具方法:构建差异与应用差异
|
||||
buildDiff(base: PromptDataset, cur: PromptDataset): CustomDiff {
|
||||
const categories: CustomDiff['categories'] = [];
|
||||
const baseCatMap = new Map(base.categories.map((c) => [c.name, c]));
|
||||
const curCatMap = new Map(cur.categories.map((c) => [c.name, c]));
|
||||
|
||||
for (const [name, curCat] of curCatMap.entries()) {
|
||||
const baseCat = baseCatMap.get(name);
|
||||
const catDiff: CustomDiff['categories'][number] = { name };
|
||||
if (!baseCat) {
|
||||
catDiff.addedGroups = curCat.groups.map((g) => deepClone(g));
|
||||
} else {
|
||||
const baseGrpMap = new Map(baseCat.groups.map((g) => [g.name, g]));
|
||||
const curGrpMap = new Map(curCat.groups.map((g) => [g.name, g]));
|
||||
const groupsDiff: NonNullable<typeof catDiff.groups> = [];
|
||||
|
||||
for (const [gname, curGrp] of curGrpMap.entries()) {
|
||||
const baseGrp = baseGrpMap.get(gname);
|
||||
if (!baseGrp) {
|
||||
if (!catDiff.addedGroups) catDiff.addedGroups = [];
|
||||
catDiff.addedGroups.push(deepClone(curGrp));
|
||||
continue;
|
||||
}
|
||||
const updated: { name: string; color?: string; added?: PromptTag[]; removed?: string[]; updated?: Array<{ key: string; translation?: Partial<Record<LangCode, string>>; hidden?: boolean }>; order?: string[] } = { name: gname } as any;
|
||||
if ((curGrp.color || '') !== (baseGrp.color || '')) updated.color = curGrp.color;
|
||||
const baseTagMap = new Map(baseGrp.tags.map((t) => [t.key, t]));
|
||||
const curTagMap = new Map(curGrp.tags.map((t) => [t.key, t]));
|
||||
|
||||
const addList: PromptTag[] = [];
|
||||
const updList: Array<{ key: string; translation?: Partial<Record<LangCode, string>>; hidden?: boolean }> = [];
|
||||
for (const [key, curTag] of curTagMap.entries()) {
|
||||
const baseTag = baseTagMap.get(key);
|
||||
if (!baseTag) {
|
||||
addList.push(deepClone(curTag));
|
||||
} else {
|
||||
let changed = false;
|
||||
const change: { key: string; translation?: Partial<Record<LangCode, string>>; hidden?: boolean } = { key };
|
||||
for (const l of ['en', 'zh_CN', 'es_ES'] as LangCode[]) {
|
||||
const a = baseTag.translation?.[l] ?? '';
|
||||
const b = curTag.translation?.[l] ?? '';
|
||||
if (a !== b) {
|
||||
if (!change.translation) change.translation = {};
|
||||
change.translation[l] = b;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
const aHidden = !!baseTag.hidden;
|
||||
const bHidden = !!curTag.hidden;
|
||||
if (aHidden !== bHidden) { change.hidden = bHidden; changed = true; }
|
||||
if (changed) updList.push(change);
|
||||
}
|
||||
}
|
||||
const remList: string[] = [];
|
||||
for (const [key] of baseTagMap.entries()) {
|
||||
if (!curTagMap.has(key)) remList.push(key);
|
||||
}
|
||||
const baseOrder = baseGrp.tags.map((t) => t.key);
|
||||
const curOrder = curGrp.tags.map((t) => t.key);
|
||||
const orderChanged = baseOrder.length !== curOrder.length || baseOrder.some((k, i) => k !== curOrder[i]);
|
||||
|
||||
if (updated.color || addList.length || remList.length || updList.length || orderChanged) {
|
||||
if (updated.color) updated.color = updated.color;
|
||||
if (addList.length) updated.added = addList;
|
||||
if (remList.length) updated.removed = remList;
|
||||
if (updList.length) updated.updated = updList;
|
||||
if (orderChanged) updated.order = curOrder;
|
||||
groupsDiff.push(updated as any);
|
||||
}
|
||||
}
|
||||
const removedGroups = baseCat.groups.filter((bg) => !curGrpMap.has(bg.name)).map((g) => g.name);
|
||||
if (removedGroups.length) catDiff.removedGroups = removedGroups;
|
||||
if (groupsDiff.length) catDiff.groups = groupsDiff;
|
||||
}
|
||||
if (catDiff.addedGroups?.length || catDiff.removedGroups?.length || catDiff.groups?.length) {
|
||||
categories.push(catDiff);
|
||||
}
|
||||
}
|
||||
for (const baseCat of base.categories) {
|
||||
if (!curCatMap.has(baseCat.name)) {
|
||||
categories.push({ name: baseCat.name, removedGroups: baseCat.groups.map((g) => g.name) });
|
||||
}
|
||||
}
|
||||
return { categories };
|
||||
},
|
||||
applyDiff(target: PromptDataset, diff: CustomDiff): PromptDataset {
|
||||
const catMap = new Map(target.categories.map((c) => [c.name, c]));
|
||||
for (const c of diff.categories || []) {
|
||||
let cat = catMap.get(c.name);
|
||||
if (!cat) {
|
||||
cat = { id: `cat_${Math.random().toString(36).slice(2, 9)}`, name: c.name, groups: [] };
|
||||
target.categories.push(cat);
|
||||
catMap.set(c.name, cat);
|
||||
}
|
||||
const grpMap = new Map(cat.groups.map((g) => [g.name, g]));
|
||||
for (const g of c.addedGroups || []) {
|
||||
const copy = deepClone(g);
|
||||
// 保持现有结构:如果没有 id,生成一个
|
||||
(copy as PromptGroup).id = `grp_${Math.random().toString(36).slice(2, 9)}`;
|
||||
(copy as PromptGroup).id = `grp_${Math.random().toString(36).slice(2, 9)}`;
|
||||
cat.groups.push(copy as PromptGroup);
|
||||
grpMap.set(copy.name, copy as PromptGroup);
|
||||
}
|
||||
for (const gname of c.removedGroups || []) {
|
||||
cat.groups = cat.groups.filter((g) => g.name !== gname);
|
||||
grpMap.delete(gname);
|
||||
}
|
||||
for (const gdiff of c.groups || []) {
|
||||
let grp = grpMap.get(gdiff.name);
|
||||
if (!grp) {
|
||||
grp = { id: `grp_${Math.random().toString(36).slice(2, 9)}`, name: gdiff.name, color: gdiff.color, tags: [] };
|
||||
cat.groups.push(grp);
|
||||
grpMap.set(grp.name, grp);
|
||||
}
|
||||
if (gdiff.color !== undefined) grp.color = gdiff.color;
|
||||
const tagMap = new Map(grp.tags.map((t) => [t.key, t]));
|
||||
for (const t of gdiff.added || []) {
|
||||
const copy = deepClone(t);
|
||||
grp.tags.push(copy);
|
||||
tagMap.set(copy.key, copy);
|
||||
}
|
||||
for (const key of gdiff.removed || []) {
|
||||
grp.tags = grp.tags.filter((t) => t.key !== key);
|
||||
tagMap.delete(key);
|
||||
}
|
||||
for (const u of gdiff.updated || []) {
|
||||
const tag = tagMap.get(u.key);
|
||||
if (!tag) continue;
|
||||
if (u.translation) tag.translation = { ...(tag.translation || {}), ...u.translation };
|
||||
if (u.hidden !== undefined) tag.hidden = u.hidden;
|
||||
}
|
||||
if (gdiff.order && gdiff.order.length) {
|
||||
const keyToTag = new Map(grp.tags.map((t) => [t.key, t]));
|
||||
const reordered: PromptTag[] = [];
|
||||
for (const key of gdiff.order) {
|
||||
const tag = keyToTag.get(key);
|
||||
if (tag) reordered.push(tag);
|
||||
}
|
||||
// 保留未列出的条目
|
||||
for (const t of grp.tags) {
|
||||
if (!gdiff.order.includes(t.key)) reordered.push(t);
|
||||
}
|
||||
grp.tags = reordered;
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
},
|
||||
},
|
||||
});
|
||||
// —— 工具方法 ——
|
||||
function splitTokens(text: string): string[] {
|
||||
return text
|
||||
.split(/[,,]/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
}
|
||||
function normalizeToken(t: string): string {
|
||||
return t.trim();
|
||||
}
|
||||
function normalizePrompt(text: string): string {
|
||||
return splitTokens(text).join(', ');
|
||||
}
|
||||
Reference in New Issue
Block a user