const {
plugin: { store, scoped },
ui: {
openModal,
Button,
ButtonColors,
ButtonLooks,
TextBox,
TextArea,
ModalRoot,
ModalHeader,
ModalBody,
ModalFooter,
Header,
HeaderTags,
Text,
TextTags,
Divider,
showToast,
ToastColors,
injectCss,
},
} = shelter;
const UNSLOTH_DEFAULT_URL = "http://localhost:8000";
const UNSLOTH_MODEL = "default";
// ── Default settings ────────────────────────────────────────────
store.apiUrl ??= UNSLOTH_DEFAULT_URL;
store.apiKey ??= "";
store.model ??= UNSLOTH_MODEL;
store.maxTokens ??= 1024;
store.temperature ??= 0.7;
// ── Injected CSS for the button ─────────────────────────────────
const removeCss = injectCss(`
[data-unsloth-chat-btn] {
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;
cursor: pointer !important;
font-size: 16px !important;
font-weight: 700 !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;
}
[data-unsloth-chat-btn]:hover {
color: var(--interactive-hover) !important;
background: var(--background-modifier-hover) !important;
}
[data-unsloth-chat-btn] svg {
width: 20px !important;
height: 20px !important;
}
`);
scoped.onDispose(() => removeCss());
// ── Helper: find and fill the message input ─────────────────────
function setMessageInput(text) {
// Find the Discord chat textarea
const textarea = document.querySelector(
'main [class*="channelTextArea"] textarea, ' +
'main [class*="channelTextArea"] [role="textbox"], ' +
'[class*="chat"] [class*="channelTextArea"] textarea, ' +
'[class*="chat"] [class*="channelTextArea"] [role="textbox"]'
);
if (!textarea) return false;
// The native textarea (or contenteditable div) inside
const nativeInput = textarea.tagName === "TEXTAREA"
? textarea
: textarea.querySelector("textarea");
if (!nativeInput) return false;
// Set the native value
nativeInput.focus();
nativeInput.value = text;
// Dispatch an input event so React picks up the change
const nativeInputEv = new Event("input", { bubbles: true, cancelable: true });
nativeInput.dispatchEvent(nativeInputEv);
// Also dispatch on the outer element if it's a contenteditable
if (textarea !== nativeInput && textarea.isContentEditable) {
textarea.textContent = text;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
}
return true;
}
// ── API call ────────────────────────────────────────────────────
async function callUnsloth(prompt) {
const url = `${store.apiUrl.replace(/\/+$/, "")}/v1/chat/completions`;
const headers = { "Content-Type": "application/json" };
if (store.apiKey) {
headers["Authorization"] = `Bearer ${store.apiKey}`;
}
const body = {
model: store.model,
messages: [{ role: "user", content: prompt }],
stream: false,
max_tokens: store.maxTokens,
temperature: store.temperature,
};
const res = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!res.ok) {
let detail = `API returned ${res.status}`;
try {
const err = await res.json();
detail = err.error?.message || err.detail || detail;
} catch {}
throw new Error(detail);
}
const data = await res.json();
const text = data?.choices?.[0]?.message?.content || "";
return text;
}
// ── The prompt modal ────────────────────────────────────────────
function PromptModal(props) {
const [prompt, setPrompt] = shelter.solid.createSignal("");
const [loading, setLoading] = shelter.solid.createSignal(false);
const [error, setError] = shelter.solid.createSignal("");
let inputRef = undefined;
let confirmRef = undefined;
shelter.solid.onMount(() => {
if (inputRef) inputRef.focus();
});
async function handleSubmit() {
const p = prompt().trim();
if (!p) return;
setLoading(true);
setError("");
try {
const result = await callUnsloth(p);
if (result) {
const ok = setMessageInput(result);
if (ok) {
showToast({
title: "Unsloth Chat",
content: "Response inserted into message input!",
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 {
setError("The model returned an empty response.");
}
props.close();
} catch (e) {
setError(e.message || "An error occurred");
} finally {
setLoading(false);
}
}
function handleKeyDown(e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
if (e.key === "Escape") {
props.close();
}
}
return (