@cremini/skillpack 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/cli.js +492 -0
- package/package.json +64 -0
- package/runtime/README.md +41 -0
- package/runtime/scripts/start.bat +15 -0
- package/runtime/scripts/start.sh +22 -0
- package/runtime/server/chat-proxy.js +161 -0
- package/runtime/server/index.js +59 -0
- package/runtime/server/package.json +13 -0
- package/runtime/server/routes.js +104 -0
- package/runtime/server/skills-loader.js +31 -0
- package/runtime/web/app.js +582 -0
- package/runtime/web/index.html +72 -0
- package/runtime/web/marked.min.js +2215 -0
- package/runtime/web/styles.css +774 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
const API_BASE = "";
|
|
2
|
+
let chatHistory = [];
|
|
3
|
+
|
|
4
|
+
// Initialize
|
|
5
|
+
async function init() {
|
|
6
|
+
await loadConfig();
|
|
7
|
+
setupEventListeners();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function loadConfig() {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(API_BASE + "/api/config");
|
|
13
|
+
const config = await res.json();
|
|
14
|
+
|
|
15
|
+
document.getElementById("pack-name").textContent = config.name;
|
|
16
|
+
document.getElementById("pack-desc").textContent = config.description;
|
|
17
|
+
document.title = config.name;
|
|
18
|
+
|
|
19
|
+
// Skills
|
|
20
|
+
const skillsList = document.getElementById("skills-list");
|
|
21
|
+
skillsList.innerHTML = config.skills
|
|
22
|
+
.map(
|
|
23
|
+
(s) =>
|
|
24
|
+
`<li><div class="skill-name">${s.name}</div><div class="skill-desc">${s.description}</div></li>`,
|
|
25
|
+
)
|
|
26
|
+
.join("");
|
|
27
|
+
|
|
28
|
+
// Pre-fill when there is exactly one prompt
|
|
29
|
+
if (config.prompts && config.prompts.length === 1) {
|
|
30
|
+
const input = document.getElementById("user-input");
|
|
31
|
+
input.value = config.prompts[0];
|
|
32
|
+
input.style.height = "auto";
|
|
33
|
+
input.style.height = Math.min(input.scrollHeight, 120) + "px";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// API key status and provider
|
|
37
|
+
const keyStatus = document.getElementById("key-status");
|
|
38
|
+
if (config.hasApiKey) {
|
|
39
|
+
keyStatus.textContent = "API key configured";
|
|
40
|
+
keyStatus.className = "status-text success";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const providerSelect = document.getElementById("provider-select");
|
|
44
|
+
if (providerSelect && config.provider) {
|
|
45
|
+
providerSelect.value = config.provider;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const updatePlaceholder = () => {
|
|
49
|
+
const p = providerSelect.value;
|
|
50
|
+
const input = document.getElementById("api-key-input");
|
|
51
|
+
if (p === "openai") input.placeholder = "sk-proj-...";
|
|
52
|
+
else if (p === "anthropic") input.placeholder = "sk-ant-api03-...";
|
|
53
|
+
else input.placeholder = "sk-...";
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
providerSelect.addEventListener("change", updatePlaceholder);
|
|
57
|
+
updatePlaceholder();
|
|
58
|
+
|
|
59
|
+
// Show welcome view
|
|
60
|
+
showWelcome(config);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error("Failed to load config:", err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function showWelcome(config) {
|
|
67
|
+
const welcomeContent = document.getElementById("welcome-content");
|
|
68
|
+
|
|
69
|
+
let promptsHtml = "";
|
|
70
|
+
if (config.prompts && config.prompts.length > 1) {
|
|
71
|
+
promptsHtml = `
|
|
72
|
+
<div class="prompt-cards">
|
|
73
|
+
${config.prompts
|
|
74
|
+
.map(
|
|
75
|
+
(u, i) => `
|
|
76
|
+
<div class="prompt-card" data-index="${i}" title="${u}">
|
|
77
|
+
${u.length > 60 ? u.substring(0, 60) + "..." : u}
|
|
78
|
+
</div>
|
|
79
|
+
`,
|
|
80
|
+
)
|
|
81
|
+
.join("")}
|
|
82
|
+
</div>
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (welcomeContent) {
|
|
87
|
+
welcomeContent.innerHTML = `
|
|
88
|
+
<div class="welcome-message">
|
|
89
|
+
<h2>Turn Skills into a Standalone App with UI</h2>
|
|
90
|
+
<p>One command to orchestrate skills into a standalone app users can download and use on their computer</p>
|
|
91
|
+
${promptsHtml}
|
|
92
|
+
</div>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setupEventListeners() {
|
|
98
|
+
// Send button
|
|
99
|
+
document.getElementById("send-btn").addEventListener("click", sendMessage);
|
|
100
|
+
|
|
101
|
+
// Send on Enter
|
|
102
|
+
document.getElementById("user-input").addEventListener("keydown", (e) => {
|
|
103
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
sendMessage();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Auto-resize the input box
|
|
110
|
+
document.getElementById("user-input").addEventListener("input", (e) => {
|
|
111
|
+
e.target.style.height = "auto";
|
|
112
|
+
e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px";
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Save API key
|
|
116
|
+
document.getElementById("save-key-btn").addEventListener("click", saveApiKey);
|
|
117
|
+
|
|
118
|
+
// Prompt click
|
|
119
|
+
const welcomeContent = document.getElementById("welcome-content");
|
|
120
|
+
if (welcomeContent) {
|
|
121
|
+
welcomeContent.addEventListener("click", (e) => {
|
|
122
|
+
const item = e.target.closest(".prompt-card");
|
|
123
|
+
if (!item) return;
|
|
124
|
+
const index = parseInt(item.dataset.index);
|
|
125
|
+
|
|
126
|
+
// Get the full prompt text
|
|
127
|
+
fetch(API_BASE + "/api/config")
|
|
128
|
+
.then((r) => r.json())
|
|
129
|
+
.then((config) => {
|
|
130
|
+
if (config.prompts[index]) {
|
|
131
|
+
const input = document.getElementById("user-input");
|
|
132
|
+
input.value = config.prompts[index];
|
|
133
|
+
input.focus();
|
|
134
|
+
input.style.height = "auto";
|
|
135
|
+
input.style.height = Math.min(input.scrollHeight, 120) + "px";
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function saveApiKey() {
|
|
143
|
+
const input = document.getElementById("api-key-input");
|
|
144
|
+
const providerSelect = document.getElementById("provider-select");
|
|
145
|
+
const status = document.getElementById("key-status");
|
|
146
|
+
const key = input.value.trim();
|
|
147
|
+
const provider = providerSelect ? providerSelect.value : "openai";
|
|
148
|
+
|
|
149
|
+
if (!key) {
|
|
150
|
+
status.textContent = "Enter an API key";
|
|
151
|
+
status.className = "status-text error";
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch(API_BASE + "/api/config/key", {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify({ key, provider }),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (res.ok) {
|
|
163
|
+
status.textContent = "API key saved";
|
|
164
|
+
status.className = "status-text success";
|
|
165
|
+
input.value = "";
|
|
166
|
+
} else {
|
|
167
|
+
status.textContent = "Save failed";
|
|
168
|
+
status.className = "status-text error";
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
status.textContent = "Save failed: " + err.message;
|
|
172
|
+
status.className = "status-text error";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let ws = null;
|
|
177
|
+
let currentAssistantMsg = null;
|
|
178
|
+
|
|
179
|
+
function renderMarkdown(mdText, { renderEmbeddedMarkdown = true } = {}) {
|
|
180
|
+
if (typeof marked === "undefined") {
|
|
181
|
+
return escapeHtml(mdText);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const html = marked.parse(mdText);
|
|
185
|
+
if (!renderEmbeddedMarkdown) {
|
|
186
|
+
return html;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return renderEmbeddedMarkdownBlocks(html);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function renderEmbeddedMarkdownBlocks(html) {
|
|
193
|
+
const template = document.createElement("template");
|
|
194
|
+
template.innerHTML = html;
|
|
195
|
+
|
|
196
|
+
const codeBlocks = template.content.querySelectorAll("pre > code");
|
|
197
|
+
codeBlocks.forEach((codeEl) => {
|
|
198
|
+
const languageClass = Array.from(codeEl.classList).find((className) =>
|
|
199
|
+
className.startsWith("language-"),
|
|
200
|
+
);
|
|
201
|
+
const language = languageClass
|
|
202
|
+
? languageClass.slice("language-".length)
|
|
203
|
+
: "";
|
|
204
|
+
|
|
205
|
+
if (!/^(markdown|md)$/i.test(language)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const preview = document.createElement("div");
|
|
210
|
+
preview.className = "embedded-markdown-preview markdown-body";
|
|
211
|
+
preview.innerHTML = renderMarkdown(codeEl.textContent || "", {
|
|
212
|
+
renderEmbeddedMarkdown: false,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const pre = codeEl.parentElement;
|
|
216
|
+
if (pre) {
|
|
217
|
+
pre.replaceWith(preview);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return template.innerHTML;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function getOrCreateWs() {
|
|
225
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
226
|
+
return ws;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
231
|
+
const providerSelect = document.getElementById("provider-select");
|
|
232
|
+
const provider = providerSelect ? providerSelect.value : "openai";
|
|
233
|
+
|
|
234
|
+
// URLSearchParams would be cleaner if more query params are added later
|
|
235
|
+
const wsUrl = `${protocol}//${window.location.host}${API_BASE}/api/chat?provider=${provider}`;
|
|
236
|
+
|
|
237
|
+
ws = new WebSocket(wsUrl);
|
|
238
|
+
|
|
239
|
+
ws.onopen = () => resolve(ws);
|
|
240
|
+
ws.onerror = (err) => {
|
|
241
|
+
console.error(err);
|
|
242
|
+
reject(new Error("WebSocket connection failed"));
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
ws.onmessage = (event) => {
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(event.data);
|
|
248
|
+
if (parsed.error) {
|
|
249
|
+
handleError(parsed.error);
|
|
250
|
+
} else if (parsed.done) {
|
|
251
|
+
handleDone();
|
|
252
|
+
} else if (parsed.type) {
|
|
253
|
+
handleAgentEvent(parsed);
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
console.error("Failed to parse message", e);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
ws.onclose = () => {
|
|
261
|
+
ws = null;
|
|
262
|
+
enableInput();
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function handleError(errorMsg) {
|
|
268
|
+
if (!currentAssistantMsg) {
|
|
269
|
+
appendMessage("assistant", "Error: " + errorMsg).classList.add("error");
|
|
270
|
+
} else {
|
|
271
|
+
const errDiv = document.createElement("div");
|
|
272
|
+
errDiv.className = "content error-text";
|
|
273
|
+
errDiv.textContent = "Error: " + errorMsg;
|
|
274
|
+
currentAssistantMsg.appendChild(errDiv);
|
|
275
|
+
currentAssistantMsg.classList.add("error");
|
|
276
|
+
}
|
|
277
|
+
enableInput();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function handleDone() {
|
|
281
|
+
let fullText = "";
|
|
282
|
+
if (currentAssistantMsg) {
|
|
283
|
+
const blocks = currentAssistantMsg.querySelectorAll(".text-block");
|
|
284
|
+
blocks.forEach((b) => {
|
|
285
|
+
fullText += b.dataset.mdContent + "\n";
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
chatHistory.push({ role: "assistant", content: fullText });
|
|
289
|
+
enableInput();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function showLoadingIndicator() {
|
|
293
|
+
if (!currentAssistantMsg) return;
|
|
294
|
+
let indicator = currentAssistantMsg.querySelector(".loading-indicator");
|
|
295
|
+
if (!indicator) {
|
|
296
|
+
indicator = document.createElement("div");
|
|
297
|
+
indicator.className = "loading-indicator";
|
|
298
|
+
indicator.innerHTML = `<span></span><span></span><span></span>`;
|
|
299
|
+
currentAssistantMsg.appendChild(indicator);
|
|
300
|
+
}
|
|
301
|
+
indicator.style.display = "flex";
|
|
302
|
+
scrollToBottom();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function hideLoadingIndicator() {
|
|
306
|
+
if (!currentAssistantMsg) return;
|
|
307
|
+
const indicator = currentAssistantMsg.querySelector(".loading-indicator");
|
|
308
|
+
if (indicator) {
|
|
309
|
+
indicator.style.display = "none";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function handleAgentEvent(event) {
|
|
314
|
+
if (!currentAssistantMsg) return;
|
|
315
|
+
|
|
316
|
+
if (
|
|
317
|
+
["text_delta", "thinking_delta", "tool_start", "tool_end"].includes(
|
|
318
|
+
event.type,
|
|
319
|
+
)
|
|
320
|
+
) {
|
|
321
|
+
hideLoadingIndicator();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
switch (event.type) {
|
|
325
|
+
case "agent_start":
|
|
326
|
+
case "message_start":
|
|
327
|
+
showLoadingIndicator();
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
case "agent_end":
|
|
331
|
+
case "message_end":
|
|
332
|
+
hideLoadingIndicator();
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case "thinking_delta":
|
|
336
|
+
const thinkingBlock = getOrCreateThinkingBlock();
|
|
337
|
+
thinkingBlock.dataset.mdContent += event.delta;
|
|
338
|
+
const contentEl = thinkingBlock.querySelector(".thinking-content");
|
|
339
|
+
if (typeof marked !== "undefined") {
|
|
340
|
+
contentEl.innerHTML = renderMarkdown(thinkingBlock.dataset.mdContent);
|
|
341
|
+
} else {
|
|
342
|
+
contentEl.textContent = thinkingBlock.dataset.mdContent;
|
|
343
|
+
}
|
|
344
|
+
scrollToBottom();
|
|
345
|
+
break;
|
|
346
|
+
|
|
347
|
+
case "text_delta":
|
|
348
|
+
const textBlock = getOrCreateTextBlock();
|
|
349
|
+
textBlock.dataset.mdContent += event.delta;
|
|
350
|
+
if (typeof marked !== "undefined") {
|
|
351
|
+
textBlock.innerHTML = renderMarkdown(textBlock.dataset.mdContent);
|
|
352
|
+
} else {
|
|
353
|
+
textBlock.textContent = textBlock.dataset.mdContent;
|
|
354
|
+
}
|
|
355
|
+
scrollToBottom();
|
|
356
|
+
break;
|
|
357
|
+
|
|
358
|
+
case "tool_start":
|
|
359
|
+
const toolCard = document.createElement("div");
|
|
360
|
+
toolCard.className = "tool-card running collapsed";
|
|
361
|
+
const safeInput =
|
|
362
|
+
typeof event.toolInput === "string"
|
|
363
|
+
? event.toolInput
|
|
364
|
+
: JSON.stringify(event.toolInput, null, 2);
|
|
365
|
+
|
|
366
|
+
let inputHtml = "";
|
|
367
|
+
if (typeof marked !== "undefined") {
|
|
368
|
+
inputHtml = marked.parse("```json\n" + safeInput + "\n```");
|
|
369
|
+
} else {
|
|
370
|
+
inputHtml = escapeHtml(safeInput);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
toolCard.innerHTML = `
|
|
374
|
+
<div class="tool-header">
|
|
375
|
+
<span class="tool-chevron">
|
|
376
|
+
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
|
377
|
+
</span>
|
|
378
|
+
<span class="tool-icon">🛠️</span>
|
|
379
|
+
<span class="tool-name">${escapeHtml(event.toolName)}</span>
|
|
380
|
+
<span class="tool-status spinner"></span>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="tool-content">
|
|
383
|
+
<div class="tool-input markdown-body">${inputHtml}</div>
|
|
384
|
+
<div class="tool-result markdown-body" style="display: none;"></div>
|
|
385
|
+
</div>
|
|
386
|
+
`;
|
|
387
|
+
|
|
388
|
+
toolCard.querySelector(".tool-header").addEventListener("click", () => {
|
|
389
|
+
toolCard.classList.toggle("collapsed");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Insert before loading indicator if exists
|
|
393
|
+
const toolIndicator =
|
|
394
|
+
currentAssistantMsg.querySelector(".loading-indicator");
|
|
395
|
+
if (toolIndicator) {
|
|
396
|
+
currentAssistantMsg.insertBefore(toolCard, toolIndicator);
|
|
397
|
+
} else {
|
|
398
|
+
currentAssistantMsg.appendChild(toolCard);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
toolCard.dataset.toolName = event.toolName;
|
|
402
|
+
scrollToBottom();
|
|
403
|
+
|
|
404
|
+
showLoadingIndicator();
|
|
405
|
+
break;
|
|
406
|
+
|
|
407
|
+
case "tool_end":
|
|
408
|
+
const cards = Array.from(
|
|
409
|
+
currentAssistantMsg.querySelectorAll(".tool-card.running"),
|
|
410
|
+
);
|
|
411
|
+
const card = cards
|
|
412
|
+
.reverse()
|
|
413
|
+
.find((c) => c.dataset.toolName === event.toolName);
|
|
414
|
+
if (card) {
|
|
415
|
+
card.classList.remove("running");
|
|
416
|
+
card.classList.add(event.isError ? "error" : "success");
|
|
417
|
+
|
|
418
|
+
if (event.isError) {
|
|
419
|
+
card.classList.remove("collapsed");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const statusEl = card.querySelector(".tool-status");
|
|
423
|
+
statusEl.className = "tool-status";
|
|
424
|
+
statusEl.textContent = event.isError ? "❌" : "✅";
|
|
425
|
+
|
|
426
|
+
const resultEl = card.querySelector(".tool-result");
|
|
427
|
+
resultEl.style.display = "block";
|
|
428
|
+
const safeResult =
|
|
429
|
+
typeof event.result === "string"
|
|
430
|
+
? event.result
|
|
431
|
+
: JSON.stringify(event.result, null, 2);
|
|
432
|
+
|
|
433
|
+
const mdText =
|
|
434
|
+
event.result &&
|
|
435
|
+
typeof event.result === "string" &&
|
|
436
|
+
(event.result.includes("\n") || event.result.length > 50)
|
|
437
|
+
? "```bash\n" + safeResult + "\n```"
|
|
438
|
+
: "```json\n" + safeResult + "\n```";
|
|
439
|
+
|
|
440
|
+
if (typeof marked !== "undefined") {
|
|
441
|
+
resultEl.innerHTML = marked.parse(mdText);
|
|
442
|
+
} else {
|
|
443
|
+
resultEl.textContent = safeResult;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
scrollToBottom();
|
|
447
|
+
|
|
448
|
+
showLoadingIndicator();
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function getOrCreateThinkingBlock() {
|
|
454
|
+
const children = Array.from(currentAssistantMsg.children).filter(
|
|
455
|
+
(c) => !c.classList.contains("loading-indicator"),
|
|
456
|
+
);
|
|
457
|
+
let lastChild = children[children.length - 1];
|
|
458
|
+
|
|
459
|
+
if (!lastChild || !lastChild.classList.contains("thinking-card")) {
|
|
460
|
+
lastChild = document.createElement("div");
|
|
461
|
+
lastChild.className = "tool-card thinking-card collapsed";
|
|
462
|
+
lastChild.dataset.mdContent = "";
|
|
463
|
+
|
|
464
|
+
lastChild.innerHTML = `
|
|
465
|
+
<div class="tool-header thinking-header">
|
|
466
|
+
<span class="tool-chevron">
|
|
467
|
+
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
|
468
|
+
</span>
|
|
469
|
+
<span class="tool-icon">🧠</span>
|
|
470
|
+
<span class="tool-name" style="color: var(--text-secondary);">Thinking Process</span>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="tool-content thinking-content markdown-body"></div>
|
|
473
|
+
`;
|
|
474
|
+
|
|
475
|
+
lastChild.querySelector(".tool-header").addEventListener("click", () => {
|
|
476
|
+
lastChild.classList.toggle("collapsed");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const indicator = currentAssistantMsg.querySelector(".loading-indicator");
|
|
480
|
+
if (indicator) {
|
|
481
|
+
currentAssistantMsg.insertBefore(lastChild, indicator);
|
|
482
|
+
} else {
|
|
483
|
+
currentAssistantMsg.appendChild(lastChild);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return lastChild;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function getOrCreateTextBlock() {
|
|
490
|
+
const children = Array.from(currentAssistantMsg.children).filter(
|
|
491
|
+
(c) => !c.classList.contains("loading-indicator"),
|
|
492
|
+
);
|
|
493
|
+
let lastChild = children[children.length - 1];
|
|
494
|
+
|
|
495
|
+
if (!lastChild || !lastChild.classList.contains("text-block")) {
|
|
496
|
+
lastChild = document.createElement("div");
|
|
497
|
+
lastChild.className = "content text-block markdown-body";
|
|
498
|
+
lastChild.dataset.mdContent = "";
|
|
499
|
+
|
|
500
|
+
const indicator = currentAssistantMsg.querySelector(".loading-indicator");
|
|
501
|
+
if (indicator) {
|
|
502
|
+
currentAssistantMsg.insertBefore(lastChild, indicator);
|
|
503
|
+
} else {
|
|
504
|
+
currentAssistantMsg.appendChild(lastChild);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return lastChild;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function enableInput() {
|
|
511
|
+
const sendBtn = document.getElementById("send-btn");
|
|
512
|
+
if (sendBtn) sendBtn.disabled = false;
|
|
513
|
+
currentAssistantMsg = null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function sendMessage() {
|
|
517
|
+
const input = document.getElementById("user-input");
|
|
518
|
+
const text = input.value.trim();
|
|
519
|
+
if (!text) return;
|
|
520
|
+
|
|
521
|
+
const chatArea = document.getElementById("chat-area");
|
|
522
|
+
if (chatArea.classList.contains("mode-welcome")) {
|
|
523
|
+
chatArea.classList.remove("mode-welcome");
|
|
524
|
+
chatArea.classList.add("mode-chat");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
input.value = "";
|
|
528
|
+
input.style.height = "auto";
|
|
529
|
+
|
|
530
|
+
// Add the user message
|
|
531
|
+
appendMessage("user", text);
|
|
532
|
+
chatHistory.push({ role: "user", content: text });
|
|
533
|
+
|
|
534
|
+
// Disable input while the agent is responding
|
|
535
|
+
const sendBtn = document.getElementById("send-btn");
|
|
536
|
+
sendBtn.disabled = true;
|
|
537
|
+
|
|
538
|
+
// Create an assistant message placeholder
|
|
539
|
+
currentAssistantMsg = appendMessage("assistant", "");
|
|
540
|
+
showLoadingIndicator();
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const socket = await getOrCreateWs();
|
|
544
|
+
socket.send(JSON.stringify({ text }));
|
|
545
|
+
} catch (err) {
|
|
546
|
+
handleError(err.message);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function appendMessage(role, text) {
|
|
551
|
+
const messages = document.getElementById("messages");
|
|
552
|
+
const div = document.createElement("div");
|
|
553
|
+
div.className = "message " + role;
|
|
554
|
+
|
|
555
|
+
if (role === "user") {
|
|
556
|
+
div.innerHTML = '<div class="content">' + escapeHtml(text) + "</div>";
|
|
557
|
+
} else if (text) {
|
|
558
|
+
const tb = document.createElement("div");
|
|
559
|
+
tb.className = "content text-block markdown-body";
|
|
560
|
+
tb.dataset.mdContent = text;
|
|
561
|
+
tb.innerHTML = renderMarkdown(text);
|
|
562
|
+
div.appendChild(tb);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
messages.appendChild(div);
|
|
566
|
+
scrollToBottom();
|
|
567
|
+
return div;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function escapeHtml(text) {
|
|
571
|
+
const div = document.createElement("div");
|
|
572
|
+
div.textContent = text;
|
|
573
|
+
return div.innerHTML;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function scrollToBottom() {
|
|
577
|
+
const messages = document.getElementById("messages");
|
|
578
|
+
messages.scrollTop = messages.scrollHeight;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Start the app
|
|
582
|
+
init();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta name="description" content="Skills Pack Chat" />
|
|
8
|
+
<title>Skills Pack</title>
|
|
9
|
+
<link rel="stylesheet" href="styles.css" />
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
12
|
+
</head>
|
|
13
|
+
|
|
14
|
+
<body>
|
|
15
|
+
<div id="app">
|
|
16
|
+
<aside id="sidebar">
|
|
17
|
+
<div class="sidebar-header">
|
|
18
|
+
<h1 id="pack-name">Skills Pack</h1>
|
|
19
|
+
<p id="pack-desc">Loading...</p>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="sidebar-section">
|
|
23
|
+
<h3>Skills</h3>
|
|
24
|
+
<ul id="skills-list"></ul>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="sidebar-section">
|
|
28
|
+
<h3>API KEYS</h3>
|
|
29
|
+
<div class="provider-select-wrapper">
|
|
30
|
+
<select id="provider-select">
|
|
31
|
+
<option value="openai">OpenAI</option>
|
|
32
|
+
<option value="anthropic">Anthropic</option>
|
|
33
|
+
</select>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="api-key-form">
|
|
36
|
+
<input type="password" id="api-key-input" placeholder="sk-..." />
|
|
37
|
+
</div>
|
|
38
|
+
<div class="api-key-footer">
|
|
39
|
+
<p id="key-status" class="status-text"></p>
|
|
40
|
+
<button id="save-key-btn">Save</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</aside>
|
|
44
|
+
|
|
45
|
+
<main id="chat-area" class="mode-welcome">
|
|
46
|
+
<div id="welcome-view">
|
|
47
|
+
<div id="welcome-content"></div>
|
|
48
|
+
</div>
|
|
49
|
+
<div id="chat-view">
|
|
50
|
+
<div id="messages"></div>
|
|
51
|
+
</div>
|
|
52
|
+
<div id="input-area">
|
|
53
|
+
<div class="input-container">
|
|
54
|
+
<textarea id="user-input" placeholder="Type a message..." rows="1"></textarea>
|
|
55
|
+
<button id="send-btn">
|
|
56
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
57
|
+
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />
|
|
58
|
+
</svg>
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
<p class="powered-by">
|
|
62
|
+
Powered by
|
|
63
|
+
<a href="https://skillpack.sh" target="_blank" rel="noopener noreferrer">SkillPack.sh</a>
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
</main>
|
|
67
|
+
</div>
|
|
68
|
+
<script src="marked.min.js"></script>
|
|
69
|
+
<script src="app.js"></script>
|
|
70
|
+
</body>
|
|
71
|
+
|
|
72
|
+
</html>
|