// Built via el() rather than as a raw HTML block: this document mixes
// executable ojs cells with plain markdown, and pandoc's raw-HTML-block
// passthrough is unreliable in that mix, so every other custom-styled piece
// on this page already goes through el() — this keeps that one convention.
stepsIntro = el("div", { class: "gaidet-steps" }, [
el("div", { class: "gaidet-step" }, [
el("div", { class: "gaidet-step-number" }, "1"),
el("div", { class: "gaidet-step-body" }, [
el("h3", {}, "Choose what is allowed"),
el("p", {}, "Tick the categories of AI use your assignment or module permits.")
])
]),
el("div", { class: "gaidet-step" }, [
el("div", { class: "gaidet-step-number" }, "2"),
el("div", { class: "gaidet-step-body" }, [
el("h3", {}, "Describe your AI use"),
el("p", {}, "For each task you used AI for, name the tool, explain what you did, and how you validated the output.")
])
]),
el("div", { class: "gaidet-step" }, [
el("div", { class: "gaidet-step-number" }, "3"),
el("div", { class: "gaidet-step-body" }, [
el("h3", {}, "Automatically generate your declaration"),
el("p", {}, "Review declaration, then copy it into your assignment.")
])
])
])AI Disclosure Generator
An open-source tool that helps students create structured, task-by-task AI-use disclosure statements for assignment submissions. It’s free, runs entirely in your browser, and never sends anything you enter to a server.
About this tool
The use of AI tools among students continues to grow. Some assignments prohibit generative AI entirely, while others allow it but require students to disclose how it was used. Clear guidance on how to do this is not always available.
If your assignment permits AI use but requires disclosure, this tool can help. It is adapted for student assignments from the GAIDeT Declaration Generator and the Artificial Intelligence Disclosure (AID) Framework.
This tool is built specifically for student coursework. If you’re a researcher looking to document AI use in a publication or grant application, check out the related literature and tools below for options aimed at research rather than teaching.
exampleDeclarationHtml = `<p><strong>Declaration on the Use of Artificial Intelligence Tools</strong></p>
<p><strong>AI tools used:</strong> Claude (Sonnet 5), Claude Code (Sonnet 4.6), Microsoft Copilot (GPT-5.5)</p>
<p>The core analysis, writing, and conclusions in this assignment are my own. Generative AI assisted with the following specific, limited tasks:</p>
<ul>
<li><strong>Clarifying definitions or concepts</strong><br>Tool: Claude (Sonnet 5)<br>Summary: I asked Claude to explain a few concepts I was unsure about, then checked the literature it pointed me to against the original sources before relying on it.</li>
<li><strong>Code review</strong><br>Tool: Claude Code (Sonnet 4.6)<br>Summary: I wrote the core analysis logic myself; Claude Code was used only to draft repetitive boilerplate (e.g. file-loading functions), which I reviewed, tested, and adapted to my dataset.</li>
<li><strong>Proofreading</strong><br>Tool: Microsoft Copilot (GPT-5.5)<br>Summary: After completing my own final draft, I asked Copilot to flag grammar and punctuation issues, and manually reviewed and accepted only the suggestions I agreed with.</li>
</ul>
<p>The author confirms that this AI use falls within what was permitted for this assignment, and complies with any other applicable rules, such as their institution's Declaration of Authorship.</p>
<p>Responsibility for the submitted assignment lies entirely with the author of this declaration.</p>
<p>Generative AI tools cannot be listed as authors and do not bear responsibility for the final outcomes.</p>`{
const wrap = el("div", { class: "gaidet-example-wrap" }, [
el("button", {
class: "gaidet-example-toggle",
onclick: () => { mutable showExample = !showExample; }
}, showExample ? "Hide an example declaration" : "Show an example declaration")
]);
if (showExample) {
const example = el("div", { class: "gaidet-example" }, []);
example.innerHTML = exampleDeclarationHtml;
wrap.append(example);
}
return wrap;
}
Warning
Many universities do not have a single institution-wide policy on generative AI. Permission varies by Department, School, programme, and even individual module or assessment. Before using any AI tool, check your module descriptor and Learning Management System (LMS) course site, and ask your lecturer whether (and how) it may be used. Some assignments may prohibit generative AI entirely, or allow AI use only for a subset of tasks. Some institutions also limit which AI tools students may use, for example restricting you to a specific, approved list.
Disclosing a task or AI tool here does not constitute permission to use it, nor a means of circumventing any such prohibition imposed by your institution, School, programme, module, or assessment brief. You are ultimately responsible for complying with your institution’s and module’s policies — including any separate declaration of authorship or originality statement you are required to sign, which may itself restrict how AI can be used (for example, by requiring that you have personally read everything you cite); see the full disclaimer at the end of this page for details.
Using generative AI responsibly also means applying quality control: AI is known to hallucinate, presenting false information, fabricated references, or biased output, so verify anything you rely on against reliable sources before submitting. It also means complying with your own institution’s policies. For an example of what institution-specific AI guidance looks like, see UCD Library’s guide to using generative AI.
function el(tag, props = {}, children = []) {
const node = document.createElement(tag);
for (const [k, v] of Object.entries(props)) {
if (v === undefined || v === null) continue;
if (k.startsWith("on") && typeof v === "function") {
node.addEventListener(k.slice(2), v);
} else if (k === "checked" || k === "value" || k === "disabled") {
node[k] = v;
} else if (k === "class") {
node.className = v;
} else {
node.setAttribute(k, v);
}
}
for (const child of [].concat(children)) {
if (child === null || child === undefined) continue;
node.append(child instanceof Node ? child : document.createTextNode(child));
}
return node;
}
// Cabin's free tier only reports page views, not custom events, so this
// piggybacks a "did someone actually generate a declaration" conversion
// signal on top of page-view tracking: a hidden iframe requests a dummy
// static page (count/generate-declaration.html) that carries Cabin's
// tracking script, and Cabin logs that as a page view. It's not dead code —
// see the "Analytics" section of the Privacy policy below, which discloses
// this. Fires once per confirmed declaration (not on "Update declaration").
function trackGenerateClick() {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'https://disclose-ai.app/count/generate-declaration.html?t=' + Date.now();
document.body.appendChild(iframe);
setTimeout(() => iframe.remove(), 3000);
}
function taskIsComplete(sentence, tools, openEnded, customLabel) {
return !!(sentence && sentence.trim())
&& Object.keys(tools ?? {}).length > 0
&& (!openEnded || !!(customLabel && customLabel.trim()));
}
function countWords(text) {
return (text || "").trim().split(/\s+/).filter(Boolean).length;
}
function autoGrow(textarea, attempts = 0) {
if (!textarea.isConnected) {
if (attempts < 30) requestAnimationFrame(() => autoGrow(textarea, attempts + 1));
return;
}
// Deferred to the next frame so the keystroke itself paints immediately;
// reading scrollHeight synchronously here would force a layout reflow
// before the browser can render the typed character.
if (textarea._autoGrowRaf) cancelAnimationFrame(textarea._autoGrowRaf);
textarea._autoGrowRaf = requestAnimationFrame(() => {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
});
}
function preserveFocus(build) {
const active = document.activeElement;
const field = active?.dataset?.field;
const selStart = active?.selectionStart ?? null;
const selEnd = active?.selectionEnd ?? null;
// Where the field sits on screen right now, not the page's absolute scroll
// position: restoring the old absolute scrollY fought the new layout when
// an edit changed a box's height, but not correcting for anything at all
// lets the browser's own scroll-anchoring reset to the top of this whole
// block on every rebuild. Keeping the field at the same viewport-relative
// spot avoids both.
const beforeTop = active?.getBoundingClientRect?.().top ?? null;
const node = build();
if (field) {
let attempts = 0;
const refocus = () => {
attempts += 1;
const next = node.querySelector(`[data-field="${CSS.escape(field)}"]`);
if (!next) return;
if (!next.isConnected) {
if (attempts < 30) requestAnimationFrame(refocus);
return;
}
next.focus({ preventScroll: true });
if (selStart !== null && typeof next.setSelectionRange === "function") {
next.setSelectionRange(selStart, selEnd);
}
// One more frame before correcting scroll: Safari can apply its own
// scroll adjustment on focus a tick after preventScroll would suggest,
// so measuring immediately can get overridden right after by Safari's
// own late adjustment. Waiting a frame lets that settle first.
if (beforeTop !== null) {
requestAnimationFrame(() => {
const afterTop = next.getBoundingClientRect().top;
window.scrollBy(0, afterTop - beforeTop);
});
}
};
requestAnimationFrame(refocus);
}
return node;
}
// Shared "not available yet" treatment: dims contentEl, makes it fully
// inert, and adds an overlay that explains why on hover/focus. One
// implementation so every locked box looks the same; the message differs
// because a Step 2 category is gated only by Step 1, while Step 3 and the
// prompts question are gated by Step 2 (and transitively Step 1) instead.
function lockBox(contentEl, message = "Locked. Select the relevant categories in Step 1 above first.") {
contentEl.classList.add("gaidet-locked");
contentEl.setAttribute("inert", "");
// Small solid-padlock glyph, drawn inline (no icon font/package) so it
// just inherits text color via currentColor. Purely decorative — the
// accessible name still comes from the plain-text `message` below.
const lockIcon = el("span", { class: "gaidet-lock-icon", "aria-hidden": "true" });
lockIcon.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V11a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5zm-3 8V6a3 3 0 0 1 6 0v3zm3 4a2 2 0 0 1 1 3.73V19a1 1 0 1 1-2 0v-2.27A2 2 0 0 1 12 13z"/></svg>`;
const lockMessage = el("div", { class: "gaidet-lock-message" }, [lockIcon, message]);
const overlay = el("div", {
class: "gaidet-lock-overlay",
tabindex: "0",
"aria-label": message
});
return el("div", { class: "gaidet-lock-wrap" }, [contentEl, overlay, lockMessage]);
}{
document.addEventListener("click", (e) => {
for (const { input, closeList } of autocompletePickers.values()) {
if (!input.parentElement?.contains(e.target)) closeList();
}
});
}// "" means not specified. Independent of allowedCategories: it never locks
// or unlocks anything below, it's just an optional label carried into the
// declaration, so it's safe to change back and forth at any time.
mutable aiasLevel = ""// Empty by default: a category only unlocks once a student actively ticks
// it as permitted, rather than assuming everything is allowed unless
// deselected.
mutable allowedCategories = new Set()// When true, the assignment/module gives no guidance at all on AI use.
// This overrides allowedCategories: the category gate is disabled and
// cleared, and a warning tells the student to check with their lecturer
// before using this tool, rather than letting them guess at what's allowed.
mutable noGuidance = false{
const hasData = Object.keys(selections).length > 0 || !!aiasLevel;
window.onbeforeunload = hasData ? (e) => { e.preventDefault(); e.returnValue = ""; } : null;
}noVersionTools = new Set([
"Otter.ai",
"Grammarly",
"QuillBot",
"Canva",
"NotebookLM",
"Consensus",
"Elicit",
"Scite"
])promptsToggle = {
// Stays locked until there's at least one ticked task to attach prompts
// to, so it doesn't feel like an active choice before Step 2 has started.
const locked = Object.keys(selections).length === 0;
const box = el("div", { class: "gaidet-prompts-question" }, [
el("p", { class: "gaidet-tool-label" }, "Provide prompts used for each task?"),
el("p", { class: "gaidet-tool-instructions" }, "If you choose \"Yes\", an extra box will appear under each ticked task where you can copy-paste the exact prompt(s) you used with the AI tool, for extra transparency. This is optional and off by default, but some lecturers or assignment briefs may require you to include the full prompts."),
el("div", { class: "gaidet-tool-grid" }, [
el("label", { class: "gaidet-tool-option" }, [
el("input", {
type: "radio",
name: "gaidet-provide-prompts",
checked: providePrompts === true,
onchange: (e) => {
if (e.target.checked) {
mutable providePrompts = true;
document.getElementById("generate-declaration")?.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
}),
" Yes"
]),
el("label", { class: "gaidet-tool-option" }, [
el("input", {
type: "radio",
name: "gaidet-provide-prompts",
checked: providePrompts === false,
onchange: (e) => { if (e.target.checked) mutable providePrompts = false; }
}),
" No"
])
])
]);
// Just dimmed + inert, no hover explanation: this question is minor enough
// that a tooltip here would be more noise than help.
if (locked) {
box.classList.add("gaidet-locked");
box.setAttribute("inert", "");
}
return box;
}aiasSelector = {
// Clicking the already-selected level clears it, so there's always an
// obvious, one-click way back to "not specified" without a separate
// reset control.
function selectLevel(levelStr) {
mutable aiasLevel = (aiasLevel === levelStr) ? "" : levelStr;
}
const selectedEntry = data.aiasLevels.find(l => String(l.level) === aiasLevel);
// A single row of pills rather than five stacked cards: the full summary
// for each level lives in its tooltip, so comparing all five costs a
// hover instead of a page's worth of vertical space. Only the selected
// level's instruction is printed out below.
const options = data.aiasLevels.map(lvl => {
const levelStr = String(lvl.level);
const isSelected = aiasLevel === levelStr;
return el("span", { class: "gaidet-aias-option" }, [
el("button", {
type: "button",
class: isSelected ? "gaidet-aias-btn gaidet-aias-btn-selected" : "gaidet-aias-btn",
"aria-pressed": isSelected ? "true" : "false",
onclick: () => selectLevel(levelStr)
}, lvl.name),
el("span", {
class: "gaidet-info-icon",
tabindex: "0",
role: "img",
"aria-label": `What ${lvl.name} means`,
"data-tooltip": `${lvl.summary}\n\n${lvl.instruction}`
}, "ⓘ")
]);
});
return el("div", { class: "gaidet-aias-question" }, [
el("p", { class: "gaidet-tool-label" }, "AI Assessment Scale level (optional)"),
el("p", { class: "gaidet-tool-instructions" }, [
"If your assignment brief or module states a level from the ",
el("a", { href: "https://aiassessmentscale.com", target: "_blank", rel: "noopener" }, "AI Assessment Scale"),
", select the matching level below. It will be added to your declaration."
]),
el("div", { class: "gaidet-aias-list" }, options),
selectedEntry ? el("p", { class: "gaidet-aias-instruction" }, selectedEntry.instruction) : null
]);
}aiasLevelLabel = {
const entry = data.aiasLevels.find(l => String(l.level) === aiasLevel);
return entry ? `${entry.level} – ${entry.name}` : "";
}categoryGate = {
const allCatIds = data.categories.map(cat => cat.id);
const allSelected = allCatIds.every(id => allowedCategories.has(id));
const optionClass = (base, checked) =>
checked ? `${base} gaidet-category-gate-option-checked` : base;
const categoryOptions = data.categories.map(cat => {
const isAllowed = allowedCategories.has(cat.id);
return el("label", {
class: optionClass(
noGuidance ? "gaidet-category-gate-option gaidet-category-gate-option-disabled" : "gaidet-category-gate-option",
isAllowed
)
}, [
el("input", {
type: "checkbox",
class: "gaidet-checkbox-input",
checked: isAllowed,
disabled: noGuidance,
onchange: (e) => {
const next = new Set(allowedCategories);
if (e.target.checked) next.add(cat.id); else next.delete(cat.id);
mutable allowedCategories = next;
}
}),
el("span", { class: "gaidet-checkbox-box" }, []),
el("span", {}, cat.label),
// No tooltip for "Additional tasks": its only item is the open-ended
// catch-all itself, so a preview listing it back would be pointless.
cat.id === "additional_tasks" ? null : el("span", {
class: "gaidet-info-icon",
tabindex: "0",
role: "img",
"aria-label": `What ${cat.label} covers`,
"data-tooltip": `This category can include tasks such as:\n${cat.items.filter(i => i.id !== "other").map(i => `• ${i.label}`).join("\n")}`,
// Blocks the click from also toggling the parent <label>'s checkbox
// (the browser forwards any click inside a <label> to its control
// unless the event is cancelled), so tapping the icon just opens the
// tooltip instead of ticking/un-ticking the category by accident.
onclick: (e) => e.preventDefault()
}, "ⓘ")
]);
});
const selectAllOption = el("label", {
class: optionClass(
noGuidance
? "gaidet-category-gate-option gaidet-category-gate-option-all gaidet-category-gate-option-disabled"
: "gaidet-category-gate-option gaidet-category-gate-option-all",
allSelected
)
}, [
el("input", {
type: "checkbox",
class: "gaidet-checkbox-input",
checked: allSelected,
disabled: noGuidance,
onchange: (e) => {
mutable allowedCategories = e.target.checked ? new Set(allCatIds) : new Set();
}
}),
el("span", { class: "gaidet-checkbox-box" }, []),
el("span", {}, "My assignment allows using AI without limiting it to specific categories")
]);
const noGuidanceOption = el("label", {
class: optionClass("gaidet-category-gate-option gaidet-category-gate-option-noguidance", noGuidance)
}, [
el("input", {
type: "checkbox",
class: "gaidet-checkbox-input",
checked: noGuidance,
onchange: (e) => {
mutable noGuidance = e.target.checked;
if (e.target.checked) mutable allowedCategories = new Set();
}
}),
el("span", { class: "gaidet-checkbox-box" }, []),
el("span", {}, "My assignment/module gives no guidance at all on AI use")
]);
const noGuidanceWarning = noGuidance
? el("div", { class: "gaidet-category-gate-warning" }, [
el("strong", {}, "Don’t assume. "),
"No guidance does not mean AI use is allowed. Ask your module coordinator before using AI or generating a declaration, rather than assuming you can use it."
])
: null;
return el("div", { class: "gaidet-category-gate" }, [
el("p", { class: "gaidet-tool-label gaidet-step-label" }, "Step 1 (What is allowed): Which categories of AI use does your assignment permit?"),
el("p", { class: "gaidet-tool-instructions" }, "Check the categories your assignment brief or module instructions allow. Everything else stays locked below until you check it here, so tick only what is permitted."),
el("div", { class: "gaidet-category-gate-list" }, [...categoryOptions, selectAllOption, noGuidanceOption]),
noGuidanceWarning
]);
}step2Label = el("div", {}, [
el("p", { class: "gaidet-step-label" }, "Step 2 (What you used): Tick every task where you used AI"),
el("p", { class: "gaidet-tool-instructions" }, "Select and describe your use of AI for one or more tasks below.")
])form = {
// A working copy of `selections`, shared by every commit function created
// during this render. `selections` itself is frozen for the lifetime of this
// render (it won't reflect a commit made moments ago by this same render,
// even though the commit did trigger a future re-render), so committing from
// it directly risks one task's update clobbering another's when several
// fields across tasks are edited/committed in quick succession. Every commit
// below reads and writes `pendingSelections` instead, so they chain off each
// other correctly within this render, and only publish to the reactive
// `selections` (starting the next render) as their last step.
let pendingSelections = selections;
if (allowedCategories.size === 0) {
return el("div", { class: "gaidet-output-placeholder gaidet-output-placeholder-warning" }, [
el("p", {}, "Complete Step 1 above first. Once you tick the categories of AI use your assignment allows, they’ll appear here for you to describe.")
]);
}
return preserveFocus(() => el("div", {}, data.categories.filter(cat => allowedCategories.has(cat.id)).map(cat => {
const categoryEl = el("div", { class: "gaidet-category" }, [
el("h3", {}, cat.label),
...cat.items.map(item => {
const key = `${cat.id}::${item.id}`;
const checked = key in selections;
const sel = selections[key];
const isComplete = checked && taskIsComplete(sel.sentence, sel.tools, item.openEnded, sel.customLabel);
// Tracks this task's tools independently of `selections`, which is frozen
// for the lifetime of this render: a commit updates `selections` and
// schedules a full re-render, but doesn't retroactively unfreeze it for
// code (like markCompleteIfReady, below) still running in this same render.
let currentTools = sel?.tools ?? {};
function buildToolPicker() {
const toolIndex = data.aiToolGroups.flatMap(group =>
group.tools.map(tool => ({ name: tool, category: group.label, needsVersion: !noVersionTools.has(tool) }))
);
function currentSelected() {
return Object.entries(currentTools ?? {}).map(([name, val]) => ({
name,
needsVersion: typeof val === "object",
version: (val && typeof val === "object" && val.version) || ""
}));
}
function isSelected(name) {
return currentSelected().some(t => t.name.toLowerCase() === name.toLowerCase());
}
function commit(nextSelected) {
const next = { ...pendingSelections };
if (!next[key]) return;
const toolsMap = {};
nextSelected.forEach(t => {
toolsMap[t.name] = t.needsVersion ? { version: t.version || "" } : true;
});
currentTools = toolsMap;
next[key] = { ...next[key], tools: toolsMap };
pendingSelections = next;
// Update the visible chip list immediately, rather than waiting for
// the full task list to rebuild (which still happens, for the rest
// of the app to pick up the change, but no longer gates this box's
// own visual feedback).
renderSelected();
mutable selections = next;
}
function escapeRegExp(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function highlight(name, query) {
if (!query) return name;
const re = new RegExp(`(${escapeRegExp(query)})`, "ig");
return name.replace(re, "<mark>$1</mark>");
}
const hasSelection = currentSelected().length > 0;
const input = el("input", {
type: "text",
class: hasSelection ? "gaidet-autocomplete-input gaidet-autocomplete-input-more" : "gaidet-autocomplete-input",
"data-field": `toolSearch::${key}`,
"aria-label": hasSelection ? "Add another AI tool used for this task" : "Search for an AI tool",
placeholder: hasSelection ? "+ Add another tool" : "Start typing to add a tool…",
autocomplete: "off"
});
const list = el("ul", { class: "gaidet-autocomplete-list" }, []);
list.style.display = "none";
const selectedWrap = el("div", { class: "gaidet-autocomplete-selected" }, []);
let activeIndex = -1;
let currentMatches = [];
function closeList() {
list.style.display = "none";
list.innerHTML = "";
activeIndex = -1;
currentMatches = [];
}
function selectTool(entry) {
if (!isSelected(entry.name)) {
commit([...currentSelected(), { name: entry.name, needsVersion: entry.needsVersion, version: "" }]);
}
input.value = "";
closeList();
input.focus();
}
function renderList(query) {
const q = query.trim();
if (!q) { closeList(); return; }
const ql = q.toLowerCase();
const matches = toolIndex.filter(t => t.name.toLowerCase().includes(ql) && !isSelected(t.name));
list.innerHTML = "";
if (matches.length === 0) {
currentMatches = [{ name: q, needsVersion: true, isCustom: true }];
const li = el("li", { class: "gaidet-autocomplete-item gaidet-autocomplete-add-custom" }, []);
li.append("Add “", Object.assign(document.createElement("strong"), { textContent: q }), "” as a tool");
li.addEventListener("mousedown", (e) => { e.preventDefault(); selectTool(currentMatches[0]); });
list.append(li);
list.style.display = "block";
activeIndex = -1;
return;
}
currentMatches = matches;
matches.forEach(entry => {
const li = el("li", { class: "gaidet-autocomplete-item" }, []);
const nameSpan = el("span", {}, []);
nameSpan.innerHTML = highlight(entry.name, q);
const catSpan = el("span", { class: "gaidet-autocomplete-item-category" }, entry.category);
li.append(nameSpan, catSpan);
li.addEventListener("mousedown", (e) => { e.preventDefault(); selectTool(entry); });
list.append(li);
});
activeIndex = -1;
list.style.display = "block";
}
function setActive(i) {
const items = list.querySelectorAll(".gaidet-autocomplete-item");
items.forEach((node, idx) => node.classList.toggle("active", idx === i));
if (items[i]) items[i].scrollIntoView({ block: "nearest" });
}
input.addEventListener("input", (e) => renderList(e.target.value));
input.addEventListener("keydown", (e) => {
const items = list.querySelectorAll(".gaidet-autocomplete-item");
if (e.key === "ArrowDown") {
e.preventDefault();
if (!items.length) return;
activeIndex = (activeIndex + 1) % items.length;
setActive(activeIndex);
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (!items.length) return;
activeIndex = (activeIndex - 1 + items.length) % items.length;
setActive(activeIndex);
} else if (e.key === "Enter") {
e.preventDefault();
if (activeIndex >= 0 && currentMatches[activeIndex]) {
selectTool(currentMatches[activeIndex]);
} else if (currentMatches.length === 1) {
selectTool(currentMatches[0]);
}
} else if (e.key === "Escape") {
closeList();
}
});
autocompletePickers.set(key, { input, closeList });
function renderSelected() {
selectedWrap.innerHTML = "";
currentSelected().forEach(entry => {
const row = el("div", { class: "gaidet-autocomplete-chip" }, []);
const header = el("div", { class: "gaidet-autocomplete-chip-header" }, [
el("span", { class: "gaidet-autocomplete-chip-name" }, entry.name),
el("button", {
type: "button",
class: "gaidet-autocomplete-remove",
"aria-label": `Remove ${entry.name}`,
onclick: () => commit(currentSelected().filter(t => t.name !== entry.name))
}, "×")
]);
row.append(header);
if (entry.needsVersion) {
const suggestedVersion = (data.commonVersions ?? {})[entry.name];
row.append(el("div", { class: "gaidet-autocomplete-version-wrap" }, [
el("label", { class: "gaidet-autocomplete-version-label" }, [
`Model/version of ${entry.name} `,
el("span", {
class: "gaidet-info-icon",
tabindex: "0",
role: "img",
"aria-label": "Why this matters and how to find it",
"data-tooltip": "Capability can vary a lot between versions of the same tool, so the exact model helps others judge what it could and couldn't do.\n\nTo find it: check the tool's model picker/settings, or look at the chat window itself (e.g. ChatGPT shows the model above the chat box; Claude shows it in the model selector).\n\nIf you cannot identify the exact model or version, include the timeframe during which you used the tool (e.g., January 2026)."
}, "ⓘ")
]),
el("input", {
type: "text",
class: "gaidet-autocomplete-version-input",
"data-field": `toolVersion::${key}::${entry.name}`,
"aria-label": `${entry.name} model or version`,
placeholder: suggestedVersion ? `e.g., ${suggestedVersion}` : "Model/version",
value: entry.version,
onblur: (e) => {
commit(currentSelected().map(t => t.name === entry.name ? { ...t, version: e.target.value } : t));
},
onkeydown: (e) => {
if (e.key === "Enter") {
e.preventDefault();
// Blur fires naturally on focus-shift, committing the
// version via the handler above before we move on.
sentenceTextarea.focus();
sentenceTextarea.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
})
]));
}
selectedWrap.append(row);
});
}
renderSelected();
return el("div", { class: "gaidet-autocomplete" }, [
el("p", { class: "gaidet-tool-label" }, "AI tool(s) used for this task:"),
el("p", { class: "gaidet-tool-instructions" }, "Press Enter or click a suggestion to add a tool. If yours isn't listed, type its name and add it as a custom tool."),
el("div", { class: "gaidet-autocomplete-wrap" }, [input, list]),
selectedWrap
]);
}
const checkboxLabel = el("label", { class: "gaidet-item-checkbox-label" }, [
el("input", {
type: "checkbox",
class: "gaidet-checkbox-input",
checked,
onchange: (e) => {
const next = { ...pendingSelections };
if (e.target.checked) {
next[key] = {
label: item.label,
openEnded: item.openEnded ?? false,
customLabel: next[key]?.customLabel ?? "",
sentence: next[key]?.sentence ?? "",
prompts: next[key]?.prompts ?? "",
tools: next[key]?.tools ?? {},
proceeded: next[key]?.proceeded ?? false
};
} else {
delete next[key];
}
pendingSelections = next;
mutable selections = next;
}
}),
el("span", { class: "gaidet-checkbox-box" }, []),
el("span", { class: "gaidet-item-label" }, item.label),
isComplete ? el("span", { class: "gaidet-item-complete", "aria-label": "Sentence and tool provided" }, " ✓") : null
]);
const sentenceTextarea = el("textarea", {
class: "gaidet-sentence",
"data-field": `sentence::${key}`,
"aria-label": "Description of what you asked the AI to do and how you used and validated the output",
placeholder: "What did you ask the AI to do, and how did you use and validate the output?",
maxlength: "3000",
value: sel?.sentence ?? "",
onblur: (e) => {
const next = { ...pendingSelections };
if (next[key]) {
next[key] = { ...next[key], sentence: e.target.value };
pendingSelections = next;
mutable selections = next;
}
}
});
function markCompleteIfReady() {
// Read sentence/tools/customLabel from live sources (the textarea/input's
// own DOM value, and the `currentTools` tracker) rather than `selections`,
// which is frozen for this render and can be missing an edit that was
// just committed a moment ago in this same render.
const currentCustomLabel = customLabelInput ? customLabelInput.value : (pendingSelections[key]?.customLabel ?? "");
const currentPrompts = promptsTextarea ? promptsTextarea.value : (pendingSelections[key]?.prompts ?? "");
const taskComplete = taskIsComplete(sentenceTextarea.value, currentTools, item.openEnded, currentCustomLabel);
if (!taskComplete) return false;
detailsDiv.classList.add("gaidet-item-details-complete");
const next = { ...pendingSelections };
if (next[key]) {
next[key] = {
...next[key],
tools: currentTools,
sentence: sentenceTextarea.value,
customLabel: currentCustomLabel,
prompts: currentPrompts,
proceeded: true
};
pendingSelections = next;
mutable selections = next;
}
return true;
}
function confirmAndAdvance() {
if (!markCompleteIfReady()) return;
// Jump to the next task's description field, if there is one. The
// target field is captured by its data-field identifier before the
// commit above rebuilds the whole checklist, then located again
// (polling via rAF, since the rebuild isn't necessarily synchronous)
// once it exists in the freshly rebuilt DOM.
const allSentences = Array.from(document.querySelectorAll('textarea[data-field^="sentence::"]'));
const idx = allSentences.indexOf(sentenceTextarea);
const nextField = allSentences[idx + 1]?.dataset.field;
if (!nextField) return;
let attempts = 0;
const tryFocusNext = () => {
attempts += 1;
const next = document.querySelector(`[data-field="${CSS.escape(nextField)}"]`);
if (next && next.isConnected) {
next.focus({ preventScroll: true });
next.scrollIntoView({ behavior: "smooth", block: "center" });
return;
}
if (attempts < 30) requestAnimationFrame(tryFocusNext);
};
requestAnimationFrame(tryFocusNext);
}
const promptsTextarea = providePrompts ? el("textarea", {
class: "gaidet-sentence",
"data-field": `prompts::${key}`,
"aria-label": "Prompts used for this task",
placeholder: "If applicable, please copy-paste the prompts you have used.",
value: sel?.prompts ?? "",
onblur: (e) => {
const next = { ...pendingSelections };
if (next[key]) {
next[key] = { ...next[key], prompts: e.target.value };
pendingSelections = next;
mutable selections = next;
}
}
}) : null;
const customLabelInput = item.openEnded ? el("input", {
type: "text",
"data-field": `customLabel::${key}`,
"aria-label": "Task or component description",
style: "width:100%;max-width:500px;",
placeholder: "What was this task or component? (short description)",
value: sel?.customLabel ?? "",
onblur: (e) => {
const next = { ...pendingSelections };
if (next[key]) {
next[key] = { ...next[key], customLabel: e.target.value };
pendingSelections = next;
mutable selections = next;
}
}
}) : null;
const sentenceWordCount = el("span", { class: "gaidet-sentence-wordcount" }, `${countWords(sel?.sentence)} words`);
const detailsDiv = checked ? el("div", { class: "gaidet-item-details" }, [
customLabelInput,
buildToolPicker(),
el("div", { class: "gaidet-sentence-wrap" }, [
sentenceTextarea,
sentenceWordCount,
el("button", {
type: "button",
class: "gaidet-sentence-enter",
"aria-label": "Mark this task's description as done and go to the next task",
onclick: () => confirmAndAdvance()
}, "→")
]),
providePrompts ? promptsTextarea : null
]) : null;
if (detailsDiv && isComplete && sel.proceeded) {
detailsDiv.classList.add("gaidet-item-details-complete");
}
if (detailsDiv) {
autoGrow(sentenceTextarea);
sentenceTextarea.addEventListener("input", (e) => {
autoGrow(e.target);
// Plain textContent update on an already-cheap, non-reactive
// listener: no reflow, no `mutable` commit, so this doesn't
// reintroduce the typing lag that was fixed earlier.
sentenceWordCount.textContent = `${countWords(e.target.value)} words`;
});
// Plain Enter acts like clicking the arrow (confirm + advance);
// Shift+Enter still inserts a newline, as usual for a text box.
sentenceTextarea.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
confirmAndAdvance();
}
});
// "Moved to the next section": once focus leaves this task's fields
// (tabbing/clicking to the next task, or down to the declaration),
// mark it complete if it qualifies.
detailsDiv.addEventListener("focusout", () => markCompleteIfReady());
}
if (detailsDiv && providePrompts) {
autoGrow(promptsTextarea);
promptsTextarea.addEventListener("input", (e) => autoGrow(e.target));
// Prompts is the last field for this task when shown, so Enter here
// confirms and advances the same way it does from the description.
promptsTextarea.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
confirmAndAdvance();
}
});
}
const details = detailsDiv;
return el("div", { class: "gaidet-item" }, [checkboxLabel, details]);
})
]);
return categoryEl;
})));
}Generate declaration
promptsReminder = providePrompts
? el("p", { class: "gaidet-prompts-reminder" }, "Please add your prompts for each task below.")
: nullBefore you begin, please read the privacy policy and disclaimer of responsibility at the end of this page for details.
declarationText = {
const entries = Object.values(selections);
function formatToolWithVersion([name, val]) {
const version = (val && typeof val === "object" && val.version) ? val.version.trim() : "";
return version ? `${name} (${version})` : name;
}
const overallToolNames = [...new Set(entries.flatMap(e =>
Object.entries(e.tools ?? {}).map(formatToolWithVersion)
))].sort((a, b) => a.localeCompare(b));
const overallToolsPart = overallToolNames.length > 0 ? overallToolNames.join(", ") : "[no tools ticked]";
const aiasLine = aiasLevelLabel ? `**AIAS level:** ${aiasLevelLabel}\n` : "";
const heading = `**Declaration on the Use of Artificial Intelligence Tools**\n\n**AI tools used:** ${overallToolsPart}\n${aiasLine}`;
const closing = `\n\nThe author confirms that this AI use falls within what was permitted for this assignment, and complies with any other applicable rules, such as their institution's Declaration of Authorship.\n\nResponsibility for the submitted assignment lies entirely with the author of this declaration.\n\nGenerative AI tools cannot be listed as authors and do not bear responsibility for the final outcomes.`;
if (entries.length === 0) {
return `${heading}\nNo tasks have been ticked yet.${closing}`;
}
const lines = entries.map(e => {
const labelPart = (e.customLabel && e.customLabel.trim()) ? e.customLabel.trim() : e.label;
const itemToolNames = Object.entries(e.tools ?? {}).map(formatToolWithVersion);
const itemToolsPart = itemToolNames.length > 0 ? itemToolNames.join(", ") : "[no tool specified]";
const toolLabel = itemToolNames.length === 1 ? "Tool" : "Tools";
const sentencePart = e.sentence.trim() || "[please add an explainer]";
const promptsPart = providePrompts && e.prompts && e.prompts.trim() ? `\n Prompt: ${e.prompts.trim()}` : "";
return `- **${labelPart}**\n ${toolLabel}: ${itemToolsPart}\n Summary: ${sentencePart}${promptsPart}`;
});
return `${heading}\nThe following generative AI tasks were delegated as part of this assignment:\n\n${lines.join("\n\n")}${closing}`;
}declarationHtml = {
// A rich-text twin of declarationText, for the "Copy to clipboard" button:
// declarationText's markdown (asterisks, "- " lines) renders correctly in
// markdown-aware editors, but pasted as-is into Word/Docs/Outlook it would
// show the literal asterisks; this HTML version gives those rich-text
// targets a real bold heading and bulleted list instead.
function escapeHtml(s) {
return (s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
const entries = Object.values(selections);
function formatToolWithVersion([name, val]) {
const version = (val && typeof val === "object" && val.version) ? val.version.trim() : "";
return escapeHtml(version ? `${name} (${version})` : name);
}
const overallToolNames = [...new Set(entries.flatMap(e =>
Object.entries(e.tools ?? {}).map(formatToolWithVersion)
))].sort((a, b) => a.localeCompare(b));
const overallToolsPart = overallToolNames.length > 0 ? overallToolNames.join(", ") : "[no tools ticked]";
const aiasLineHtml = aiasLevelLabel ? `<p><strong>AIAS level:</strong> ${escapeHtml(aiasLevelLabel)}</p>` : "";
const headingHtml = `<p><strong>Declaration on the Use of Artificial Intelligence Tools</strong></p><p><strong>AI tools used:</strong> ${overallToolsPart}</p>${aiasLineHtml}`;
const closingHtml = `<p>The author confirms that this AI use falls within what was permitted for this assignment, and complies with any other applicable rules, such as their institution's Declaration of Authorship.</p><p>Responsibility for the submitted assignment lies entirely with the author of this declaration.</p><p>Generative AI tools cannot be listed as authors and do not bear responsibility for the final outcomes.</p>`;
if (entries.length === 0) {
return `${headingHtml}<p>No tasks have been ticked yet.</p>${closingHtml}`;
}
const items = entries.map(e => {
const labelPart = escapeHtml((e.customLabel && e.customLabel.trim()) ? e.customLabel.trim() : e.label);
const itemToolNames = Object.entries(e.tools ?? {}).map(formatToolWithVersion);
const itemToolsPart = itemToolNames.length > 0 ? itemToolNames.join(", ") : "[no tool specified]";
const toolLabel = itemToolNames.length === 1 ? "Tool" : "Tools";
const sentencePart = escapeHtml(e.sentence.trim() || "[please add an explainer]");
const promptsPart = providePrompts && e.prompts && e.prompts.trim() ? `<br>Prompt: ${escapeHtml(e.prompts.trim())}` : "";
return `<li><strong>${labelPart}</strong><br>${toolLabel}: ${itemToolsPart}<br>Summary: ${sentencePart}${promptsPart}</li>`;
});
return `${headingHtml}<p>The following generative AI tasks were delegated as part of this assignment:</p><ul>${items.join("")}</ul>${closingHtml}`;
}outputBox = {
const entries = Object.values(selections);
const noTasks = entries.length === 0;
const incompleteEntries = entries.filter(sel =>
!(sel.sentence && sel.sentence.trim())
|| Object.keys(sel.tools ?? {}).length === 0
|| (sel.openEnded && !(sel.customLabel && sel.customLabel.trim()))
);
const incomplete = noTasks || incompleteEntries.length > 0;
const warningContent = noTasks
? "Please tick at least one task below before creating the declaration."
: incompleteEntries.length > 0
? [
"Please add a description and at least one tool before creating the declaration, for:",
el("ul", { class: "gaidet-warning-list" },
incompleteEntries.map(sel => el("li", {},
(sel.customLabel && sel.customLabel.trim()) ? sel.customLabel.trim() : sel.label
))
)
]
: null;
const warning = (incomplete && (copyWarning || declarationConfirmed) && warningContent)
? el("div", { class: "gaidet-warning" }, warningContent)
: null;
const showDeclaration = declarationConfirmed && !incomplete;
const successText = copySuccess === "download"
? "Declaration downloaded as a text file. You can open it, paste it into your assignment, and make manual edits if needed."
: "Declaration copied to clipboard. You can paste it into your assignment and make manual edits if needed.";
const success = (showDeclaration && copySuccess && declarationText === copiedText)
? el("div", { class: "gaidet-success" }, successText)
: null;
const placeholderText = ["Your declaration will appear here once every ticked task has a tool and a short description.", "Your declaration should accurately reflect all uses of generative AI in preparing the assignment.", "If you didn't use AI at all, add: ‘No generative AI tools were used in the preparation of this assignment.’"];
const confirmBox = showConfirmPrompt ? el("div", { class: "gaidet-confirm" }, [
el("p", {}, "Did you disclose all use of generative AI in this assignment?"),
el("div", { class: "gaidet-confirm-actions" }, [
el("button", {
class: "gaidet-btn-primary",
onclick: () => {
trackGenerateClick();
mutable declarationConfirmed = true;
mutable showConfirmPrompt = false;
mutable copySuccess = false;
}
}, "Yes, generate declaration"),
el("button", {
class: "gaidet-btn-secondary",
onclick: () => { mutable showConfirmPrompt = false; }
}, "No, let me edit")
])
]) : null;
function onCreateClick() {
if (incomplete) {
mutable copyWarning = true;
return;
}
mutable copyWarning = false;
// Every entry is already complete at this point (the `incomplete` check
// above guarantees it), so generating the declaration also marks any
// not-yet-proceeded task as done, turning its box green.
const next = { ...selections };
let anyNewlyProceeded = false;
for (const k of Object.keys(next)) {
if (!next[k].proceeded) {
next[k] = { ...next[k], proceeded: true };
anyNewlyProceeded = true;
}
}
if (anyNewlyProceeded) mutable selections = next;
if (declarationConfirmed) {
mutable copySuccess = false;
mutable justUpdated = true;
setTimeout(() => { mutable justUpdated = false; }, 1800);
} else {
mutable showConfirmPrompt = true;
}
}
const outputDiv = el("div", { class: "gaidet-output", id: "gaidet-output-section" }, [
el("h2", { class: "gaidet-output-title gaidet-step-label" }, "Step 3: Your declaration"),
el("div", { class: "gaidet-output-header-actions" }, [
showConfirmPrompt ? null : el("button", {
class: "gaidet-btn-primary",
onclick: onCreateClick
}, declarationConfirmed ? "Update declaration" : "Create declaration"),
el("div", { class: "gaidet-output-actions-secondary" }, [
showDeclaration ? el("button", {
class: "gaidet-btn-secondary gaidet-btn-small",
onclick: () => {
const item = new ClipboardItem({
"text/plain": new Blob([declarationText], { type: "text/plain" }),
"text/html": new Blob([declarationHtml], { type: "text/html" })
});
navigator.clipboard.write([item]).catch(() => {
// Older/other browsers without rich-clipboard support.
navigator.clipboard.writeText(declarationText);
});
mutable copySuccess = "copy";
mutable copiedText = declarationText;
}
}, "Copy to clipboard") : null,
showDeclaration ? el("button", {
class: "gaidet-btn-secondary gaidet-btn-small",
onclick: () => {
const blob = new Blob([declarationText], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "ai-disclosure-declaration.txt";
a.click();
URL.revokeObjectURL(url);
mutable copySuccess = "download";
mutable copiedText = declarationText;
}
}, "Download as txt file") : null
])
]),
warning,
confirmBox,
success,
showConfirmPrompt
? null
: showDeclaration
? (() => {
const div = el("div", { id: "gaidet-output-text", class: justUpdated ? "gaidet-output-flash" : "" }, []);
div.innerHTML = declarationHtml;
return div;
})()
: el("div", { class: "gaidet-output-placeholder" },
Array.isArray(placeholderText)
? placeholderText.map(t => el("p", {}, t))
: [el("p", {}, placeholderText)]
),
(showDeclaration && !showConfirmPrompt) ? el("p", { class: "gaidet-output-note" }, [
"I recommend reviewing and adjusting the declaration to fit your own voice and your module's requirements before submitting."
]) : null
]);
// Step 3 stays visibly "off" until Step 2 has at least one ticked task,
// matching the locked look used for unpermitted categories.
return noTasks ? lockBox(outputDiv, "Step 1 and Step 2 need to be completed first before you can generate a declaration.") : outputDiv;
}
Note
Where do these categories come from? The list below is drawn from the task categories from student surveys and existing AI disclosure guidelines. It is a checklist of common use cases, not a recommendation to use AI for all (or indeed any) of them. Whether AI is appropriate for a given category still depends entirely on your assignment brief and your lecturer’s instructions.
Ticking Step 1 below is the crucial filtering step: it should reflect only what your assignment or module actually permits, not everything AI happens to be capable of. If you are unsure whether a category applies, leave it unticked and ask your lecturer rather than guessing.
Save or resume your progress
Nothing is saved automatically. If you want to continue later, click “Save progress” to save a JSON file to your computer, then use “Load progress” to pick it up again on your next visit. This file is saved directly to your device only; nothing is sent to a server.
progressBox = {
const fileInput = el("input", {
type: "file",
accept: "application/json",
style: "display:none;",
onchange: (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
mutable selections = data.selections ?? {};
mutable providePrompts = data.providePrompts ?? false;
mutable aiasLevel = data.aiasLevel ?? "";
} catch (err) {
alert("Could not read this file. Please make sure it's a progress file saved from this tool.");
}
e.target.value = "";
};
reader.readAsText(file);
}
});
return el("div", { class: "gaidet-item" }, [
el("div", { class: "gaidet-output-actions" }, [
el("button", {
type: "button",
class: "gaidet-btn-secondary",
onclick: () => {
const payload = { version: 1, providePrompts, aiasLevel, selections };
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "ai-disclosure-progress.json";
a.click();
URL.revokeObjectURL(url);
}
}, "Save progress"),
el("button", {
type: "button",
class: "gaidet-btn-secondary",
onclick: () => fileInput.click()
}, "Load progress"),
fileInput
])
]);
}
About the maintainer
This tool was built by Stefan Müller with assistance from Claude Code, under the maintainer’s direction.
I’d love to hear how you’re using this tool and any suggestions for improving it. Please share your thoughts via this short feedback form.
How to cite this tool
If you use this tool in your teaching, you can cite it as:
Müller, Stefan. AI Disclosure Generator. https://disclose-ai.app
If you are using this tool in your teaching, it would be great if you could let me know
Related literature and tools
A number of similar tools exist to support transparent AI-use disclosure, including the AID Framework, the GAIDeT Declaration Generator, and sIfA (Statement of Intellectual Fellowship and Accountability), a browser-based tool from Busara that maps AI contributions onto the 14 CRediT contributor roles and is aimed more at researchers documenting a publication than at student coursework. All aim at the same goal: helping students and researchers disclose their AI use clearly and consistently. Use whichever fits your context best.
The optional “AIAS level” field above uses the AI Assessment Scale, a complementary framework instructors use to specify how much AI use is permitted on a given assessment; this tool only lets you record that level, it doesn’t set or enforce it.
This tool’s approach is comparable to the American Journal of Political Science’s AI Policy, which asks authors to describe how AI was used rather than reproduce full transcripts. If you’re an instructor who wants students to submit the entire output of their AI conversations, not just a summary and the prompts used, ask for that separately.
Further reading on AI-use disclosure:
- Suchikova, Yana, Tsybuliak, Natalia, Teixeira da Silva, Jaime A., & Nazarovets, Serhii (2026). “GAIDeT (Generative AI Delegation Taxonomy): A Taxonomy for Humans to Delegate Tasks to Generative Artificial Intelligence in Scientific Research and Publishing“. Accountability in Research, 33(3). 10.1080/08989621.2025.2544331
- Weaver, Kari D. (2024). “The Artificial Intelligence Disclosure (AID) Framework: An Introduction“. College & Research Libraries News, 85(10), 407–411. arxiv.org/abs/2408.01904
- Perkins, Mike, Furze, Leon, Roe, Jasper, & MacVaugh, Jason (2024). “The AI Assessment Scale (AIAS)“. aiassessmentscale.com
- American Journal of Political Science (2026). “AJPS AI Policy“. Updated 11 August 2026. ajps.org/ajps-ai-policy
- Saleh, Engy, & Schomerus, Mareike (2026). “The sIfA Tool for a Statement of Intellectual Fellowship and Accountability: An Invitation to Reflect on How Humans and AI Interact in Producing Knowledge“. Busara. 10.62372/IUPB2137
- Schomerus, Mareike. sIfA Tool (Tool to create a Statement of Intellectual Fellowship and Accountability). Version 1.4. 2026. Busara. 10.5281/zenodo.20285993
Disclaimer of responsibility
This tool is provided purely as a free, unofficial aid to help you structure your own AI-use disclosure. It is not legal, academic, or official institutional guidance, and using it does not constitute compliance with any university, School, programme, or module policy.
The author, Stefan Müller, and any contributors accept no responsibility or liability whatsoever for how this tool is used, for whether the tasks and tools listed by the user constitute a complete account of all generative AI use in connection with the relevant work, for whether such use is declared honestly and in good faith, or for any academic, disciplinary, legal, or other consequences arising from its use or misuse.
You are solely responsible for checking your own university’s and module’s specific generative AI policies and citation requirements, and for confirming directly with your lecturer or module coordinator whether, and how, AI tools may be used before you submit any work. The author cannot be held legally accountable for any reliance placed on this tool.
This tool is an independent personal project of the author and is not an official service of, or endorsed by, University College Dublin or any other institution.
The AI tools listed or suggested in this tool are trademarks of their respective owners. Their appearance here, including in the autocomplete suggestions, is for identification purposes only and does not imply any affiliation with, or endorsement of, this tool by those companies, nor endorsement of those tools by the author.
Privacy policy
This tool is fully open source, so you can verify these claims yourself.
Your declaration and task details. Everything you enter here (tasks, tools, descriptions, prompts) stays in your own browser. It is never stored, transmitted, or saved anywhere, and remains only in your browser and is cleared when you refresh or close the page. The generated declaration is provided only so you can copy or download it yourself.
Analytics. This site uses Cabin, a privacy-focused analytics service, to understand overall visitor numbers. Cabin does not use cookies, does not track IP addresses, and cannot identify individual visitors. It counts page views, plus one aggregate, anonymous tally per completed declaration (clicking “Yes, generate declaration”; not on later edits). See Cabin’s privacy policy for details. No data about the content you enter into this tool is ever sent to Cabin or anyone else.
Questions or feedback. If you have questions about this policy, contact Stefan Müller. If you spot an error or have an improvement suggestion, feel free to open an issue or pull request on GitHub.