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, }; let res; let fetchError = null; try { res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body), }); } catch (e) { fetchError = e; } if (fetchError) { // Network-level error (e.g. Failed to fetch, CORS, DNS, etc.) throw new DetailedApiError({ message: fetchError.message || "Failed to fetch", url, prompt, model: store.model, apiUrl: store.apiUrl, status: null, responseBody: null, timestamp: new Date().toISOString(), cause: fetchError, }); } if (!res.ok) { let responseBody = null; let detail = `API returned ${res.status}`; try { responseBody = await res.text(); try { const err = JSON.parse(responseBody); detail = err.error?.message || err.detail || detail; } catch {} } catch {} throw new DetailedApiError({ message: detail, url, prompt, model: store.model, apiUrl: store.apiUrl, status: res.status, responseBody, timestamp: new Date().toISOString(), cause: null, }); } const data = await res.json(); const text = data?.choices?.[0]?.message?.content || ""; return text; } // ── Detailed error class ───────────────────────────────────────── class DetailedApiError extends Error { constructor(details) { super(details.message); this.name = "DetailedApiError"; this.url = details.url; this.prompt = details.prompt; this.model = details.model; this.apiUrl = details.apiUrl; this.status = details.status; this.responseBody = details.responseBody; this.timestamp = details.timestamp; this.cause = details.cause; } } // ── Error detail modal ────────────────────────────────────────── function ErrorDetailModal(props) { const error = props.error; return ( Unsloth Chat - Error Details
{/* Error message */}
Error
{error.message}
{/* Status code */}
HTTP Status
{error.status !== null ? error.status : "N/A (network error)"}
{/* API URL used */}
Request URL
{error.url}
{/* Configured API URL */}
Configured API URL
{error.apiUrl}
{/* Model */}
Model
{error.model}
{/* Prompt */}
Prompt
{error.prompt}
{/* Response body */} {error.responseBody && (
Response Body
{error.responseBody}
)} {/* Cause (network error details) */} {error.cause && (
Network Error
{error.cause.toString()}
)} {/* Timestamp */}
Occurred at: {error.timestamp}
); } // ── 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) { if (e instanceof DetailedApiError) { openModal((p) => ); } else { 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 ( Unsloth Studio
Prompt