|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
}; |
|
} |
|
|