📘 Lesson 5 — Reset Chat + Show Sources (Docs & Chunks) in the UI
By now you have:
– A RAG index in Sheets (Lesson 4B)
– Chat history + RAG working together (Lesson 4C)
– A web app UI (from Lesson 2) calling google.script.run
But two things are missing:
– Users can’t reset the conversation easily
– You can’t see which documents Gemini actually used
Lesson 5 fixes both:
– Add a “Reset chat” function on the server + button in the UI
– Extend the API to return debug info (sources) for each answer
– Show that info in the UI under the assistant messages
🧠 What We’ll Add
1. On the server (Apps Script)
We’ll:
Upgrade answerWithChatRag() so it returns:
{
answer: “…”,
sources: [
{ fileName, chunkIndex, score, preview }
]
}
– Add chatWithChatRagDebug(question) that returns this object to the UI
– Add resetChatHistoryPublic() to clear chat history from the UI
2. On the client (Index.html)
We’ll:
– Add a “Reset conversation” button at the top
– Update onSend() to use chatWithChatRagDebug
– When the assistant responds, show a small “Sources” block under the message with file names + scores
🧩 Part 1 — Update the Server: Chat + Sources + Reset
In your Lesson4C_ChatRag.gs file (or similar), we’ll:
– Replace answerWithChatRag() with a version that returns {answer, sources}
– Adjust the chatWithChatRag() wrapper (or add a new one)
– Add a public reset function
1.1 Updated answerWithChatRag (return answer + sources)
/**
* Main function for Lesson 5:
* – Same idea as Lesson 4C, but now returns:
* {
* answer: string,
* sources: [
* { fileName, chunkIndex, score, preview }
* ]
* }
*/
function answerWithChatRag(question) {
const q = (question || ”).trim();
if (!q) throw new Error(‘Question cannot be empty.’);
// 1) Load RAG index (from Sheet)
const index = loadRagIndex_();
if (index.length === 0) {
throw new Error(‘RAG index is empty. Run buildRagIndex() first.’);
}
// 2) Load chat history
const history = getChatHistory_(); // [{role, text}, …]
// 3) Embed the question and compute similarity
const queryEmbedding = embedText_(q);
const ranked = index
.map(entry => ({
entry,
score: cosineSimilarity_(queryEmbedding, entry.embedding)
}))
.sort((a, b) => b.score – a.score);
const top = ranked.slice(0, TOP_K);
// 4) Build document context + source metadata
const sources = top.map(r => {
const preview = (r.entry.text || ”).slice(0, 180).replace(/\s+/g, ‘ ‘) +
(r.entry.text.length > 180 ? ‘…’ : ”);
return {
fileName: r.entry.fileName,
chunkIndex: r.entry.chunkIndex,
score: r.score,
preview
};
});
const docContext = top.map((r, i) =>
`[#${i + 1} — ${r.entry.fileName} — chunk=${r.entry.chunkIndex} — score=${r.score.toFixed(3)}]\n` +
r.entry.text
).join(‘\n\n’);
// 5) Format chat history
const conversationText = history.map(turn =>
`${turn.role.toUpperCase()}: ${turn.text}`
).join(‘\n’);
// 6) Build prompt
const prompt =
‘You are a helpful assistant that uses BOTH:\n’ +
‘- The previous conversation with the user (chat history)\n’ +
‘- Retrieved document chunks from a RAG index in Sheets\n\n’ +
‘Rules:\n’ +
‘- Use the chat history for follow-up understanding.\n’ +
‘- Use the retrieved chunks for factual or policy information.\n’ +
‘- If the documents do not contain the answer, say you do not know.\n’ +
‘- Do NOT invent details that are not present in the documents.\n\n’ +
‘— CHAT HISTORY —\n’ +
(conversationText || ‘[No previous messages; this is the first turn.]’) +
‘\n\n’ +
‘— RETRIEVED DOCUMENT CHUNKS —\n’ +
docContext +
‘\n— END DOCUMENT CHUNKS —\n\n’ +
‘Now the user says:\n’ +
‘USER: ‘ + q + ‘\n\n’ +
‘Please answer as the ASSISTANT in a clear, concise way.’;
// 7) Call Gemini
const answer = callGeminiText_(prompt);
// 8) Update history (user + assistant)
const newHistory = addTurnToHistory_(history, { role: ‘user’, text: q });
const newHistoryWithAssistant = addTurnToHistory_(newHistory, {
role: ‘assistant’,
text: answer
});
saveChatHistory_(newHistoryWithAssistant);
// 9) Return both answer + sources
return {
answer: answer || ”,
sources
};
}
1.2 New wrapper for UI: chatWithChatRagDebug
If you already have chatWithChatRag, you can either replace it or add a new one. For clarity, let’s use a debug-friendly name:
/**
* Web-app wrapper for Lesson 5.
* The UI will call this via google.script.run.
*/
function chatWithChatRagDebug(question) {
return answerWithChatRag(question); // already returns {answer, sources}
}
1.3 Public function to reset chat history
We already have getChatHistory_, saveChatHistory_, etc.
Now we expose a public reset:
/**
* Clears chat history for the current user.
* Can be called from the web UI (Reset Conversation button).
*/
function resetChatHistoryPublic() {
const cache = CacheService.getUserCache();
cache.remove(CHAT_HISTORY_KEY);
return { ok: true };
}
That’s all you need server-side for Lesson 5.
🧩 Part 2 — Update the Web App UI
Now we’ll adjust your Index.html to:
– Add a Reset button
– Use chatWithChatRagDebug instead of the old function
– Render Sources under each assistant answer
I’ll show a minimal version assuming you’re using the chat UI style we built earlier (Lesson 2 / API + Drive assistant).
2.1 Add a “Reset” button in the header
Inside <body>, your main container probably looks like this:
<div class=”app”>
<header>
<h1>API + Drive + Gemini Assistant</h1>
<div class=”subtitle”>
Ask a question and the assistant will use your documents.
</div>
</header>
…
</div>
Update the header to include a small button on the right:
<header style=”display:flex; justify-content:space-between; align-items:center; gap:8px;”>
<div>
<h1>Docs + RAG Chat Assistant</h1>
<div class=”subtitle”>
Ask questions grounded in your Drive documents with short-term chat memory.
</div>
</div>
<button id=”resetBtn” onclick=”onResetChat()” style=”font-size:0.8rem;”>
Reset conversation
</button>
</header>
2.2 Update the message rendering to support “Sources”
In your <script> block, you probably had something like:
function addMessage(text, sender) {
const wrapper = document.createElement(‘div’);
wrapper.className = ‘message ‘ + sender;
…
}
Let’s extend it to optionally attach a “Sources” panel.
Replace your addMessage function with:
<script>
const messagesEl = document.getElementById(‘messages’);
const userInputEl = document.getElementById(‘userInput’);
const sendBtn = document.getElementById(‘sendBtn’);
const statusEl = document.getElementById(‘status’);
let typingMessageEl = null;
function addMessage(text, sender, sources) {
const wrapper = document.createElement(‘div’);
wrapper.className = ‘message ‘ + sender;
// Label (You / Assistant)
if (sender === ‘user’ || sender === ‘bot’) {
const label = document.createElement(‘span’);
label.className = ‘bubble-label’;
label.textContent = sender === ‘user’ ? ‘You’ : ‘Assistant’;
wrapper.appendChild(label);
}
// Main text
const content = document.createElement(‘div’);
content.textContent = text;
wrapper.appendChild(content);
// Optional sources block (only for bot messages with debug data)
if (sender === ‘bot’ && Array.isArray(sources) && sources.length > 0) {
const src = document.createElement(‘div’);
src.style.marginTop = ‘6px’;
src.style.paddingTop = ‘4px’;
src.style.borderTop = ‘1px solid #1f2937’;
src.style.fontSize = ‘0.75rem’;
src.style.color = ‘#9ca3af’;
const title = document.createElement(‘div’);
title.textContent = ‘Sources used:’;
title.style.marginBottom = ‘2px’;
src.appendChild(title);
const list = document.createElement(‘ul’);
list.style.margin = 0;
list.style.paddingLeft = ’16px’;
sources.forEach((s) => {
const li = document.createElement(‘li’);
li.textContent =
`${s.fileName} (chunk ${s.chunkIndex}, score ${s.score.toFixed(3)})` +
(s.preview ? ` — “${s.preview}”` : ”);
list.appendChild(li);
});
src.appendChild(list);
wrapper.appendChild(src);
}
messagesEl.appendChild(wrapper);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
// … keep your typing, status, setLoading, etc.
</script>
2.3 Update onSend() to use the new server function
Change your previous onSend() (which probably called chatWithFolderAndApi, chatWithRagFromSheet, or chatWithChatRag) to now call chatWithChatRagDebug.
For example:
function onSend() {
const text = userInputEl.value.trim();
if (!text) {
showStatus(‘Please enter a question.’, true);
return;
}
showStatus(”, false);
addMessage(text, ‘user’);
userInputEl.value = ”;
autoGrowTextarea();
setLoading(true);
showTyping();
google.script.run
.withSuccessHandler(function (res) {
setLoading(false);
hideTyping();
if (res && res.answer) {
// res.sources may be undefined or empty
addMessage(res.answer, ‘bot’, res.sources || []);
} else {
addMessage(
‘I could not generate an answer.’,
‘bot’
);
}
})
.withFailureHandler(function (err) {
setLoading(false);
hideTyping();
showStatus(err.message || String(err), true);
addMessage(‘Sorry, something went wrong on the server.’, ‘bot’);
})
.chatWithChatRagDebug(text);
}
2.4 Implement the Reset button behavior
Add a onResetChat() function in the same script:
function onResetChat() {
setLoading(true);
showStatus(‘Resetting conversation…’, false);
google.script.run
.withSuccessHandler(function (res) {
setLoading(false);
showStatus(‘Conversation reset.’, false);
// Clear messages in the UI
messagesEl.innerHTML = ”;
// Optionally add a system message
addSystemMessage(
‘Conversation reset. Ask a new question to start a fresh chat.’
);
})
.withFailureHandler(function (err) {
setLoading(false);
showStatus(err.message || String(err), true);
})
.resetChatHistoryPublic();
}
Make sure you still have this helper (we used it earlier):
function addSystemMessage(text) {
const wrapper = document.createElement(‘div’);
wrapper.className = ‘message system’;
wrapper.textContent = text;
messagesEl.appendChild(wrapper);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
🧪 Things to Try in Lesson 5
You can give learners a few practice tasks:
– Check the sources
– Ask a question about one specific document.
– Confirm that the “Sources used” list only shows that file.
– See how scores change
– Ask a broad question → multiple files, similar scores
– Ask a very specific question → one chunk with a much higher score
– Test reset
– Have a multi-step conversation
– Click Reset conversation
– Ask: “What did I just ask you before this?”
– The assistant should say it doesn’t know (history cleared).
– Optional: add a “Show/Hide Sources” toggle per message
– Wrap the sources block in a <details> element
– Let users expand/collapse to keep the UI clean.