-
Notifications
You must be signed in to change notification settings - Fork 478
Expand file tree
/
Copy pathscript.js
More file actions
376 lines (306 loc) · 14.4 KB
/
Copy pathscript.js
File metadata and controls
376 lines (306 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
import { createCopilotClient } from "./copilotstudio/index.js";
const form = document.getElementById("ticket-form");
const statusNode = document.getElementById("status");
const yearNode = document.getElementById("year");
const submitButton = form.querySelector("button[type='submit']");
const apiKeyInput = form.querySelector("input[name='apiKey']");
const thinkingNode = document.getElementById("thinking");
const thinkingLabelNode = thinkingNode?.querySelector(".thinking-label");
const thinkingUpdatesNode = document.getElementById("thinking-updates");
const thinkingLabels = [
"Copilot Studio is thinking...",
"Checking your submission...",
"Weaving ideas into shape...",
"Reviewing possible options...",
"Verifying details...",
"Processing your request...",
"Review in progress...",
"Compiling results..."
];
const labelChangeIntervalMs = 3700;
const labelTransitionDurationMs = 220;
const maxThinkingUpdates = 5;
let labelIntervalId;
let labelTimeoutId;
let currentLabelIndex = 0;
let isThinkingActive = false;
let copilotClient;
let conversationId;
let initializationError;
const resolveApiKey = () => {
// for testing you could return your API key here, otherwise get it from the HTML or, in production, implement a secure proxy/backend
const rawKey = apiKeyInput?.value;
return typeof rawKey === "string" ? rawKey.trim() : "";
};
// Initialize the Copilot Studio client and start a conversation
const clientReady = (async () => {
try {
copilotClient = await createCopilotClient();
for await (const act of copilotClient.startConversationAsync(true)) {
conversationId = act.conversation?.id ?? conversationId;
if (conversationId) {
break; // Stop once we have a conversation identifier
}
}
} catch (error) {
initializationError = error;
console.error("Unable to initialize Copilot Studio client.", error);
}
})();
// Thinking label management functions
const clearThinkingTimers = () => {
if (labelIntervalId) {
window.clearInterval(labelIntervalId);
labelIntervalId = undefined;
}
if (labelTimeoutId) {
window.clearTimeout(labelTimeoutId);
labelTimeoutId = undefined;
}
};
const applyThinkingLabel = (label, { immediate = false } = {}) => {
if (!thinkingLabelNode) {
return;
}
if (immediate) {
thinkingLabelNode.textContent = label;
thinkingLabelNode.classList.remove("thinking-label--changing");
return;
}
thinkingLabelNode.classList.add("thinking-label--changing");
if (labelTimeoutId) {
window.clearTimeout(labelTimeoutId);
}
labelTimeoutId = window.setTimeout(() => {
thinkingLabelNode.textContent = label;
thinkingLabelNode.classList.remove("thinking-label--changing");
labelTimeoutId = undefined;
}, labelTransitionDurationMs);
};
const startThinkingRotation = () => {
if (!thinkingLabelNode || thinkingLabels.length <= 1) {
return;
}
labelIntervalId = window.setInterval(() => {
currentLabelIndex = (currentLabelIndex + 1) % thinkingLabels.length;
applyThinkingLabel(thinkingLabels[currentLabelIndex]);
}, labelChangeIntervalMs);
};
const resetThinkingUpdates = () => {
if (!thinkingUpdatesNode) {
return;
}
thinkingUpdatesNode.replaceChildren();
};
const addThinkingUpdate = (updateText) => {
if (!thinkingUpdatesNode || !isThinkingActive) {
return;
}
const normalized = typeof updateText === "string" ? updateText.trim() : "";
if (!normalized) {
return;
}
const updateNode = document.createElement("div");
updateNode.className = "thinking-update thinking-update--fresh";
updateNode.textContent = normalized;
thinkingUpdatesNode.prepend(updateNode);
while (thinkingUpdatesNode.children.length > maxThinkingUpdates) {
thinkingUpdatesNode.removeChild(thinkingUpdatesNode.lastElementChild);
}
const finalizeAnimation = () => updateNode.classList.remove("thinking-update--fresh");
if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
window.requestAnimationFrame(finalizeAnimation);
} else {
setTimeout(finalizeAnimation, 260);
}
};
const setThinking = (isVisible) => {
if (!thinkingNode) {
return;
}
clearThinkingTimers();
if (isVisible) {
isThinkingActive = true;
resetThinkingUpdates();
submitButton.hidden = true;
thinkingNode.hidden = false;
currentLabelIndex = 0;
applyThinkingLabel(thinkingLabels[currentLabelIndex], { immediate: true });
startThinkingRotation();
return;
}
isThinkingActive = false;
resetThinkingUpdates();
thinkingNode.hidden = true;
submitButton.hidden = false;
currentLabelIndex = 0;
applyThinkingLabel(thinkingLabels[currentLabelIndex], { immediate: true });
};
// Status result message management
const setStatus = (message, type, asHtml = false) => {
statusNode.className = `status${type ? ` ${type}` : ""}`;
if (asHtml) {
statusNode.innerHTML = message;
} else {
statusNode.textContent = message;
}
};
const summarize = async (textToSummarize) => {
const resolvedKey = resolveApiKey();
if (!resolvedKey) {
throw new Error("Azure OpenAI API key is missing.");
}
const payload = {
"messages": [
{ "role": "system", "content": "You summarize any message received in no more than 10 words. You need to maintain their original sense, but no more than 10 words per summary. Some examples: 1) \"The user wants to create a new ticket with:\n- Title: \u0022VPN outage in germany\u0022\n- Description: \u0022all operations blocked open ticket\u0022\n\nAccording to the instructions, before creating a new ticket, I need to:\n1. Check if there is an existing ticket that is semantically similar\n2. If a similar ticket exists, don\u0027t create a new one - instead add a comment to the existing ticket and update its title\n3. If no similar ticket exists, proceed to create a new one\n\nThe ticket should be created in the \u0022Unassigned Tickets\u0022 category (which is a group on the website issue tracker board).\n\nFirst, I need to find the board and check for existing similar tickets. I should search for tickets related to VPN, Germany, or outages.\n\nLet me start by getting board information to understand the structure, then search for existing similar tickets.\n\nWait, I don\u0027t have a specific board ID yet. I need to either:\n1. List workspaces to find the right board\n2. Or try to search for items related to VPN/Germany\n\nActually, looking at the instructions again, it mentions \u0022website issue tracker board\u0022 - this seems to be a specific board. But I don\u0027t have the board ID.\n\nLet me think about the approach:\n1. I could list workspaces to find the board\n2. Or I could try to search broadly\n\nActually, I don\u0027t have enough context about which board to use. But the instructions mention \u0022website issue tracker board\u0022 and \u0022Unassigned Tickets\u0022 group. \n\nLet me start by listing workspaces to find the appropriate board, or I could ask the user for the board ID. But according to the instructions, I should not ask for missing inputs and should call tools without them.\n\nHmm, but I need a board ID to check for existing tickets and to create new ones. Let me list workspaces first to find the right board.\" should become \"User wants a new ticket about VPN outage. Let me seek into the workspace for a similar one.\" 2) \"I\u0027ll help you create this ticket. First, let me check if there are any existing similar tickets related to VPN outages or issues in Germany to avoid duplicates.\" becomes \"Checking for duplicates related to VPN outages in Germany...\" 3) \"Good, I found a workspace called \u0022Tickets queue\u0022 with ID 5124896. Now I need to get the boards within this workspace to find the \u0022website issue tracker board\u0022 or the relevant board where tickets are tracked.\" becomes \"Found 'tickets queue' workspace. Let me seek for a website issue tracker board" },
{ "role": "user", "content": textToSummarize }
],
"max_completion_tokens": 5000,
"temperature": 1,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0,
"model": "gpt-4.1-mini"
};
const endpoint = "YOUR-AOAI-ENDPOINT/chat/completions?api-version=2025-01-01-preview";
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${resolvedKey}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`OpenAI request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const content = data?.choices?.[0]?.message?.content;
if (!content) {
throw new Error("OpenAI response did not contain a summary.");
}
return content;
};
form.addEventListener("submit", async (event) => {
event.preventDefault();
const shortTitle = form.shortTitle.value;
const longDescription = form.longDescription.value;
setThinking(true);
submitButton.disabled = true;
try {
await clientReady;
const prompt = `Create the following ticket:\n\nTitle: ${shortTitle}\nDescription: ${longDescription}`;
const activityStream = copilotClient.askQuestionAsync(prompt, conversationId);
const agentMessages = [];
const thinkingMessages = [];
const streamLastActivity = new Map();
const resolveStreamKey = (activity) => {
if (!activity) {
return "stream:__unidentified";
}
const streamId = activity.channelData?.streamId;
if (streamId) {
return `stream:${streamId}`;
}
const activityId = activity.id;
if (activityId) {
return `activity:${activityId}`;
}
return "stream:__default";
};
const normalizeActivityText = (text) => (typeof text === "string" ? text.trim() : "");
const isContinuationOfPrevious = (previousActivity, nextActivity) => {
const previousText = normalizeActivityText(previousActivity?.text);
const nextText = normalizeActivityText(nextActivity?.text);
if (!previousText || !nextText) {
return false;
}
return nextText.startsWith(previousText);
};
const isRegressionOf = (previousText, nextActivity) => {
const nextText = normalizeActivityText(nextActivity?.text);
if (!previousText || !nextText) {
return false;
}
return previousText.startsWith(nextText);
};
const flushActivity = async (activityToFlush, recordForUi = false) => {
const finalText = activityToFlush?.text?.trim();
if (!finalText) {
return;
}
if (recordForUi) {
agentMessages.push(finalText);
} else {
thinkingMessages.push(finalText);
try {
const summary = await summarize(finalText);
addThinkingUpdate(summary);
} catch (summaryError) {
console.error("Unable to summarize activity text.", summaryError);
}
}
};
for await (const activity of activityStream) {
if (!activity) {
continue;
}
const activityType = activity.type?.toLowerCase();
if (activityType === "event") {
continue;
}
if (activityType === "typing" && activity.channelData?.streamType === "informative") {
const streamKey = resolveStreamKey(activity);
const previousActivity = streamLastActivity.get(streamKey);
if (previousActivity) {
if (isContinuationOfPrevious(previousActivity, activity)) {
streamLastActivity.set(streamKey, activity);
continue;
}
if (isRegressionOf(thinkingMessages[0], activity)) {
continue;
}
await flushActivity(previousActivity, false);
}
streamLastActivity.set(streamKey, activity);
continue;
}
if (activityType === "message") {
for (const [streamKey, pendingActivity] of Array.from(streamLastActivity.entries())) {
if (pendingActivity?.type?.toLowerCase() === "typing" && !isRegressionOf(thinkingMessages[0], pendingActivity)) {
await flushActivity(pendingActivity, false);
}
streamLastActivity.delete(streamKey);
}
const messageText = normalizeActivityText(activity.text);
if (messageText) {
agentMessages.push(messageText);
}
continue;
}
}
for (const activity of streamLastActivity.values()) {
if (activity?.type?.toLowerCase() === "typing") {
await flushActivity(activity, true);
}
}
// Handle HTML formatting in agent messages
const formattedMessages = agentMessages.map((message) => {
const content = message.includes("<") ? message : message.replace(/\n/g, "<br>");
return `<div class="agent-message">${content}</div>`;
});
const htmlResponse = formattedMessages.length > 0
? formattedMessages.join("<div class=\"agent-divider\" role=\"presentation\"></div>")
: "<div class=\"agent-message\">Ticket summary sent. Awaiting agent response.</div>";
setStatus(htmlResponse, "success", true);
form.reset();
} catch (error) {
setStatus("We could not relay this ticket to the Copilot agent. Refresh and try again.", "error");
console.error(error);
} finally {
setThinking(false);
submitButton.disabled = false;
}
});
yearNode.textContent = new Date().getFullYear();
applyThinkingLabel(thinkingLabels[currentLabelIndex], { immediate: true });