| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { slashFactory, SlashProvider } from '@milkdown/plugin-slash'; |
|
|
| import './model-slash.css'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| export const modelSlash = slashFactory('ModelCommands'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function createModelSlashPlugin({ getModels, onSlashCommand }) { |
| |
| const menu = document.createElement('div'); |
| menu.className = "slash-menu"; |
| |
| menu.style.display = 'none'; |
|
|
| |
| function rebuildMenu() { |
| menu.innerHTML = ''; |
|
|
| const availableModels = getModels(); |
|
|
| if (availableModels.length === 0) { |
| const noModels = document.createElement('div'); |
| noModels.className = "px-3 py-4 text-sm text-gray-500 text-center"; |
| noModels.textContent = "No models available"; |
| menu.appendChild(noModels); |
| return; |
| } |
|
|
| |
| const modelList = document.createElement('ul'); |
| modelList.className = 'model-list'; |
|
|
| availableModels.forEach((model, index) => { |
| const item = document.createElement('li'); |
| item.className = 'model-entry'; |
| item.dataset.modelId = model.id; |
|
|
| |
| const icon = document.createElement('span'); |
| icon.className = 'model-icon'; |
| icon.textContent = model.requiresAuth ? '🔒' : '🤖'; |
|
|
| |
| const textContainer = document.createElement('div'); |
| textContainer.className = 'model-text-container'; |
|
|
| const name = document.createElement('div'); |
| name.className = 'name'; |
| name.textContent = model.name; |
| textContainer.appendChild(name); |
|
|
| if (model.size) { |
| const subtitle = document.createElement('div'); |
| subtitle.className = 'size'; |
| subtitle.textContent = `(${model.size})`; |
| textContainer.appendChild(subtitle); |
| } |
|
|
| item.appendChild(icon); |
| item.appendChild(textContainer); |
|
|
| |
| if (model.requiresAuth) { |
| const authSpan = document.createElement('span'); |
| authSpan.className = 'auth'; |
| authSpan.textContent = "Auth Required"; |
| item.appendChild(authSpan); |
| } |
|
|
| modelList.appendChild(item); |
| }); |
|
|
| menu.appendChild(modelList); |
| } |
|
|
| |
|
|
| |
| let currentView = null; |
|
|
| |
| function hasTriggerSlash(view) { |
| if (!view) return false; |
| const { state } = view; |
| const { from } = state.selection; |
| if (from === 0) return false; |
| const $pos = state.doc.resolve(from); |
| |
| const prevChar = state.doc.textBetween(from - 1, from, '\n', '\n'); |
| if (prevChar !== '/') return false; |
| |
| const beforePrev = from - 2 >= 0 ? state.doc.textBetween(from - 2, from - 1, '\n', '\n') : ''; |
| if (beforePrev && /[\w/]/.test(beforePrev)) return false; |
| return true; |
| } |
|
|
| |
| function removeTriggerSlash(view) { |
| try { |
| if (!view) return; |
| const { state } = view; |
| const { from } = state.selection; |
| if (from === 0) return; |
| const prevChar = state.doc.textBetween(from - 1, from, '\n', '\n'); |
| if (prevChar === '/') { |
| const tr = state.tr.delete(from - 1, from); |
| view.dispatch(tr); |
| } |
| } catch (e) { |
| |
| } |
| } |
|
|
| |
| const provider = new SlashProvider({ |
| content: menu, |
| shouldShow(view) { |
| return hasTriggerSlash(view); |
| }, |
| offset: 15, |
| }); |
|
|
| |
| function onKeyDown(e) { |
| if (!e) return; |
| const key = e.key || e.keyCode; |
| if (key === 'Escape' || key === 'Esc' || key === 27) { |
| try { |
| provider.hide(); |
| removeTriggerSlash(currentView); |
| } catch (err) { |
| |
| } |
| } |
| } |
|
|
| document.addEventListener('keydown', onKeyDown); |
|
|
| |
| const slashConfig = (ctx) => { |
| ctx.set(modelSlash.key, { |
| view: () => ({ |
| update: (view, prevState) => { |
| currentView = view; |
| |
| rebuildMenu(); |
| provider.update(view, prevState); |
| if (hasTriggerSlash(view)) { |
| menu.style.display = ''; |
| } else { |
| menu.style.display = 'none'; |
| } |
| }, |
| destroy: () => { |
| provider.destroy(); |
| |
| document.removeEventListener('keydown', onKeyDown); |
| document.removeEventListener('mousedown', onOutsideMouseDown, true); |
| }, |
| }), |
| }); |
| }; |
|
|
| |
| function finalize() { |
| provider.hide(); |
| removeTriggerSlash(currentView); |
| menu.style.display = 'none'; |
| } |
|
|
| |
| const wrapped = onSlashCommand ? async (modelId) => { |
| try { |
| await onSlashCommand(modelId); |
| } catch (error) { |
| console.error('Error executing slash command:', error); |
| } |
| } : null; |
|
|
| |
| menu.addEventListener('click', async (e) => { |
| if (!e.target || !(e.target instanceof Element)) return; |
| const target = e.target.closest('li[data-model-id]'); |
| if (!target || !(target instanceof HTMLElement)) return; |
| const modelId = target.dataset.modelId; |
| if (!modelId) return; |
| |
| finalize(); |
| if (wrapped) await wrapped(modelId); |
| }); |
|
|
| |
| function onOutsideMouseDown(e) { |
| if (menu.style.display === 'none') return; |
| if (e.target instanceof Node && !menu.contains(e.target)) { |
| finalize(); |
| } |
| } |
| document.addEventListener('mousedown', onOutsideMouseDown, true); |
|
|
| return { |
| plugin: modelSlash, |
| config: slashConfig |
| }; |
| } |
|
|