dot-agents 0.6.0 → 0.7.4
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/dist/cli/commands/channel.d.ts.map +1 -1
- package/dist/cli/commands/channel.js +70 -2
- package/dist/cli/commands/channel.js.map +1 -1
- package/dist/daemon/api/channels.d.ts +6 -0
- package/dist/daemon/api/channels.d.ts.map +1 -0
- package/dist/daemon/api/channels.js +143 -0
- package/dist/daemon/api/channels.js.map +1 -0
- package/dist/daemon/api/server.d.ts.map +1 -1
- package/dist/daemon/api/server.js +56 -0
- package/dist/daemon/api/server.js.map +1 -1
- package/dist/daemon/daemon.d.ts +18 -0
- package/dist/daemon/daemon.d.ts.map +1 -1
- package/dist/daemon/daemon.js +124 -6
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/lib/index.d.ts +1 -0
- package/dist/daemon/lib/index.d.ts.map +1 -1
- package/dist/daemon/lib/index.js +1 -0
- package/dist/daemon/lib/index.js.map +1 -1
- package/dist/daemon/lib/safeguards.d.ts +68 -0
- package/dist/daemon/lib/safeguards.d.ts.map +1 -0
- package/dist/daemon/lib/safeguards.js +135 -0
- package/dist/daemon/lib/safeguards.js.map +1 -0
- package/dist/daemon/lib/watcher.d.ts.map +1 -1
- package/dist/daemon/lib/watcher.js +48 -8
- package/dist/daemon/lib/watcher.js.map +1 -1
- package/dist/daemon/web/app.js +433 -0
- package/dist/daemon/web/index.html +68 -0
- package/dist/daemon/web/styles.css +452 -0
- package/dist/lib/environment.d.ts +1 -0
- package/dist/lib/environment.d.ts.map +1 -1
- package/dist/lib/environment.js +14 -0
- package/dist/lib/environment.js.map +1 -1
- package/dist/lib/invoke.d.ts +8 -0
- package/dist/lib/invoke.d.ts.map +1 -1
- package/dist/lib/invoke.js +28 -16
- package/dist/lib/invoke.js.map +1 -1
- package/dist/lib/processor.d.ts +8 -0
- package/dist/lib/processor.d.ts.map +1 -1
- package/dist/lib/processor.js +16 -2
- package/dist/lib/processor.js.map +1 -1
- package/dist/lib/validation/persona.d.ts.map +1 -1
- package/dist/lib/validation/persona.js +41 -4
- package/dist/lib/validation/persona.js.map +1 -1
- package/package.json +15 -9
- package/dist/lib/channel.test.d.ts +0 -2
- package/dist/lib/channel.test.d.ts.map +0 -1
- package/dist/lib/channel.test.js +0 -33
- package/dist/lib/channel.test.js.map +0 -1
- package/dist/lib/frontmatter.test.d.ts +0 -2
- package/dist/lib/frontmatter.test.d.ts.map +0 -1
- package/dist/lib/frontmatter.test.js +0 -60
- package/dist/lib/frontmatter.test.js.map +0 -1
- package/dist/lib/integration.test.d.ts +0 -2
- package/dist/lib/integration.test.d.ts.map +0 -1
- package/dist/lib/integration.test.js +0 -445
- package/dist/lib/integration.test.js.map +0 -1
- package/dist/lib/invoke.test.d.ts +0 -2
- package/dist/lib/invoke.test.d.ts.map +0 -1
- package/dist/lib/invoke.test.js +0 -82
- package/dist/lib/invoke.test.js.map +0 -1
- package/dist/lib/persona.test.d.ts +0 -2
- package/dist/lib/persona.test.d.ts.map +0 -1
- package/dist/lib/persona.test.js +0 -324
- package/dist/lib/persona.test.js.map +0 -1
- package/dist/lib/processor.test.d.ts +0 -2
- package/dist/lib/processor.test.d.ts.map +0 -1
- package/dist/lib/processor.test.js +0 -134
- package/dist/lib/processor.test.js.map +0 -1
- package/dist/lib/registry.test.d.ts +0 -2
- package/dist/lib/registry.test.d.ts.map +0 -1
- package/dist/lib/registry.test.js +0 -236
- package/dist/lib/registry.test.js.map +0 -1
- package/dist/lib/session-thread.test.d.ts +0 -2
- package/dist/lib/session-thread.test.d.ts.map +0 -1
- package/dist/lib/session-thread.test.js +0 -235
- package/dist/lib/session-thread.test.js.map +0 -1
- package/dist/lib/session.test.d.ts +0 -2
- package/dist/lib/session.test.d.ts.map +0 -1
- package/dist/lib/session.test.js +0 -336
- package/dist/lib/session.test.js.map +0 -1
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dot-agents Channels Web UI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const API_BASE = window.location.origin;
|
|
6
|
+
|
|
7
|
+
// State
|
|
8
|
+
let currentChannel = null;
|
|
9
|
+
let currentThread = null;
|
|
10
|
+
let channels = [];
|
|
11
|
+
let eventSource = null;
|
|
12
|
+
|
|
13
|
+
// DOM Elements
|
|
14
|
+
const channelListEl = document.getElementById("channel-list");
|
|
15
|
+
const channelNameEl = document.getElementById("channel-name");
|
|
16
|
+
const channelDescriptionEl = document.getElementById("channel-description");
|
|
17
|
+
const messagesEl = document.getElementById("messages");
|
|
18
|
+
const composerEl = document.getElementById("composer");
|
|
19
|
+
const messageFormEl = document.getElementById("message-form");
|
|
20
|
+
const messageInputEl = document.getElementById("message-input");
|
|
21
|
+
const threadPanelEl = document.getElementById("thread-panel");
|
|
22
|
+
const threadMessagesEl = document.getElementById("thread-messages");
|
|
23
|
+
const closeThreadBtn = document.getElementById("close-thread");
|
|
24
|
+
const replyFormEl = document.getElementById("reply-form");
|
|
25
|
+
const replyInputEl = document.getElementById("reply-input");
|
|
26
|
+
const connectionStatusEl = document.getElementById("connection-status");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format ISO timestamp to readable time
|
|
30
|
+
*/
|
|
31
|
+
function formatTime(isoString) {
|
|
32
|
+
const date = new Date(isoString);
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const diffMs = now - date;
|
|
35
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
36
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
37
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
38
|
+
|
|
39
|
+
if (diffMins < 1) return "just now";
|
|
40
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
41
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
42
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
43
|
+
|
|
44
|
+
return date.toLocaleDateString("en-US", {
|
|
45
|
+
month: "short",
|
|
46
|
+
day: "numeric",
|
|
47
|
+
year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format message ID (ISO timestamp) to readable datetime
|
|
53
|
+
*/
|
|
54
|
+
function formatDateTime(isoString) {
|
|
55
|
+
const date = new Date(isoString);
|
|
56
|
+
return date.toLocaleString("en-US", {
|
|
57
|
+
month: "short",
|
|
58
|
+
day: "numeric",
|
|
59
|
+
hour: "numeric",
|
|
60
|
+
minute: "2-digit",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Simple markdown-like rendering (basic support)
|
|
66
|
+
*/
|
|
67
|
+
function renderContent(content) {
|
|
68
|
+
// Escape HTML
|
|
69
|
+
let html = content
|
|
70
|
+
.replace(/&/g, "&")
|
|
71
|
+
.replace(/</g, "<")
|
|
72
|
+
.replace(/>/g, ">");
|
|
73
|
+
|
|
74
|
+
// Code blocks
|
|
75
|
+
html = html.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
|
76
|
+
|
|
77
|
+
// Inline code
|
|
78
|
+
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
79
|
+
|
|
80
|
+
// Bold
|
|
81
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
82
|
+
|
|
83
|
+
// Italic
|
|
84
|
+
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
85
|
+
|
|
86
|
+
// Line breaks to paragraphs
|
|
87
|
+
html = html
|
|
88
|
+
.split(/\n\n+/)
|
|
89
|
+
.map((p) => `<p>${p.replace(/\n/g, "<br>")}</p>`)
|
|
90
|
+
.join("");
|
|
91
|
+
|
|
92
|
+
return html;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Render a single message
|
|
97
|
+
*/
|
|
98
|
+
function renderMessage(message, isReply = false) {
|
|
99
|
+
const from = message.meta?.from || "unknown";
|
|
100
|
+
const host = message.meta?.host;
|
|
101
|
+
const tags = message.meta?.tags || [];
|
|
102
|
+
const replyCount = message.replies?.length || 0;
|
|
103
|
+
|
|
104
|
+
return `
|
|
105
|
+
<div class="message" data-id="${message.id}">
|
|
106
|
+
<div class="message-header">
|
|
107
|
+
<span class="message-from">${escapeHtml(from)}</span>
|
|
108
|
+
<span class="message-time">${formatDateTime(message.id)}</span>
|
|
109
|
+
${host ? `<span class="message-host">${escapeHtml(host)}</span>` : ""}
|
|
110
|
+
</div>
|
|
111
|
+
<div class="message-content">
|
|
112
|
+
${renderContent(message.content)}
|
|
113
|
+
</div>
|
|
114
|
+
<div class="message-footer">
|
|
115
|
+
${tags.length ? `<div class="message-tags">${tags.map((t) => `<span class="tag">${escapeHtml(t)}</span>`).join("")}</div>` : ""}
|
|
116
|
+
${!isReply && replyCount > 0 ? `<span class="reply-count" data-thread="${message.id}">${replyCount} ${replyCount === 1 ? "reply" : "replies"}</span>` : ""}
|
|
117
|
+
${!isReply ? `<span class="reply-count" data-thread="${message.id}" style="cursor: pointer;">${replyCount === 0 ? "Reply" : ""}</span>` : ""}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Escape HTML entities
|
|
125
|
+
*/
|
|
126
|
+
function escapeHtml(str) {
|
|
127
|
+
const div = document.createElement("div");
|
|
128
|
+
div.textContent = str;
|
|
129
|
+
return div.innerHTML;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Fetch and render channel list
|
|
134
|
+
*/
|
|
135
|
+
async function loadChannels() {
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(`${API_BASE}/channels`);
|
|
138
|
+
const data = await response.json();
|
|
139
|
+
channels = data.channels;
|
|
140
|
+
|
|
141
|
+
// Separate public channels (#) and DMs (@)
|
|
142
|
+
const publicChannels = channels.filter((c) => c.name.startsWith("#"));
|
|
143
|
+
const dmChannels = channels.filter((c) => c.name.startsWith("@"));
|
|
144
|
+
|
|
145
|
+
let html = "";
|
|
146
|
+
|
|
147
|
+
if (publicChannels.length > 0) {
|
|
148
|
+
html += `
|
|
149
|
+
<div class="channel-section">
|
|
150
|
+
<div class="channel-section-title">Channels</div>
|
|
151
|
+
${publicChannels
|
|
152
|
+
.map(
|
|
153
|
+
(c) => `
|
|
154
|
+
<div class="channel-item" data-channel="${c.name}">
|
|
155
|
+
<span class="channel-icon public">#</span>
|
|
156
|
+
<span class="channel-name">${escapeHtml(c.name.slice(1))}</span>
|
|
157
|
+
</div>
|
|
158
|
+
`
|
|
159
|
+
)
|
|
160
|
+
.join("")}
|
|
161
|
+
</div>
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (dmChannels.length > 0) {
|
|
166
|
+
html += `
|
|
167
|
+
<div class="channel-section">
|
|
168
|
+
<div class="channel-section-title">Direct Messages</div>
|
|
169
|
+
${dmChannels
|
|
170
|
+
.map(
|
|
171
|
+
(c) => `
|
|
172
|
+
<div class="channel-item" data-channel="${c.name}">
|
|
173
|
+
<span class="channel-icon dm">@</span>
|
|
174
|
+
<span class="channel-name">${escapeHtml(c.name.slice(1))}</span>
|
|
175
|
+
</div>
|
|
176
|
+
`
|
|
177
|
+
)
|
|
178
|
+
.join("")}
|
|
179
|
+
</div>
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (publicChannels.length === 0 && dmChannels.length === 0) {
|
|
184
|
+
html = '<div class="loading">No channels found</div>';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
channelListEl.innerHTML = html;
|
|
188
|
+
|
|
189
|
+
// Add click handlers
|
|
190
|
+
channelListEl.querySelectorAll(".channel-item").forEach((item) => {
|
|
191
|
+
item.addEventListener("click", () => {
|
|
192
|
+
const channelName = item.dataset.channel;
|
|
193
|
+
selectChannel(channelName);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error("Failed to load channels:", error);
|
|
198
|
+
channelListEl.innerHTML =
|
|
199
|
+
'<div class="loading">Failed to load channels</div>';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Select and load a channel
|
|
205
|
+
*/
|
|
206
|
+
async function selectChannel(channelName) {
|
|
207
|
+
currentChannel = channelName;
|
|
208
|
+
currentThread = null;
|
|
209
|
+
|
|
210
|
+
// Update sidebar
|
|
211
|
+
channelListEl.querySelectorAll(".channel-item").forEach((item) => {
|
|
212
|
+
item.classList.toggle("active", item.dataset.channel === channelName);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Update header
|
|
216
|
+
const channelData = channels.find((c) => c.name === channelName);
|
|
217
|
+
channelNameEl.textContent = channelName;
|
|
218
|
+
channelDescriptionEl.textContent = channelData?.metadata?.description || "";
|
|
219
|
+
|
|
220
|
+
// Show composer
|
|
221
|
+
composerEl.style.display = "block";
|
|
222
|
+
|
|
223
|
+
// Close thread panel
|
|
224
|
+
closeThread();
|
|
225
|
+
|
|
226
|
+
// Load messages
|
|
227
|
+
await loadMessages(channelName);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Load messages for a channel
|
|
232
|
+
*/
|
|
233
|
+
async function loadMessages(channelName) {
|
|
234
|
+
messagesEl.innerHTML =
|
|
235
|
+
'<div class="empty-state"><div class="loading-spinner"></div></div>';
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const response = await fetch(
|
|
239
|
+
`${API_BASE}/channels/${encodeURIComponent(channelName)}?limit=100`
|
|
240
|
+
);
|
|
241
|
+
const data = await response.json();
|
|
242
|
+
|
|
243
|
+
if (data.messages.length === 0) {
|
|
244
|
+
messagesEl.innerHTML =
|
|
245
|
+
'<div class="empty-state"><p>No messages in this channel</p></div>';
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Reverse to show oldest first (chronological order)
|
|
250
|
+
const messages = [...data.messages].reverse();
|
|
251
|
+
messagesEl.innerHTML = messages.map((m) => renderMessage(m)).join("");
|
|
252
|
+
|
|
253
|
+
// Scroll to bottom
|
|
254
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
255
|
+
|
|
256
|
+
// Add thread click handlers
|
|
257
|
+
messagesEl.querySelectorAll(".reply-count").forEach((el) => {
|
|
258
|
+
el.addEventListener("click", () => {
|
|
259
|
+
const threadId = el.dataset.thread;
|
|
260
|
+
openThread(threadId);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error("Failed to load messages:", error);
|
|
265
|
+
messagesEl.innerHTML =
|
|
266
|
+
'<div class="empty-state"><p>Failed to load messages</p></div>';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Open thread panel for a message
|
|
272
|
+
*/
|
|
273
|
+
async function openThread(messageId) {
|
|
274
|
+
currentThread = messageId;
|
|
275
|
+
threadPanelEl.style.display = "flex";
|
|
276
|
+
|
|
277
|
+
// Find the message
|
|
278
|
+
const response = await fetch(
|
|
279
|
+
`${API_BASE}/channels/${encodeURIComponent(currentChannel)}/${encodeURIComponent(messageId)}`
|
|
280
|
+
);
|
|
281
|
+
const data = await response.json();
|
|
282
|
+
|
|
283
|
+
if (!data.message) {
|
|
284
|
+
threadMessagesEl.innerHTML = "<p>Message not found</p>";
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const message = data.message;
|
|
289
|
+
|
|
290
|
+
// Render original message + replies
|
|
291
|
+
let html = renderMessage(message, true);
|
|
292
|
+
|
|
293
|
+
if (message.replies && message.replies.length > 0) {
|
|
294
|
+
html += message.replies.map((r) => renderMessage(r, true)).join("");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
threadMessagesEl.innerHTML = html;
|
|
298
|
+
threadMessagesEl.scrollTop = threadMessagesEl.scrollHeight;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Close thread panel
|
|
303
|
+
*/
|
|
304
|
+
function closeThread() {
|
|
305
|
+
currentThread = null;
|
|
306
|
+
threadPanelEl.style.display = "none";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Send a message to current channel
|
|
311
|
+
*/
|
|
312
|
+
async function sendMessage(content) {
|
|
313
|
+
if (!currentChannel || !content.trim()) return;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch(
|
|
317
|
+
`${API_BASE}/channels/${encodeURIComponent(currentChannel)}`,
|
|
318
|
+
{
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: { "Content-Type": "application/json" },
|
|
321
|
+
body: JSON.stringify({
|
|
322
|
+
content: content.trim(),
|
|
323
|
+
from: "web-ui",
|
|
324
|
+
}),
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (response.ok) {
|
|
329
|
+
messageInputEl.value = "";
|
|
330
|
+
await loadMessages(currentChannel);
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error("Failed to send message:", error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Reply to a thread
|
|
339
|
+
*/
|
|
340
|
+
async function sendReply(content) {
|
|
341
|
+
if (!currentChannel || !currentThread || !content.trim()) return;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const response = await fetch(
|
|
345
|
+
`${API_BASE}/channels/${encodeURIComponent(currentChannel)}/${encodeURIComponent(currentThread)}/reply`,
|
|
346
|
+
{
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: { "Content-Type": "application/json" },
|
|
349
|
+
body: JSON.stringify({
|
|
350
|
+
content: content.trim(),
|
|
351
|
+
from: "web-ui",
|
|
352
|
+
}),
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (response.ok) {
|
|
357
|
+
replyInputEl.value = "";
|
|
358
|
+
await openThread(currentThread);
|
|
359
|
+
await loadMessages(currentChannel);
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error("Failed to send reply:", error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Connect to SSE stream for real-time updates
|
|
368
|
+
*/
|
|
369
|
+
function connectSSE() {
|
|
370
|
+
if (eventSource) {
|
|
371
|
+
eventSource.close();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
eventSource = new EventSource(`${API_BASE}/channels-stream`);
|
|
375
|
+
|
|
376
|
+
eventSource.onopen = () => {
|
|
377
|
+
connectionStatusEl.textContent = "Connected";
|
|
378
|
+
connectionStatusEl.className = "connection-status connected";
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
eventSource.onerror = () => {
|
|
382
|
+
connectionStatusEl.textContent = "Disconnected";
|
|
383
|
+
connectionStatusEl.className = "connection-status disconnected";
|
|
384
|
+
|
|
385
|
+
// Attempt reconnect after 5 seconds
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
if (eventSource.readyState === EventSource.CLOSED) {
|
|
388
|
+
connectSSE();
|
|
389
|
+
}
|
|
390
|
+
}, 5000);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
eventSource.onmessage = (event) => {
|
|
394
|
+
try {
|
|
395
|
+
const data = JSON.parse(event.data);
|
|
396
|
+
|
|
397
|
+
if (data.type === "connected") {
|
|
398
|
+
console.log("SSE connected");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Reload channel list on any channel activity
|
|
403
|
+
if (data.type === "dm:received" || data.type === "channel:message") {
|
|
404
|
+
// Refresh channel list
|
|
405
|
+
loadChannels();
|
|
406
|
+
|
|
407
|
+
// If we're viewing the affected channel, reload messages
|
|
408
|
+
if (currentChannel === data.channel) {
|
|
409
|
+
loadMessages(currentChannel);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error("Failed to parse SSE message:", error);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Event listeners
|
|
419
|
+
messageFormEl.addEventListener("submit", (e) => {
|
|
420
|
+
e.preventDefault();
|
|
421
|
+
sendMessage(messageInputEl.value);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
replyFormEl.addEventListener("submit", (e) => {
|
|
425
|
+
e.preventDefault();
|
|
426
|
+
sendReply(replyInputEl.value);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
closeThreadBtn.addEventListener("click", closeThread);
|
|
430
|
+
|
|
431
|
+
// Initialize
|
|
432
|
+
loadChannels();
|
|
433
|
+
connectSSE();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>dot-agents Channels</title>
|
|
7
|
+
<link rel="stylesheet" href="styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="app">
|
|
11
|
+
<aside class="sidebar">
|
|
12
|
+
<header class="sidebar-header">
|
|
13
|
+
<h1>dot-agents</h1>
|
|
14
|
+
<span class="connection-status" id="connection-status">Connecting...</span>
|
|
15
|
+
</header>
|
|
16
|
+
<nav class="channel-list" id="channel-list">
|
|
17
|
+
<div class="loading">Loading channels...</div>
|
|
18
|
+
</nav>
|
|
19
|
+
</aside>
|
|
20
|
+
|
|
21
|
+
<main class="content">
|
|
22
|
+
<header class="content-header" id="content-header">
|
|
23
|
+
<h2 id="channel-name">Select a channel</h2>
|
|
24
|
+
<span class="channel-description" id="channel-description"></span>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<div class="messages" id="messages">
|
|
28
|
+
<div class="empty-state">
|
|
29
|
+
<p>Select a channel from the sidebar to view messages</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<footer class="composer" id="composer" style="display: none;">
|
|
34
|
+
<form id="message-form">
|
|
35
|
+
<input
|
|
36
|
+
type="text"
|
|
37
|
+
id="message-input"
|
|
38
|
+
placeholder="Type a message..."
|
|
39
|
+
autocomplete="off"
|
|
40
|
+
>
|
|
41
|
+
<button type="submit">Send</button>
|
|
42
|
+
</form>
|
|
43
|
+
</footer>
|
|
44
|
+
</main>
|
|
45
|
+
|
|
46
|
+
<aside class="thread-panel" id="thread-panel" style="display: none;">
|
|
47
|
+
<header class="thread-header">
|
|
48
|
+
<h3>Thread</h3>
|
|
49
|
+
<button class="close-thread" id="close-thread">×</button>
|
|
50
|
+
</header>
|
|
51
|
+
<div class="thread-messages" id="thread-messages"></div>
|
|
52
|
+
<footer class="thread-composer">
|
|
53
|
+
<form id="reply-form">
|
|
54
|
+
<input
|
|
55
|
+
type="text"
|
|
56
|
+
id="reply-input"
|
|
57
|
+
placeholder="Reply in thread..."
|
|
58
|
+
autocomplete="off"
|
|
59
|
+
>
|
|
60
|
+
<button type="submit">Reply</button>
|
|
61
|
+
</form>
|
|
62
|
+
</footer>
|
|
63
|
+
</aside>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<script src="app.js"></script>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|