feat: implement inline autocomplete suggestions with FastAPI backend and Milkdown editor integration
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { Plugin, PluginKey } from '@milkdown/prose/state';
|
||||
import { EditorView } from '@milkdown/prose/view';
|
||||
|
||||
const INLINE_SUGGESTION_KEY = new PluginKey('inline-suggestion');
|
||||
const DEBOUNCE_MS = 150;
|
||||
let debounceTimer = null;
|
||||
let currentSuggestion = '';
|
||||
let suggestionPos = { from: 0, to: 0 };
|
||||
|
||||
interface InlineSuggestionOptions {
|
||||
apiUrl?: string;
|
||||
}
|
||||
|
||||
function createInlineSuggestionPlugin(options: InlineSuggestionOptions = {}) {
|
||||
const apiUrl = options.apiUrl || 'http://localhost:8000/v1/completions';
|
||||
|
||||
return new Plugin({
|
||||
key: INLINE_SUGGESTION_KEY,
|
||||
state: {
|
||||
init: () => ({ suggestion: '', visible: false }),
|
||||
apply: (tr, value) => {
|
||||
if (!tr.docChanged) return value;
|
||||
const { from, to } = tr.selection;
|
||||
if (from === suggestionPos.from && to === suggestionPos.to) {
|
||||
return value;
|
||||
}
|
||||
return { suggestion: '', visible: false };
|
||||
},
|
||||
},
|
||||
props: {
|
||||
handleKeyDown: (view: EditorView, event: KeyboardEvent) => {
|
||||
if (event.key === 'Tab' && INLINE_SUGGESTION_KEY.getState(view.state).visible) {
|
||||
event.preventDefault();
|
||||
const { suggestion } = INLINE_SUGGESTION_KEY.getState(view.state);
|
||||
if (suggestion) {
|
||||
view.dispatch(view.state.tr.insertText(suggestion, view.state.selection.from));
|
||||
currentSuggestion = '';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
const state = INLINE_SUGGESTION_KEY.getState(view.state);
|
||||
if (state.visible) {
|
||||
view.dispatch(view.state.tr.setMeta(INLINE_SUGGESTION_KEY, { suggestion: '', visible: false }));
|
||||
currentSuggestion = '';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
const lastTr = transactions[transactions.length - 1];
|
||||
if (!lastTr || !lastTr.docChanged) return null;
|
||||
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
const { from, to } = newState.selection;
|
||||
const prefix = newState.doc.textBetween(0, from);
|
||||
const suffix = newState.doc.textBetween(to, newState.doc.content.size);
|
||||
|
||||
try {
|
||||
const res = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prefix, suffix, languageId: 'markdown' }),
|
||||
});
|
||||
|
||||
if (!res.ok) return;
|
||||
|
||||
const reader = res.body?.getReader();
|
||||
if (!reader) return;
|
||||
|
||||
let text = '';
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = new TextDecoder().decode(value);
|
||||
const lines = chunk.split('\n').filter(l => l.startsWith('data: '));
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6));
|
||||
if (data.content) text += data.content;
|
||||
if (data.done) break;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (text && newState.selection.from === from) {
|
||||
currentSuggestion = text;
|
||||
suggestionPos = { from, to: from + text.length };
|
||||
newState.apply(newState.tr.setMeta(INLINE_SUGGESTION_KEY, { suggestion: text, visible: true }));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Inline suggestion error:', e);
|
||||
}
|
||||
}, DEBOUNCE_MS);
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { createInlineSuggestionPlugin, INLINE_SUGGESTION_KEY };
|
||||
Reference in New Issue
Block a user