diff --git a/plugins/unsloth-chat/index.jsx b/plugins/unsloth-chat/index.jsx
index 945d8a4..1437a82 100644
--- a/plugins/unsloth-chat/index.jsx
+++ b/plugins/unsloth-chat/index.jsx
@@ -20,6 +20,7 @@ const {
ToastColors,
injectCss,
},
+ flux: { stores },
} = shelter;
const UNSLOTH_DEFAULT_URL = "http://localhost:8000";
@@ -32,39 +33,38 @@ store.model ??= UNSLOTH_MODEL;
store.maxTokens ??= 1024;
store.temperature ??= 0.7;
-// ── Injected CSS for the button ─────────────────────────────────
+// ── Injected CSS for the menu items ─────────────────────────────
const removeCss = injectCss(`
- [data-unsloth-chat-btn] {
+ [data-unsloth-menu-item] {
display: flex !important;
align-items: center !important;
- justify-content: center !important;
- width: 32px !important;
- height: 32px !important;
- min-width: 32px !important;
- border-radius: 8px !important;
+ gap: 8px !important;
+ padding: 6px 10px !important;
+ margin: 2px 6px !important;
cursor: pointer !important;
- font-size: 16px !important;
- font-weight: 700 !important;
+ border-radius: 3px !important;
color: var(--interactive-normal) !important;
- background: transparent !important;
- border: none !important;
- padding: 0 !important;
- transition: color 0.15s ease, background 0.15s ease !important;
- line-height: 1 !important;
- margin: 0 2px !important;
- order: 10 !important;
+ font-size: 14px !important;
+ line-height: 18px !important;
+ font-weight: 500 !important;
}
- [data-unsloth-chat-btn]:hover {
- color: var(--interactive-hover) !important;
+ [data-unsloth-menu-item]:hover {
background: var(--background-modifier-hover) !important;
+ color: var(--interactive-hover) !important;
}
- [data-unsloth-chat-btn] svg {
- width: 20px !important;
- height: 20px !important;
+ [data-unsloth-menu-item] svg {
+ width: 18px !important;
+ height: 18px !important;
+ flex-shrink: 0 !important;
}
`);
scoped.onDispose(() => removeCss());
+// ── SVG icons ───────────────────────────────────────────────────
+const SPARKLE_ICON = ``;
+
+const REPLY_ICON = ``;
+
// ── Helper: find and fill the message input ─────────────────────
function setMessageInput(text) {
// Broad selectors for Discord's chat input (textarea or contenteditable div)
@@ -431,7 +431,7 @@ function ErrorDetailModal(props) {
// ── The prompt modal ────────────────────────────────────────────
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 [error, setError] = shelter.solid.createSignal("");
@@ -544,38 +544,164 @@ function PromptModal(props) {
);
}
-// ── Insert the button into the chat bar ─────────────────────────
-function injectButton() {
- // Look for the button row in the chat bar
- const buttonRow = document.querySelector(
- '[class*="channelTextArea"] [class*="buttons"], ' +
- '[class*="chat"] [class*="channelTextArea"] [class*="buttons"]'
- );
-
- if (!buttonRow || buttonRow.querySelector("[data-unsloth-chat-btn]")) return;
-
- const btn = document.createElement("button");
- btn.setAttribute("data-unsloth-chat-btn", "");
- btn.setAttribute("aria-label", "Unsloth Studio - Generate with AI");
- btn.setAttribute("tabindex", "0");
- btn.innerHTML = `
-
- `;
- btn.addEventListener("click", () => {
- openModal((p) => );
- });
-
- buttonRow.appendChild(btn);
+// ── Flux helpers: get current channel & messages ─────────────────
+function getCurrentChannelId() {
+ try {
+ // Try SelectedChannelStore first (most common)
+ const chan = stores.SelectedChannelStore || stores.ChannelStore;
+ if (chan && typeof chan.getChannelId === "function") {
+ return chan.getChannelId();
+ }
+ } catch (_) {}
+ // Fallback: parse URL
+ try {
+ const m = window.location.pathname.match(/\/channels\/(\d+|@me)\/(\d+)/);
+ if (m) return m[2];
+ } catch (_) {}
+ return null;
+}
+
+function getRecentMessages(count = 10) {
+ const channelId = getCurrentChannelId();
+ if (!channelId) return [];
+
+ try {
+ const MessageStore = stores.MessageStore;
+ if (!MessageStore || typeof MessageStore.getMessages !== "function") return [];
+ const coll = MessageStore.getMessages(channelId);
+ 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) => );
+ } 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 + `${label}`;
+ 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) => );
+ });
+
+ 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(
- '[class*="channelTextArea"] [class*="buttons"]',
- injectButton,
+ '[class*="channelTextArea"] [class*="attachButton"]',
+ hookAttachButton,
);
// ── Export settings component ───────────────────────────────────