From 4e84ffda4de2d0e95e4a68f0d0aba27dee53b7fe Mon Sep 17 00:00:00 2001 From: silentsilas Date: Fri, 7 Feb 2025 19:59:32 -0500 Subject: [PATCH] implement suggestions/rhymes in pad editor using data muse api --- src/routes/(app)/pad/+page.svelte | 206 +++++++++++++++++++++++++++++- 1 file changed, 201 insertions(+), 5 deletions(-) diff --git a/src/routes/(app)/pad/+page.svelte b/src/routes/(app)/pad/+page.svelte index 7b62769..84394f4 100644 --- a/src/routes/(app)/pad/+page.svelte +++ b/src/routes/(app)/pad/+page.svelte @@ -5,20 +5,216 @@ import '@friendofsvelte/tipex/styles/Controls.css'; import '@friendofsvelte/tipex/styles/EditLink.css'; import '@friendofsvelte/tipex/styles/CodeBlock.css'; - const INITIAL_HTML_CONTENT = `

Simple editor

What's written here will be saved to your local storage, and won't be sent to the server.

`; - let body = localStorage.getItem('tipex') || INITIAL_HTML_CONTENT; - let editor: TipexEditor | undefined = $state(); - function handleUpdate() { + const INITIAL_HTML_CONTENT = `

Simple editor

What's written here will be saved to your local storage. Highlight a word to see suggested rhymes.

`; + let body = $state(localStorage.getItem('tipex') || INITIAL_HTML_CONTENT); + let editor = $state(); + let suggestions = $state>([]); + let rhymes = $state>([]); + let currentWord = $state(''); + let cursorPosition = $state({ x: 0, y: 0 }); + let debounceTimer = $state(); + + type Suggestion = { + word: string; + score: number; + }; + + function debounce any>( + fn: T, + delay: number + ): (...args: Parameters) => void { + return (...args: Parameters) => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => fn(...args), delay); + }; + } + + const datamuseApi = { + async getSuggestions(word: string): Promise { + if (!word || word.length < 2) return []; + const response = await fetch(`https://api.datamuse.com/sug?s=${encodeURIComponent(word)}`); + return response.json(); + }, + + async getRhymes(word: string): Promise { + if (!word) return []; + const response = await fetch( + `https://api.datamuse.com/words?rel_rhy=${encodeURIComponent(word)}` + ); + return response.json(); + } + }; + + const debouncedFetchSuggestions = debounce(async (word: string) => { + suggestions = await datamuseApi.getSuggestions(word); + }, 300); + + function updateCursorPosition() { + const selection = window.getSelection(); + if (!selection?.rangeCount) return; + + const range = selection.getRangeAt(0); + const rect = range.getBoundingClientRect(); + + cursorPosition = { + x: rect.left + window.scrollX, + y: rect.bottom + window.scrollY + 10 + }; + } + + async function handleUpdate() { const currentHtml = editor?.getHTML(); localStorage.setItem('tipex', currentHtml || ''); + + const { state } = editor?.view || {}; + const { doc, selection } = state || {}; + + if (!doc || !selection) return; + + const pos = selection.from; + const currentChar = doc.textBetween(Math.max(0, pos - 1), pos); + + if (/[\s\.,!?;:]/.test(currentChar)) { + suggestions = []; + currentWord = ''; + return; + } + + const textBefore = doc.textBetween(Math.max(0, pos - 100), pos); + + const lastSpace = textBefore.search(/\s\w+$/); + const currentWordMatch = + lastSpace >= 0 ? textBefore.slice(lastSpace).match(/\w+$/) : textBefore.match(/\w+$/); + + const word = currentWordMatch ? currentWordMatch[0] : ''; + + if (word && word !== currentWord) { + currentWord = word; + debouncedFetchSuggestions(currentWord); + } else if (!word) { + suggestions = []; + currentWord = ''; + } + updateCursorPosition(); + } + + async function handleMouseUp() { + const selection = editor?.view.state.selection; + if (selection && !selection.empty) { + const selectedText = editor?.view.state.doc.textBetween(selection.from, selection.to, ' '); + + if (selectedText) { + rhymes = await datamuseApi.getRhymes(selectedText); + } + } else { + rhymes = []; + } + updateCursorPosition(); + } + + async function handleMouseDown() { + suggestions = []; + currentWord = ''; + rhymes = []; + } + + function applySuggestion(suggestion: string) { + if (!editor?.view) return; + + const view = editor.view; + const { state } = view; + const { schema, selection, tr } = state; + + view.focus(); + + if (rhymes.length > 0 && !selection.empty) { + const newTr = tr.replaceSelectionWith(schema.text(suggestion)); + view.dispatch(newTr); + rhymes = []; + } else { + const pos = selection.from; + const wordBefore = state.doc.textBetween(Math.max(0, pos - 100), pos); + const match = wordBefore.match(/\S+$/); + + if (match) { + const wordStart = pos - match[0].length; + const newTr = tr.replaceWith(wordStart, pos, schema.text(suggestion)); + view.dispatch(newTr); + } + } + + suggestions = []; + currentWord = ''; + + setTimeout(() => { + view.focus(); + }, 0); } silentsilas - Pad + -
+
+ + {#if suggestions.length > 0 || rhymes.length > 0} +
+ {#if suggestions.length > 0} +
+

Suggestions

+ {#each suggestions.slice(0, 10) as { word }} + + {/each} +
+ {/if} + + {#if rhymes.length > 0} +
+

Rhymes

+ {#each rhymes.slice(0, 10) as { word }} + + {/each} +
+ {/if} +
+ {/if}
+ +