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.
Purpose and limits
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’s adapted from the GAIDeT Declaration Generator (Suchikova et al. 2026).
exampleDeclarationHtml = `<p><strong>Declaration on the Use of Artificial Intelligence Tools</strong></p>
<p><strong>AI tools used:</strong> ChatGPT (GPT-5.5), Claude Code (Sonnet 4.6), Grammarly</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>Idea generation</strong><br>Tool: ChatGPT (GPT-5.5)<br>Summary: I had a general research area in mind; I asked ChatGPT to suggest possible angles, then chose and developed the final research question myself.</li>
<li><strong>Code generation</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: Grammarly<br>Summary: After completing my own final draft, I ran it through Grammarly for grammar and punctuation checks, and manually reviewed and accepted only the suggestions I agreed with.</li>
</ul>
<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;
}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; 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") {
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;
}
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", "");
const lockMessage = el("div", { class: "gaidet-lock-message" }, 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(){
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'll 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 = el("div", { class: "gaidet-category-gate" }, [
el("p", { class: "gaidet-tool-label gaidet-step-label" }, "Step 1: Which categories of AI use does your assignment or module 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's actually permitted."),
el("div", { class: "gaidet-category-gate-list" }, data.categories.map(cat => {
const isAllowed = allowedCategories.has(cat.id);
return el("label", {
class: isAllowed ? "gaidet-category-gate-option gaidet-category-gate-option-checked" : "gaidet-category-gate-option"
}, [
el("input", {
type: "checkbox",
class: "gaidet-checkbox-input",
checked: isAllowed,
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)
]);
}))
])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;
return preserveFocus(() => el("div", {}, data.categories.map(cat => {
const locked = !allowedCategories.has(cat.id);
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 locked ? lockBox(categoryEl) : categoryEl;
})));
}Generate declaration
promptsReminder = providePrompts
? el("p", { class: "gaidet-prompts-reminder" }, "Please add your prompts for each task below.")
: nullYou can create a declaration in three steps:
- Step 1: tell it which categories of AI use your assignment or module permits.
- Step 2: go through the unlocked categories below, tick every task where you delegated work to AI, then add the tool(s) you used and a short description for each. Give the exact model or version where you can (e.g. GPT-5.5).
- Step 3: once every ticked task is filled in, generate your declaration, ready to copy or download.
Before 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\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 Prompts: ${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>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>Prompts: ${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: () => {
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" }, [
"This declaration is a starting point. I recommend reviewing and adjusting the generated wording to fit your own voice, your module's requirements, and your institution's referencing style before submitting. The source code for this website is available on ",
el("a", { href: "https://github.com/stefan-mueller/disclose-ai" }, "GitHub"),
"."
]) : 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;
}
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 substantial 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 (2026). AI Disclosure Generator. https://disclose-ai.app
Related literature and tools
A number of similar tools exist to support transparent AI-use disclosure, including the AID Framework and the GAIDeT Declaration Generator. 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.
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
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 form (e.g. ChatGPT, Claude, Grammarly) 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 only reports aggregate, anonymous traffic data (e.g. how many people visited, which pages). 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.