From 40b4bf61ef5dcd823ba390c9abb449f6334240b3 Mon Sep 17 00:00:00 2001 From: Lu Wilson Date: Wed, 10 Jun 2026 15:58:55 +0100 Subject: [PATCH 1/3] GO --- .../EditAttributionLine.vue | 48 ++ .../morelike-playground/fetchMorelike.ts | 254 +++++++++ .../morelike-playground/formatEditComment.ts | 129 +++++ src/prototypes/morelike-playground/index.vue | 524 ++++++++++++++++++ .../morelike-playground/morelikeMlt.ts | 151 +++++ .../morelike-playground/morelikeStorage.ts | 106 ++++ 6 files changed, 1212 insertions(+) create mode 100644 src/prototypes/morelike-playground/EditAttributionLine.vue create mode 100644 src/prototypes/morelike-playground/fetchMorelike.ts create mode 100644 src/prototypes/morelike-playground/formatEditComment.ts create mode 100644 src/prototypes/morelike-playground/index.vue create mode 100644 src/prototypes/morelike-playground/morelikeMlt.ts create mode 100644 src/prototypes/morelike-playground/morelikeStorage.ts diff --git a/src/prototypes/morelike-playground/EditAttributionLine.vue b/src/prototypes/morelike-playground/EditAttributionLine.vue new file mode 100644 index 0000000..96fed26 --- /dev/null +++ b/src/prototypes/morelike-playground/EditAttributionLine.vue @@ -0,0 +1,48 @@ + + + + diff --git a/src/prototypes/morelike-playground/fetchMorelike.ts b/src/prototypes/morelike-playground/fetchMorelike.ts new file mode 100644 index 0000000..9fd3ed1 --- /dev/null +++ b/src/prototypes/morelike-playground/fetchMorelike.ts @@ -0,0 +1,254 @@ +import { wikimediaApiFetchHeaders } from '@/config' + +import { + resolveMltParams, + type MorelikeMltCustomSettings, + type MorelikeMltPreset, + type MorelikeSortOrder, +} from './morelikeMlt' + +const WIKI_HOST = 'en.wikipedia.org' +const API_URL = `https://${WIKI_HOST}/w/api.php` + +export interface MorelikeSearchHit { + title: string + description: string + timestamp: string + pageUrl: string + revisionAuthor?: string + revisionComment?: string + revisionContent?: string + thumbnail?: { + url: string + width: number + height: number + } +} + +interface GeneratorRevision { + timestamp?: string + user?: string + comment?: string + slots?: { + main?: { + content?: string + } + } +} + +interface GeneratorPage { + title: string + index?: number + description?: string + thumbnail?: { + source: string + width: number + height: number + } + revisions?: GeneratorRevision[] +} + +interface GeneratorSearchResponse { + error?: { + code?: string + info?: string + } + query?: { + pages?: GeneratorPage[] + } +} + +export class MorelikeFetchError extends Error { + constructor( + message: string, + public readonly code: 'aborted' | 'http' | 'empty', + ) { + super(message) + this.name = 'MorelikeFetchError' + } +} + +/** Split seed input on newlines and commas; trim, dedupe, drop empties. */ +export function parseSeedTitles(raw: string): string[] { + const seen = new Set() + const titles: string[] = [] + + for (const part of raw.split(/[\n,]+/)) { + const title = part.trim() + if (!title.length || seen.has(title)) continue + seen.add(title) + titles.push(title) + } + + return titles +} + +export function buildSrsearch(seedText: string): string | null { + const seeds = parseSeedTitles(seedText) + if (!seeds.length) return null + + return `morelike:${seeds.join('|')}` +} + +function articleUrl(title: string): string { + return `https://${WIKI_HOST}/wiki/${encodeURIComponent(title.replace(/ /g, '_'))}` +} + +function buildMorelikeSearchParams( + gsrsearch: string, + limit: number, + mltParams: Record, +): URLSearchParams { + const params = new URLSearchParams({ + action: 'query', + generator: 'search', + gsrsearch, + gsrnamespace: '0', + gsrlimit: String(limit), + prop: 'pageimages|description|revisions', + piprop: 'thumbnail', + pithumbsize: '160', + rvprop: 'timestamp|user|comment|content', + rvslots: 'main', + format: 'json', + formatversion: '2', + origin: '*', + }) + + for (const [key, value] of Object.entries(mltParams)) { + params.set(key, value) + } + + return params +} + +/** Exact URL sent to the Action API (shared by fetch + UI). */ +export function buildMorelikeApiRequestUrl( + seedText: string, + limit: number, + mltPreset: MorelikeMltPreset, + mltCustom: MorelikeMltCustomSettings, +): string | null { + const gsrsearch = buildSrsearch(seedText) + if (!gsrsearch) return null + + const params = buildMorelikeSearchParams(gsrsearch, limit, resolveMltParams(mltPreset, mltCustom)) + return `${API_URL}?${params.toString()}` +} + +async function fetchMorelikeHits( + requestUrl: string, + signal?: AbortSignal, +): Promise { + const response = await fetch(requestUrl, { + signal, + headers: wikimediaApiFetchHeaders('morelike-search'), + }) + + if (!response.ok) { + throw new MorelikeFetchError(`Search failed (HTTP ${response.status})`, 'http') + } + + const data = (await response.json()) as GeneratorSearchResponse + + if (data.error) { + throw new MorelikeFetchError(data.error.info ?? data.error.code ?? 'Search failed', 'http') + } + + const pages = data.query?.pages ?? [] + + return [...pages].sort((a, b) => (a.index ?? 0) - (b.index ?? 0)) +} + +function mapRevisionFields(revision: GeneratorRevision | undefined): { + timestamp: string + author?: string + comment?: string + content?: string +} { + return { + timestamp: revision?.timestamp ?? '', + author: revision?.user, + comment: revision?.comment, + content: revision?.slots?.main?.content, + } +} + +function mapPageToHit(page: GeneratorPage): MorelikeSearchHit { + const latest = mapRevisionFields(page.revisions?.[0]) + + return { + title: page.title, + description: page.description?.trim() ?? '', + timestamp: latest.timestamp, + pageUrl: articleUrl(page.title), + revisionAuthor: latest.author, + revisionComment: latest.comment, + revisionContent: latest.content, + thumbnail: page.thumbnail + ? { + url: page.thumbnail.source, + width: page.thumbnail.width, + height: page.thumbnail.height, + } + : undefined, + } +} + +function sortByNewestEditFirst(hits: MorelikeSearchHit[]): MorelikeSearchHit[] { + return [...hits].sort((a, b) => { + const aTime = a.timestamp ? Date.parse(a.timestamp) : Number.NaN + const bTime = b.timestamp ? Date.parse(b.timestamp) : Number.NaN + + if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0 + if (Number.isNaN(aTime)) return 1 + if (Number.isNaN(bTime)) return -1 + + return bTime - aTime + }) +} + +/** Client-side sort — API results always stay in relevance order. */ +export function sortMorelikeHits( + hits: MorelikeSearchHit[], + sortOrder: MorelikeSortOrder, +): MorelikeSearchHit[] { + if (sortOrder === 'lastEdit') { + return sortByNewestEditFirst(hits) + } + + return hits +} + +export async function fetchMorelikeResults( + seedText: string, + options: { + limit: number + mltPreset: MorelikeMltPreset + mltCustom: MorelikeMltCustomSettings + signal?: AbortSignal + }, +): Promise { + if (options.signal?.aborted) { + throw new MorelikeFetchError('Request aborted', 'aborted') + } + + const requestUrl = buildMorelikeApiRequestUrl( + seedText, + options.limit, + options.mltPreset, + options.mltCustom, + ) + + if (!requestUrl) { + throw new MorelikeFetchError('Add at least one seed page title', 'empty') + } + + const seedTitles = new Set(parseSeedTitles(seedText)) + + const pages = await fetchMorelikeHits(requestUrl, options.signal) + + return pages + .filter((page) => !seedTitles.has(page.title)) + .map(mapPageToHit) +} diff --git a/src/prototypes/morelike-playground/formatEditComment.ts b/src/prototypes/morelike-playground/formatEditComment.ts new file mode 100644 index 0000000..5b5827f --- /dev/null +++ b/src/prototypes/morelike-playground/formatEditComment.ts @@ -0,0 +1,129 @@ +export type EditCommentPart = + | { type: 'text'; value: string } + | { type: 'section'; value: string } + | { type: 'link'; value: string } + +function collapseWhitespace(text: string): string { + return text.replace(/\s+/g, ' ').trim() +} + +function stripWikiMarkup(text: string): string { + let stripped = text + + stripped = stripped.replace(/<[^>]+>/g, '') + stripped = stripped.replace(/\{\|[\s\S]*?\|\}/g, '') + + let previous = '' + while (previous !== stripped) { + previous = stripped + stripped = stripped.replace(/\{\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\}/g, '') + } + + stripped = stripped.replace(/'''''(.+?)'''''/g, '$1') + stripped = stripped.replace(/'''(.+?)'''/g, '$1') + stripped = stripped.replace(/''(.+?)''/g, '$1') + stripped = stripped.replace(/''/g, '') + stripped = stripped.replace(/^=+\s*(.*?)\s*=+$/gm, '$1') + stripped = stripped.replace(/^[*#:;]+/gm, '') + + return stripped +} + +function removeNonLinkWiki(text: string): string { + let cleaned = text + + cleaned = cleaned.replace(/\[\[(?:File|Image|Media|Category):[^\]]*]]/gi, '') + cleaned = cleaned.replace(/\[(?:https?:\/\/[^\s\]]+)(?:\s+([^\]]+))?\]/gi, '$1') + + return cleaned +} + +function appendTextPart(parts: EditCommentPart[], value: string): void { + const text = collapseWhitespace(stripWikiMarkup(value)) + if (!text.length) return + + const last = parts[parts.length - 1] + if (last?.type === 'text') { + last.value = collapseWhitespace(`${last.value}${text}`) + return + } + + parts.push({ type: 'text', value: text }) +} + +function wikiSummaryToParts(wiki: string): EditCommentPart[] { + const parts: EditCommentPart[] = [] + const text = removeNonLinkWiki(wiki) + const linkPattern = /\[\[([^|\]]+)\|([^\]]+)]]|\[\[([^\]]+)]]/g + + let lastIndex = 0 + let match: RegExpExecArray | null + + while ((match = linkPattern.exec(text)) !== null) { + appendTextPart(parts, text.slice(lastIndex, match.index)) + + const display = collapseWhitespace(stripWikiMarkup((match[2] ?? match[3]).trim())) + if (display.length) { + parts.push({ type: 'link', value: display }) + } + + lastIndex = match.index + match[0].length + } + + appendTextPart(parts, text.slice(lastIndex)) + return parts +} + +function hasSummaryContent(parts: EditCommentPart[]): boolean { + return parts.some((part) => part.value.trim().length > 0) +} + +function quoteSummaryParts(parts: EditCommentPart[]): EditCommentPart[] { + if (!hasSummaryContent(parts)) return [] + + if (parts[0]?.type === 'text') { + parts[0].value = parts[0].value.trimStart() + } + + return [{ type: 'text', value: '"' }, ...parts, { type: 'text', value: '"' }] +} + +/** Structured edit comment for card display (arrow, section label, quoted summary). */ +export function parseEditComment(raw: string): EditCommentPart[] { + const trimmed = raw.trim() + if (!trimmed.length) return [] + + const sectionMatch = trimmed.match(/^\/\*\s*(.*?)\s*\*\/\s*(.*)$/s) + if (sectionMatch) { + const section = collapseWhitespace(sectionMatch[1]) + const restParts = quoteSummaryParts(wikiSummaryToParts(sectionMatch[2])) + + if (!section.length && !restParts.length) { + return [] + } + + if (!section.length) { + return restParts + } + + const parts: EditCommentPart[] = [ + { type: 'text', value: '→' }, + { type: 'section', value: section }, + ] + + if (restParts.length) { + parts.push({ type: 'text', value: ':' }, { type: 'text', value: ' ' }, ...restParts) + } + + return parts + } + + return quoteSummaryParts(wikiSummaryToParts(trimmed)) +} + +/** Plain-text join (e.g. empty checks). */ +export function formatEditComment(raw: string): string { + return parseEditComment(raw) + .map((part) => part.value) + .join('') +} diff --git a/src/prototypes/morelike-playground/index.vue b/src/prototypes/morelike-playground/index.vue new file mode 100644 index 0000000..4845da4 --- /dev/null +++ b/src/prototypes/morelike-playground/index.vue @@ -0,0 +1,524 @@ + + + + + diff --git a/src/prototypes/morelike-playground/morelikeMlt.ts b/src/prototypes/morelike-playground/morelikeMlt.ts new file mode 100644 index 0000000..7312a79 --- /dev/null +++ b/src/prototypes/morelike-playground/morelikeMlt.ts @@ -0,0 +1,151 @@ +export type MorelikeMltPreset = 'default' | 'custom' + +export type MorelikeSortOrder = 'relevance' | 'lastEdit' + +/** CirrusSearch more_like_this URL params (see Help:CirrusSearch#Morelike). */ +export interface MorelikeMltCustomSettings { + maxQueryTerms: number + minTermFreq: number + minDocFreq: number + maxDocFreq: number | null + minWordLength: number + maxWordLength: number | null + minimumShouldMatchPercent: number + fields: 'text' | 'title' | 'opening_text' + useFieldsOnly: boolean +} + +/** + * Extension defaults from CirrusSearch $wgCirrusSearchMoreLikeThisConfig + * (docs/settings.txt). The “Default” preset omits all cirrusMlt* params so the + * wiki uses these server-side — Custom only sends params that differ from this. + */ +export const CIRRUS_MLT_EXTENSION_DEFAULTS: MorelikeMltCustomSettings = { + maxQueryTerms: 25, + minTermFreq: 2, + minDocFreq: 2, + maxDocFreq: null, + minWordLength: 0, + maxWordLength: null, + minimumShouldMatchPercent: 30, + fields: 'text', + useFieldsOnly: false, +} + +/** Starting values for the Custom sliders (matches extension defaults). */ +export const DEFAULT_MLT_CUSTOM: MorelikeMltCustomSettings = { + ...CIRRUS_MLT_EXTENSION_DEFAULTS, +} + +export const MLT_PRESET_LABELS: Record = { + default: 'No overrides (wiki default)', + custom: 'Custom', +} + +export const SORT_ORDER_LABELS: Record = { + relevance: 'Most relevant', + lastEdit: 'Most recent edit', +} + +function clampInt(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, Math.round(value))) +} + +export function normalizeMltCustom(input: unknown): MorelikeMltCustomSettings { + if (typeof input !== 'object' || input === null) { + return { ...DEFAULT_MLT_CUSTOM } + } + + const record = input as Record + const fields = + record.fields === 'title' || record.fields === 'opening_text' || record.fields === 'text' + ? record.fields + : DEFAULT_MLT_CUSTOM.fields + + return { + maxQueryTerms: + typeof record.maxQueryTerms === 'number' + ? clampInt(record.maxQueryTerms, 1, 100) + : DEFAULT_MLT_CUSTOM.maxQueryTerms, + minTermFreq: + typeof record.minTermFreq === 'number' + ? clampInt(record.minTermFreq, 1, 20) + : DEFAULT_MLT_CUSTOM.minTermFreq, + minDocFreq: + typeof record.minDocFreq === 'number' + ? clampInt(record.minDocFreq, 1, 20) + : DEFAULT_MLT_CUSTOM.minDocFreq, + maxDocFreq: + typeof record.maxDocFreq === 'number' + ? clampInt(record.maxDocFreq, 1, 1_000_000) + : null, + minWordLength: + typeof record.minWordLength === 'number' + ? clampInt(record.minWordLength, 0, 20) + : DEFAULT_MLT_CUSTOM.minWordLength, + maxWordLength: + typeof record.maxWordLength === 'number' ? clampInt(record.maxWordLength, 1, 100) : null, + minimumShouldMatchPercent: + typeof record.minimumShouldMatchPercent === 'number' + ? clampInt(record.minimumShouldMatchPercent, 1, 100) + : DEFAULT_MLT_CUSTOM.minimumShouldMatchPercent, + fields, + useFieldsOnly: + typeof record.useFieldsOnly === 'boolean' + ? record.useFieldsOnly + : DEFAULT_MLT_CUSTOM.useFieldsOnly, + } +} + +function buildCustomMltParams(custom: MorelikeMltCustomSettings): Record { + const normalized = normalizeMltCustom(custom) + const defaults = CIRRUS_MLT_EXTENSION_DEFAULTS + const params: Record = {} + + if (normalized.maxQueryTerms !== defaults.maxQueryTerms) { + params.cirrusMltMaxQueryTerms = String(normalized.maxQueryTerms) + } + + if (normalized.minTermFreq !== defaults.minTermFreq) { + params.cirrusMltMinTermFreq = String(normalized.minTermFreq) + } + + if (normalized.minDocFreq !== defaults.minDocFreq) { + params.cirrusMltMinDocFreq = String(normalized.minDocFreq) + } + + if (normalized.minWordLength !== defaults.minWordLength) { + params.cirrusMltMinWordLength = String(normalized.minWordLength) + } + + if (normalized.minimumShouldMatchPercent !== defaults.minimumShouldMatchPercent) { + params.cirrusMltMinimumShouldMatch = `${normalized.minimumShouldMatchPercent}%` + } + + if (normalized.maxDocFreq !== null) { + params.cirrusMltMaxDocFreq = String(normalized.maxDocFreq) + } + + if (normalized.maxWordLength !== null) { + params.cirrusMltMaxWordLength = String(normalized.maxWordLength) + } + + if (normalized.useFieldsOnly) { + params.cirrusMltUseFields = 'true' + } + + if (normalized.useFieldsOnly || normalized.fields !== defaults.fields) { + params.cirrusMltFields = normalized.fields + } + + return params +} + +export function resolveMltParams( + preset: MorelikeMltPreset, + custom: MorelikeMltCustomSettings, +): Record { + if (preset === 'default') return {} + + return buildCustomMltParams(custom) +} diff --git a/src/prototypes/morelike-playground/morelikeStorage.ts b/src/prototypes/morelike-playground/morelikeStorage.ts new file mode 100644 index 0000000..ef79ff9 --- /dev/null +++ b/src/prototypes/morelike-playground/morelikeStorage.ts @@ -0,0 +1,106 @@ +import { + DEFAULT_MLT_CUSTOM, + normalizeMltCustom, + type MorelikeMltCustomSettings, + type MorelikeMltPreset, + type MorelikeSortOrder, +} from './morelikeMlt' + +export interface MorelikePlaygroundState { + seedText: string + resultLimit: number + sortOrder: MorelikeSortOrder + mltPreset: MorelikeMltPreset + mltCustom: MorelikeMltCustomSettings +} + +const STORAGE_KEY = 'protowiki-morelike-playground-v1' + +export const DEFAULT_MORELIKE_PLAYGROUND_STATE: MorelikePlaygroundState = { + seedText: '', + resultLimit: 20, + sortOrder: 'relevance', + mltPreset: 'default', + mltCustom: { ...DEFAULT_MLT_CUSTOM }, +} + +function clampResultLimit(value: number): number { + return Math.min(100, Math.max(1, Math.round(value))) +} + +function isSortOrder(value: unknown): value is MorelikeSortOrder { + return value === 'relevance' || value === 'lastEdit' +} + +function isMltPreset(value: unknown): value is MorelikeMltPreset { + return value === 'default' || value === 'custom' +} + +function normalizeMltPreset(value: unknown): MorelikeMltPreset { + if (isMltPreset(value)) return value + // Removed presets fall back to wiki default. + if (value === 'balancedTitles' || value === 'fewerTerms') return 'default' + return DEFAULT_MORELIKE_PLAYGROUND_STATE.mltPreset +} + +function normalizeState(input: unknown): MorelikePlaygroundState { + if (typeof input !== 'object' || input === null) { + return { ...DEFAULT_MORELIKE_PLAYGROUND_STATE, mltCustom: { ...DEFAULT_MLT_CUSTOM } } + } + + const record = input as Record + + return { + seedText: typeof record.seedText === 'string' ? record.seedText : DEFAULT_MORELIKE_PLAYGROUND_STATE.seedText, + resultLimit: + typeof record.resultLimit === 'number' + ? clampResultLimit(record.resultLimit) + : DEFAULT_MORELIKE_PLAYGROUND_STATE.resultLimit, + sortOrder: isSortOrder(record.sortOrder) + ? record.sortOrder + : DEFAULT_MORELIKE_PLAYGROUND_STATE.sortOrder, + mltPreset: normalizeMltPreset(record.mltPreset), + mltCustom: normalizeMltCustom(record.mltCustom), + } +} + +function clearStoredState(): void { + if (typeof window === 'undefined') return + + try { + window.localStorage.removeItem(STORAGE_KEY) + } catch { + // Private mode or blocked storage — ignore. + } +} + +export function loadMorelikePlaygroundState(): MorelikePlaygroundState { + if (typeof window === 'undefined') { + return { ...DEFAULT_MORELIKE_PLAYGROUND_STATE, mltCustom: { ...DEFAULT_MLT_CUSTOM } } + } + + try { + const stored = window.localStorage.getItem(STORAGE_KEY) + if (!stored) return { ...DEFAULT_MORELIKE_PLAYGROUND_STATE, mltCustom: { ...DEFAULT_MLT_CUSTOM } } + + const parsed: unknown = JSON.parse(stored) + const normalized = normalizeState(parsed) + persistMorelikePlaygroundState(normalized) + return normalized + } catch { + clearStoredState() + return { ...DEFAULT_MORELIKE_PLAYGROUND_STATE, mltCustom: { ...DEFAULT_MLT_CUSTOM } } + } +} + +export function persistMorelikePlaygroundState(state: MorelikePlaygroundState): void { + if (typeof window === 'undefined') return + + const normalized = normalizeState(state) + + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(normalized)) + } catch { + // Quota or private-mode failures — ignore. + } +} From 733961b7227e42c9e2cfee57ae9d3c7b09568117 Mon Sep 17 00:00:00 2001 From: Lu Wilson Date: Tue, 16 Jun 2026 15:09:44 +0100 Subject: [PATCH 2/3] add noboost option --- .../morelike-playground/fetchMorelike.ts | 15 +++++++++++++- src/prototypes/morelike-playground/index.vue | 20 ++++++++++++++++++- .../morelike-playground/morelikeStorage.ts | 6 ++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/prototypes/morelike-playground/fetchMorelike.ts b/src/prototypes/morelike-playground/fetchMorelike.ts index 9fd3ed1..ec1541b 100644 --- a/src/prototypes/morelike-playground/fetchMorelike.ts +++ b/src/prototypes/morelike-playground/fetchMorelike.ts @@ -98,6 +98,7 @@ function buildMorelikeSearchParams( gsrsearch: string, limit: number, mltParams: Record, + classicNoboostlinks: boolean, ): URLSearchParams { const params = new URLSearchParams({ action: 'query', @@ -115,6 +116,10 @@ function buildMorelikeSearchParams( origin: '*', }) + if (classicNoboostlinks) { + params.set('gsrqiprofile', 'classic_noboostlinks') + } + for (const [key, value] of Object.entries(mltParams)) { params.set(key, value) } @@ -128,11 +133,17 @@ export function buildMorelikeApiRequestUrl( limit: number, mltPreset: MorelikeMltPreset, mltCustom: MorelikeMltCustomSettings, + classicNoboostlinks = true, ): string | null { const gsrsearch = buildSrsearch(seedText) if (!gsrsearch) return null - const params = buildMorelikeSearchParams(gsrsearch, limit, resolveMltParams(mltPreset, mltCustom)) + const params = buildMorelikeSearchParams( + gsrsearch, + limit, + resolveMltParams(mltPreset, mltCustom), + classicNoboostlinks, + ) return `${API_URL}?${params.toString()}` } @@ -226,6 +237,7 @@ export async function fetchMorelikeResults( limit: number mltPreset: MorelikeMltPreset mltCustom: MorelikeMltCustomSettings + classicNoboostlinks?: boolean signal?: AbortSignal }, ): Promise { @@ -238,6 +250,7 @@ export async function fetchMorelikeResults( options.limit, options.mltPreset, options.mltCustom, + options.classicNoboostlinks ?? true, ) if (!requestUrl) { diff --git a/src/prototypes/morelike-playground/index.vue b/src/prototypes/morelike-playground/index.vue index 4845da4..1c4c92b 100644 --- a/src/prototypes/morelike-playground/index.vue +++ b/src/prototypes/morelike-playground/index.vue @@ -53,6 +53,7 @@ const mltPreset = ref('default') const mltCustom = ref({ ...DEFAULT_MORELIKE_PLAYGROUND_STATE.mltCustom, }) +const classicNoboostlinks = ref(DEFAULT_MORELIKE_PLAYGROUND_STATE.classicNoboostlinks) const results = ref([]) const sortedResults = computed(() => sortMorelikeHits(results.value, sortOrder.value)) @@ -96,6 +97,7 @@ const requestUrl = computed( resultLimit.value, mltPreset.value, mltCustom.value, + classicNoboostlinks.value, ) ?? '', ) @@ -119,6 +121,7 @@ function schedulePersist(immediate = false): void { sortOrder: sortOrder.value, mltPreset: mltPreset.value, mltCustom: mltCustom.value, + classicNoboostlinks: classicNoboostlinks.value, }) } @@ -145,6 +148,7 @@ async function onSearch(): Promise { limit: resultLimit.value, mltPreset: mltPreset.value, mltCustom: mltCustom.value, + classicNoboostlinks: classicNoboostlinks.value, signal: abortController.signal, }) @@ -182,10 +186,13 @@ onMounted(() => { sortOrder.value = stored.sortOrder mltPreset.value = stored.mltPreset mltCustom.value = { ...stored.mltCustom } + classicNoboostlinks.value = stored.classicNoboostlinks }) watch(seedText, () => schedulePersist(false)) -watch([resultLimit, sortOrder, mltPreset, mltCustom], () => schedulePersist(true), { deep: true }) +watch([resultLimit, sortOrder, mltPreset, mltCustom, classicNoboostlinks], () => schedulePersist(true), { + deep: true, +})