Skip to content

Commit

Permalink
Add citations
Browse files Browse the repository at this point in the history
  • Loading branch information
albandum committed Jan 6, 2025
1 parent a565b62 commit d17e4c0
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 44 deletions.
16 changes: 16 additions & 0 deletions zendesk/zd-app/assets/iframe.html
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,22 @@
.select2-results__option {
padding: 6px 12px;
}

.citation-badge {
display: inline-block;
padding: 0 4px;
margin: 0 2px;
border-radius: 3px;
background-color: #e9ecef;
color: #495057;
text-decoration: none;
font-size: 0.875em;
}

.citation-badge:hover {
background-color: #dee2e6;
text-decoration: none;
}
</style>
</head>
<body>
Expand Down
119 changes: 76 additions & 43 deletions zendesk/zd-app/assets/main.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,68 @@
function convertMarkdownToHtml(markdown) {
return markdown
.replace(/^### (.*$)/gim, "<h3>$1</h3>")
.replace(/^## (.*$)/gim, "<h2>$1</h2>")
.replace(/^# (.*$)/gim, "<h1>$1</h1>")
.replace(/\*\*(.*)\*\*/gim, "<strong>$1</strong>")
.replace(/\*(.*)\*/gim, "<em>$1</em>")
.replace(/!\[(.*?)\]\((.*?)\)/gim, "<img alt='$1' src='$2' />")
.replace(/\[(.*?)\]\((.*?)\)/gim, "<a href='$2' target='_blank'>$1</a>")
.replace(/`(.*?)`/gim, "<code>$1</code>")
.replace(
/```([\s\S]*?)```/g,
(match, p1) => "<pre><code>" + p1.trim() + "</code></pre>"
)
.replace(/(?:\r\n|\r|\n)/g, "<br>")
.replace(/:cite\[[^\]]+\]/g, "");
function convertMarkdownToHtml(markdown, conversationData) {
// Check if markdown is null or not a string
if (typeof markdown !== "string") {
console.error("Invalid markdown input:", markdown);
return ""; // Return an empty string or handle as needed
}

// Store conversation data
currentConversation = conversationData;

// Keep track of citation count
let citationCount = 0;
const citations = {};

// First pass: collect all citations and assign numbers
markdown.replace(/:cite\[([^\]]+)\]/g, (match, reference) => {
if (!citations[reference]) {
citationCount++;
citations[reference] = citationCount;
}
});

return (
markdown
.replace(/^### (.*$)/gim, "<h3>$1</h3>")
.replace(/^## (.*$)/gim, "<h2>$1</h2>")
.replace(/^# (.*$)/gim, "<h1>$1</h1>")
.replace(/\*\*(.*)\*\*/gim, "<strong>$1</strong>")
.replace(/\*(.*)\*/gim, "<em>$1</em>")
.replace(/!\[(.*?)\]\((.*?)\)/gim, "<img alt='$1' src='$2' />")
.replace(/\[(.*?)\]\((.*?)\)/gim, "<a href='$2' target='_blank'>$1</a>")
.replace(/`(.*?)`/gim, "<code>$1</code>")
.replace(
/```([\s\S]*?)```/g,
(match, p1) => "<pre><code>" + p1.trim() + "</code></pre>"
)
.replace(/(?:\r\n|\r|\n)/g, "<br>")
// Replace citations with numbered badges that are links
.replace(/:cite\[([^\]]+)\]/g, (match, reference) => {
const sourceUrl = getSourceUrlFromReference(reference);
return `<a href="${sourceUrl}" target="_blank" class="citation-badge">[${citations[reference]}]</a>`;
})
);
}

// Store conversation data at module level
let currentConversation = null;

// Helper function to get source URL from the conversation data
function getSourceUrlFromReference(reference) {
if (!currentConversation) {
console.warn("No conversation data available");
return "#";
}

try {
const document =
currentConversation.content[1][0].actions[0].documents.find(
(doc) => doc.reference === reference
);
return document ? document.sourceUrl : "#";
} catch (error) {
console.error("Error finding source URL for reference:", error);
return "#";
}
}

(async function () {
Expand Down Expand Up @@ -171,7 +220,6 @@ function convertMarkdownToHtml(markdown) {
// Clear existing options
selectElement.innerHTML = "";

// Replace the existing assistants forEach loop and Select2 initialization with this:
assistants.forEach((assistant) => {
if (assistant && assistant.sId && assistant.name) {
const option = new Option(`@${assistant.name}`, assistant.sId);
Expand All @@ -187,8 +235,8 @@ function convertMarkdownToHtml(markdown) {
.select2({
placeholder: "Select an assistant",
allowClear: true,
templateResult: formatAssistant, // Add custom formatting for dropdown items
templateSelection: formatAssistantSelection, // Add custom formatting for selected item
templateResult: formatAssistant,
templateSelection: formatAssistantSelection,
})
.on("change", (e) => {
localStorage.setItem("selectedAssistant", e.target.value);
Expand All @@ -198,7 +246,6 @@ function convertMarkdownToHtml(markdown) {
);
});

// Add these new functions for formatting
function formatAssistant(assistant) {
if (!assistant.id) {
return assistant.text;
Expand Down Expand Up @@ -236,19 +283,15 @@ function convertMarkdownToHtml(markdown) {
if (!assistant.id) {
return assistant.text;
}
// Only show the name (without description) when selected
return assistant.text;
}

// Always show the select wrapper
selectWrapper.style.display = "block";

// If there's only one assistant, select it by default
if (assistants.length === 1) {
$(selectElement).val(assistants[0].sId).trigger("change");
}

// Always show the input wrapper
inputWrapper.style.display = "block";
} else {
throw new Error("Unexpected API response format");
Expand Down Expand Up @@ -303,7 +346,6 @@ function convertMarkdownToHtml(markdown) {
const selectElement = document.getElementById("assistantSelect");

if (selectElement.style.display === "none") {
// If select is hidden, use the stored single assistant ID and name
selectedAssistantId = localStorage.getItem("selectedAssistant");
selectedAssistantName = localStorage.getItem("selectedAssistantName");
} else {
Expand All @@ -312,7 +354,6 @@ function convertMarkdownToHtml(markdown) {
selectElement.options[selectElement.selectedIndex].text;
}

// Check if we have a valid assistant selected
if (!selectedAssistantId || !selectedAssistantName) {
throw new Error(
"No assistant selected. Please select an assistant before sending a message."
Expand Down Expand Up @@ -366,44 +407,37 @@ function convertMarkdownToHtml(markdown) {
ticketInfo.customerEmail = data.ticket.requester.email || "Unknown";
}

// Append the user message to the div
dustResponse.innerHTML += `
<div class="user-message" id="user-${uniqueId}">
<strong>${userFullName}:</strong>
<pre>${userInputValue}</pre>
</div>
`;

// Add spinner below user message with the selected assistant's name
dustResponse.innerHTML += `
<div id="${"assistant-" + uniqueId}" class="assistant-message">
<h4>${selectedAssistantName}:</h4>
<div class="spinner"></div>
</div>
`;

// Scroll to the bottom of the dustResponse div
dustResponse.scrollTop = dustResponse.scrollHeight;

// Clear the user input
userInput.value = "";

// Fetch ticket comments
const commentsResponse = await client.request(
`/api/v2/tickets/${ticket.id}/comments.json`
);
const comments = commentsResponse.comments;

// Collect unique user IDs from comments and filter out invalid IDs
const userIds = [
...new Set(
comments
.map((comment) => comment.author_id)
.filter((id) => id && id > 0) // Only keep positive numeric IDs
.filter((id) => id && id > 0)
),
];

// Fetch user details with error handling
const userResponses = await Promise.all(
userIds.map((id) =>
client.request(`/api/v2/users/${id}.json`).catch((error) => ({
Expand All @@ -428,7 +462,6 @@ function convertMarkdownToHtml(markdown) {
role = author.role === "end-user" ? "Customer" : "Agent";
}

// Redact customer name in comments if hideCustomerInformation is true
const displayName =
hideCustomerInformation && role === "Customer"
? "[Customer]"
Expand Down Expand Up @@ -501,12 +534,14 @@ function convertMarkdownToHtml(markdown) {
const answerAgent = answer.configuration.name;
const answerMessage = answer.content;

// Remove the spinner
const assistantMessageElement = document.getElementById(
`assistant-${uniqueId}`
);
if (assistantMessageElement) {
const htmlAnswer = convertMarkdownToHtml(answerMessage);
const htmlAnswer = convertMarkdownToHtml(
answerMessage,
response.conversation
);
assistantMessageElement.innerHTML = `
<h4>@${answerAgent}:</h4>
<pre class="markdown-content">${htmlAnswer}</pre>
Expand All @@ -515,30 +550,28 @@ function convertMarkdownToHtml(markdown) {
`;
}

// Scroll to the bottom of the dustResponse div
dustResponse.scrollTop = dustResponse.scrollHeight;

await client.invoke("resize", { width: "100%", height: "600px" });
} catch (error) {
console.error("Error sending ticket to Dust:", error);
console.error("Error receiving response from Dust:", error);

// Remove the spinner and show error message
const assistantMessageElement = document.getElementById(
`assistant-${uniqueId}`
);
if (assistantMessageElement) {
assistantMessageElement.innerHTML = `
<h4>Error:</h4>
<pre>${
error.message || "Error sending ticket to Dust. Please try again."
error.message ||
"Error receiving response from Dust. Please try again."
}</pre>
`;
}

// Scroll to the bottom of the dustResponse div
dustResponse.scrollTop = dustResponse.scrollHeight;
} finally {
userInput.disabled = false; // Re-enable the textarea
userInput.disabled = false;
sendToDustButton.innerHTML = `
<svg viewBox="0 0 24 24">
<path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/>
Expand Down
2 changes: 1 addition & 1 deletion zendesk/zd-app/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"ticket_sidebar": "assets/iframe.html"
}
},
"version": "1.0.6",
"version": "1.0.7",
"frameworkVersion": "2.0",
"screenshots": [
{
Expand Down

0 comments on commit d17e4c0

Please sign in to comment.