✨ | Move to + menu, add Suggest reply from recent messages
This commit is contained in:
@@ -20,6 +20,7 @@ const {
|
|||||||
ToastColors,
|
ToastColors,
|
||||||
injectCss,
|
injectCss,
|
||||||
},
|
},
|
||||||
|
flux: { stores },
|
||||||
} = shelter;
|
} = shelter;
|
||||||
|
|
||||||
const UNSLOTH_DEFAULT_URL = "http://localhost:8000";
|
const UNSLOTH_DEFAULT_URL = "http://localhost:8000";
|
||||||
@@ -32,39 +33,38 @@ store.model ??= UNSLOTH_MODEL;
|
|||||||
store.maxTokens ??= 1024;
|
store.maxTokens ??= 1024;
|
||||||
store.temperature ??= 0.7;
|
store.temperature ??= 0.7;
|
||||||
|
|
||||||
// ── Injected CSS for the button ─────────────────────────────────
|
// ── Injected CSS for the menu items ─────────────────────────────
|
||||||
const removeCss = injectCss(`
|
const removeCss = injectCss(`
|
||||||
[data-unsloth-chat-btn] {
|
[data-unsloth-menu-item] {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
justify-content: center !important;
|
gap: 8px !important;
|
||||||
width: 32px !important;
|
padding: 6px 10px !important;
|
||||||
height: 32px !important;
|
margin: 2px 6px !important;
|
||||||
min-width: 32px !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
font-size: 16px !important;
|
border-radius: 3px !important;
|
||||||
font-weight: 700 !important;
|
|
||||||
color: var(--interactive-normal) !important;
|
color: var(--interactive-normal) !important;
|
||||||
background: transparent !important;
|
font-size: 14px !important;
|
||||||
border: none !important;
|
line-height: 18px !important;
|
||||||
padding: 0 !important;
|
font-weight: 500 !important;
|
||||||
transition: color 0.15s ease, background 0.15s ease !important;
|
|
||||||
line-height: 1 !important;
|
|
||||||
margin: 0 2px !important;
|
|
||||||
order: 10 !important;
|
|
||||||
}
|
}
|
||||||
[data-unsloth-chat-btn]:hover {
|
[data-unsloth-menu-item]:hover {
|
||||||
color: var(--interactive-hover) !important;
|
|
||||||
background: var(--background-modifier-hover) !important;
|
background: var(--background-modifier-hover) !important;
|
||||||
|
color: var(--interactive-hover) !important;
|
||||||
}
|
}
|
||||||
[data-unsloth-chat-btn] svg {
|
[data-unsloth-menu-item] svg {
|
||||||
width: 20px !important;
|
width: 18px !important;
|
||||||
height: 20px !important;
|
height: 18px !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
scoped.onDispose(() => removeCss());
|
scoped.onDispose(() => removeCss());
|
||||||
|
|
||||||
|
// ── SVG icons ───────────────────────────────────────────────────
|
||||||
|
const SPARKLE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.5 5.5L19 10l-5.5 1.5L12 17l-1.5-5.5L5 10l5.5-1.5z"/><path d="M6 14l.5 2L8 16.5 6.5 16 6 18l-.5-2L4 16.5 5.5 16z"/><path d="M16 16l.5 2 1.5-.5-1.5-.5.5-2-.5 2-1.5.5z"/></svg>`;
|
||||||
|
|
||||||
|
const REPLY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M4 12h14a3 3 0 0 1 3 3v4"/></svg>`;
|
||||||
|
|
||||||
// ── Helper: find and fill the message input ─────────────────────
|
// ── Helper: find and fill the message input ─────────────────────
|
||||||
function setMessageInput(text) {
|
function setMessageInput(text) {
|
||||||
// Broad selectors for Discord's chat input (textarea or contenteditable div)
|
// Broad selectors for Discord's chat input (textarea or contenteditable div)
|
||||||
@@ -431,7 +431,7 @@ function ErrorDetailModal(props) {
|
|||||||
|
|
||||||
// ── The prompt modal ────────────────────────────────────────────
|
// ── The prompt modal ────────────────────────────────────────────
|
||||||
function PromptModal(props) {
|
function PromptModal(props) {
|
||||||
const [prompt, setPrompt] = shelter.solid.createSignal("");
|
const [prompt, setPrompt] = shelter.solid.createSignal(props.initialPrompt || "");
|
||||||
const [loading, setLoading] = shelter.solid.createSignal(false);
|
const [loading, setLoading] = shelter.solid.createSignal(false);
|
||||||
const [error, setError] = shelter.solid.createSignal("");
|
const [error, setError] = shelter.solid.createSignal("");
|
||||||
|
|
||||||
@@ -544,38 +544,164 @@ function PromptModal(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Insert the button into the chat bar ─────────────────────────
|
// ── Flux helpers: get current channel & messages ─────────────────
|
||||||
function injectButton() {
|
function getCurrentChannelId() {
|
||||||
// Look for the button row in the chat bar
|
try {
|
||||||
const buttonRow = document.querySelector(
|
// Try SelectedChannelStore first (most common)
|
||||||
'[class*="channelTextArea"] [class*="buttons"], ' +
|
const chan = stores.SelectedChannelStore || stores.ChannelStore;
|
||||||
'[class*="chat"] [class*="channelTextArea"] [class*="buttons"]'
|
if (chan && typeof chan.getChannelId === "function") {
|
||||||
);
|
return chan.getChannelId();
|
||||||
|
}
|
||||||
if (!buttonRow || buttonRow.querySelector("[data-unsloth-chat-btn]")) return;
|
} catch (_) {}
|
||||||
|
// Fallback: parse URL
|
||||||
const btn = document.createElement("button");
|
try {
|
||||||
btn.setAttribute("data-unsloth-chat-btn", "");
|
const m = window.location.pathname.match(/\/channels\/(\d+|@me)\/(\d+)/);
|
||||||
btn.setAttribute("aria-label", "Unsloth Studio - Generate with AI");
|
if (m) return m[2];
|
||||||
btn.setAttribute("tabindex", "0");
|
} catch (_) {}
|
||||||
btn.innerHTML = `
|
return null;
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
}
|
||||||
<path d="M12 2a10 10 0 0 1 10 10c0 5-4 8-10 8S2 17 2 12 7 2 12 2z"/>
|
|
||||||
<path d="M8 12h8"/>
|
function getRecentMessages(count = 10) {
|
||||||
<path d="M12 8v8"/>
|
const channelId = getCurrentChannelId();
|
||||||
</svg>
|
if (!channelId) return [];
|
||||||
`;
|
|
||||||
btn.addEventListener("click", () => {
|
try {
|
||||||
openModal((p) => <PromptModal close={p.close} />);
|
const MessageStore = stores.MessageStore;
|
||||||
});
|
if (!MessageStore || typeof MessageStore.getMessages !== "function") return [];
|
||||||
|
const coll = MessageStore.getMessages(channelId);
|
||||||
buttonRow.appendChild(btn);
|
if (!coll) return [];
|
||||||
|
const arr = typeof coll.toArray === "function" ? coll.toArray() : Array.from(coll);
|
||||||
|
return arr.filter(m => m && m.content != null).slice(-count);
|
||||||
|
} catch (_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Suggest reply from recent messages ──────────────────────────
|
||||||
|
async function suggestReply() {
|
||||||
|
const messages = getRecentMessages(10);
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
showToast({
|
||||||
|
title: "Unsloth Chat",
|
||||||
|
content: "No recent messages found in this channel.",
|
||||||
|
color: ToastColors.WARNING,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build conversation transcript
|
||||||
|
const convo = messages.map(m => {
|
||||||
|
const author = m.author?.username || m.author?.globalName || "Unknown";
|
||||||
|
const content = (m.content || "").trim() || "(attachment)";
|
||||||
|
return `${author}: ${content}`;
|
||||||
|
}).join("\n");
|
||||||
|
|
||||||
|
const prompt =
|
||||||
|
"The following is a recent conversation in a Discord channel. " +
|
||||||
|
"Write a short, natural, contextually appropriate reply that " +
|
||||||
|
"continues this conversation. Just write the reply message, " +
|
||||||
|
"nothing else.\n\n---\n" + convo + "\n---\n\nReply:";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await callUnsloth(prompt);
|
||||||
|
if (result) {
|
||||||
|
const ok = setMessageInput(result);
|
||||||
|
if (ok) {
|
||||||
|
showToast({
|
||||||
|
title: "Unsloth Chat",
|
||||||
|
content: "Suggested reply inserted!",
|
||||||
|
color: ToastColors.SUCCESS,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showToast({
|
||||||
|
title: "Unsloth Chat",
|
||||||
|
content: "Could not find message input. Response copied to clipboard.",
|
||||||
|
color: ToastColors.WARNING,
|
||||||
|
});
|
||||||
|
try { await navigator.clipboard.writeText(result); } catch {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast({
|
||||||
|
title: "Unsloth Chat",
|
||||||
|
content: "The model returned an empty response.",
|
||||||
|
color: ToastColors.WARNING,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof DetailedApiError) {
|
||||||
|
openModal((p) => <ErrorDetailModal close={p.close} error={e} />);
|
||||||
|
} else {
|
||||||
|
showToast({
|
||||||
|
title: "Unsloth Chat",
|
||||||
|
content: e.message || "An error occurred",
|
||||||
|
color: ToastColors.DANGER || ToastColors.WARNING,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Menu item factory ───────────────────────────────────────────
|
||||||
|
function createMenuItem(label, iconSvg, onClick) {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.setAttribute("data-unsloth-menu-item", "");
|
||||||
|
el.setAttribute("role", "menuitem");
|
||||||
|
el.setAttribute("tabindex", "0");
|
||||||
|
el.innerHTML = iconSvg + `<span>${label}</span>`;
|
||||||
|
el.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick();
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Inject items into the + menu popout ─────────────────────────
|
||||||
|
function injectPlusMenuItems() {
|
||||||
|
// Look for open menu scrollers — the + popout is a menu layer
|
||||||
|
const scrollers = document.querySelectorAll('[class*="menu"] [class*="scroller"]');
|
||||||
|
for (const scroller of scrollers) {
|
||||||
|
// Already injected?
|
||||||
|
if (scroller.querySelector("[data-unsloth-menu-item]")) continue;
|
||||||
|
|
||||||
|
// Identify the + menu: it contains items for upload / create thread etc.
|
||||||
|
// We look for any element that hints this is the channel-attach popout.
|
||||||
|
const hint =
|
||||||
|
scroller.querySelector('[class*="upload"], [class*="attach"]') ||
|
||||||
|
scroller.closest('[class*="channelTextArea"]');
|
||||||
|
if (!hint) continue;
|
||||||
|
|
||||||
|
// Append a divider then our two items
|
||||||
|
const divider = document.createElement("div");
|
||||||
|
divider.style.cssText =
|
||||||
|
"margin:4px 6px;border-top:1px solid var(--background-modifier-accent);";
|
||||||
|
|
||||||
|
const askAi = createMenuItem("Ask AI", SPARKLE_ICON, () => {
|
||||||
|
openModal((p) => <PromptModal close={p.close} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const suggest = createMenuItem("Suggest reply", REPLY_ICON, () => {
|
||||||
|
suggestReply();
|
||||||
|
});
|
||||||
|
|
||||||
|
scroller.appendChild(divider);
|
||||||
|
scroller.appendChild(askAi);
|
||||||
|
scroller.appendChild(suggest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hook the + button to inject items when popout opens ──────────
|
||||||
|
function hookAttachButton(btn) {
|
||||||
|
if (btn.dataset.unslothHooked) return;
|
||||||
|
btn.dataset.unslothHooked = "1";
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
// The popout renders asynchronously, so wait a tick
|
||||||
|
setTimeout(injectPlusMenuItems, 50);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── DOM observer ────────────────────────────────────────────────
|
|
||||||
scoped.observeDom(
|
scoped.observeDom(
|
||||||
'[class*="channelTextArea"] [class*="buttons"]',
|
'[class*="channelTextArea"] [class*="attachButton"]',
|
||||||
injectButton,
|
hookAttachButton,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Export settings component ───────────────────────────────────
|
// ── Export settings component ───────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user