Spaces:
Sleeping
Sleeping
Chat history rename and delete are implemented
Browse files- assets/app.js +140 -10
- assets/server.css +62 -5
assets/app.js
CHANGED
|
@@ -185,15 +185,20 @@ function persistActiveSession() {
|
|
| 185 |
|
| 186 |
const sessions = loadSessions();
|
| 187 |
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
const payload = {
|
| 189 |
id: state.sessionId,
|
| 190 |
-
title
|
| 191 |
updatedAt: now,
|
| 192 |
history: state.history,
|
| 193 |
globeState: state.globeState,
|
| 194 |
};
|
| 195 |
|
| 196 |
-
const index = sessions.findIndex((session) => session.id === state.sessionId);
|
| 197 |
if (index >= 0) {
|
| 198 |
sessions[index] = { ...sessions[index], ...payload };
|
| 199 |
} else {
|
|
@@ -204,6 +209,97 @@ function persistActiveSession() {
|
|
| 204 |
saveSessions(sessions);
|
| 205 |
}
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
function openHistoryDialog() {
|
| 208 |
renderHistoryList();
|
| 209 |
els.historyDialog.hidden = false;
|
|
@@ -226,13 +322,16 @@ function renderHistoryList() {
|
|
| 226 |
}
|
| 227 |
|
| 228 |
for (const session of sessions) {
|
| 229 |
-
const
|
| 230 |
-
|
| 231 |
-
button.className = "history-item";
|
| 232 |
if (session.id === state.sessionId) {
|
| 233 |
-
|
| 234 |
}
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
const title = document.createElement("span");
|
| 237 |
title.className = "history-item-title";
|
| 238 |
title.textContent = session.title || "Untitled chat";
|
|
@@ -241,10 +340,41 @@ function renderHistoryList() {
|
|
| 241 |
meta.className = "history-item-meta";
|
| 242 |
meta.textContent = formatSessionDate(session.updatedAt || session.createdAt);
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
}
|
| 249 |
}
|
| 250 |
|
|
|
|
| 185 |
|
| 186 |
const sessions = loadSessions();
|
| 187 |
const now = Date.now();
|
| 188 |
+
const index = sessions.findIndex((session) => session.id === state.sessionId);
|
| 189 |
+
const existing = index >= 0 ? sessions[index] : null;
|
| 190 |
+
const title = existing?.titleManuallySet
|
| 191 |
+
? existing.title
|
| 192 |
+
: sessionTitle(state.history);
|
| 193 |
+
|
| 194 |
const payload = {
|
| 195 |
id: state.sessionId,
|
| 196 |
+
title,
|
| 197 |
updatedAt: now,
|
| 198 |
history: state.history,
|
| 199 |
globeState: state.globeState,
|
| 200 |
};
|
| 201 |
|
|
|
|
| 202 |
if (index >= 0) {
|
| 203 |
sessions[index] = { ...sessions[index], ...payload };
|
| 204 |
} else {
|
|
|
|
| 209 |
saveSessions(sessions);
|
| 210 |
}
|
| 211 |
|
| 212 |
+
function updateSessionTitle(sessionId, title) {
|
| 213 |
+
const trimmed = String(title || "").trim();
|
| 214 |
+
if (!trimmed) {
|
| 215 |
+
return false;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
const sessions = loadSessions();
|
| 219 |
+
const index = sessions.findIndex((session) => session.id === sessionId);
|
| 220 |
+
if (index < 0) {
|
| 221 |
+
return false;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
sessions[index] = {
|
| 225 |
+
...sessions[index],
|
| 226 |
+
title: trimmed,
|
| 227 |
+
titleManuallySet: true,
|
| 228 |
+
updatedAt: Date.now(),
|
| 229 |
+
};
|
| 230 |
+
sessions.sort((left, right) => right.updatedAt - left.updatedAt);
|
| 231 |
+
saveSessions(sessions);
|
| 232 |
+
|
| 233 |
+
if (!els.historyDialog.hidden) {
|
| 234 |
+
renderHistoryList();
|
| 235 |
+
}
|
| 236 |
+
return true;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
function deleteSession(sessionId) {
|
| 240 |
+
const sessions = loadSessions().filter((session) => session.id !== sessionId);
|
| 241 |
+
saveSessions(sessions);
|
| 242 |
+
|
| 243 |
+
if (sessionId === state.sessionId) {
|
| 244 |
+
state.sessionId = crypto.randomUUID();
|
| 245 |
+
state.history = [];
|
| 246 |
+
state.globeState = emptyGlobeState();
|
| 247 |
+
resetForm();
|
| 248 |
+
els.chatInput.value = "";
|
| 249 |
+
renderMessages();
|
| 250 |
+
applyGlobeState(state.globeState);
|
| 251 |
+
showFormView();
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
renderHistoryList();
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
function startSessionRename(session, titleElement) {
|
| 258 |
+
const previousTitle = session.title || "Untitled chat";
|
| 259 |
+
const input = document.createElement("input");
|
| 260 |
+
input.type = "text";
|
| 261 |
+
input.className = "history-item-rename-input";
|
| 262 |
+
input.value = previousTitle;
|
| 263 |
+
input.setAttribute("aria-label", "Chat title");
|
| 264 |
+
|
| 265 |
+
titleElement.replaceWith(input);
|
| 266 |
+
input.focus();
|
| 267 |
+
input.select();
|
| 268 |
+
|
| 269 |
+
let finished = false;
|
| 270 |
+
|
| 271 |
+
function finish(save) {
|
| 272 |
+
if (finished) {
|
| 273 |
+
return;
|
| 274 |
+
}
|
| 275 |
+
finished = true;
|
| 276 |
+
|
| 277 |
+
if (save) {
|
| 278 |
+
const newTitle = input.value.trim();
|
| 279 |
+
if (newTitle && newTitle !== previousTitle) {
|
| 280 |
+
if (updateSessionTitle(session.id, newTitle)) {
|
| 281 |
+
return;
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
renderHistoryList();
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
input.addEventListener("keydown", (event) => {
|
| 290 |
+
event.stopPropagation();
|
| 291 |
+
if (event.key === "Enter") {
|
| 292 |
+
event.preventDefault();
|
| 293 |
+
finish(true);
|
| 294 |
+
} else if (event.key === "Escape") {
|
| 295 |
+
event.preventDefault();
|
| 296 |
+
finish(false);
|
| 297 |
+
}
|
| 298 |
+
});
|
| 299 |
+
|
| 300 |
+
input.addEventListener("blur", () => finish(true));
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
function openHistoryDialog() {
|
| 304 |
renderHistoryList();
|
| 305 |
els.historyDialog.hidden = false;
|
|
|
|
| 322 |
}
|
| 323 |
|
| 324 |
for (const session of sessions) {
|
| 325 |
+
const row = document.createElement("div");
|
| 326 |
+
row.className = "history-item-row";
|
|
|
|
| 327 |
if (session.id === state.sessionId) {
|
| 328 |
+
row.classList.add("is-active");
|
| 329 |
}
|
| 330 |
|
| 331 |
+
const mainButton = document.createElement("button");
|
| 332 |
+
mainButton.type = "button";
|
| 333 |
+
mainButton.className = "history-item-main";
|
| 334 |
+
|
| 335 |
const title = document.createElement("span");
|
| 336 |
title.className = "history-item-title";
|
| 337 |
title.textContent = session.title || "Untitled chat";
|
|
|
|
| 340 |
meta.className = "history-item-meta";
|
| 341 |
meta.textContent = formatSessionDate(session.updatedAt || session.createdAt);
|
| 342 |
|
| 343 |
+
mainButton.appendChild(title);
|
| 344 |
+
mainButton.appendChild(meta);
|
| 345 |
+
mainButton.addEventListener("click", () => loadSession(session.id));
|
| 346 |
+
|
| 347 |
+
const actions = document.createElement("div");
|
| 348 |
+
actions.className = "history-item-actions";
|
| 349 |
+
|
| 350 |
+
const renameButton = document.createElement("button");
|
| 351 |
+
renameButton.type = "button";
|
| 352 |
+
renameButton.className = "history-item-action";
|
| 353 |
+
renameButton.textContent = "Rename";
|
| 354 |
+
renameButton.setAttribute("aria-label", "Rename chat");
|
| 355 |
+
renameButton.addEventListener("click", (event) => {
|
| 356 |
+
event.stopPropagation();
|
| 357 |
+
startSessionRename(session, title);
|
| 358 |
+
});
|
| 359 |
+
|
| 360 |
+
const deleteButton = document.createElement("button");
|
| 361 |
+
deleteButton.type = "button";
|
| 362 |
+
deleteButton.className = "history-item-action history-item-action-delete";
|
| 363 |
+
deleteButton.textContent = "Delete";
|
| 364 |
+
deleteButton.setAttribute("aria-label", "Delete chat");
|
| 365 |
+
deleteButton.addEventListener("click", (event) => {
|
| 366 |
+
event.stopPropagation();
|
| 367 |
+
if (confirm("Delete this chat? This cannot be undone.")) {
|
| 368 |
+
deleteSession(session.id);
|
| 369 |
+
}
|
| 370 |
+
});
|
| 371 |
+
|
| 372 |
+
actions.appendChild(renameButton);
|
| 373 |
+
actions.appendChild(deleteButton);
|
| 374 |
+
|
| 375 |
+
row.appendChild(mainButton);
|
| 376 |
+
row.appendChild(actions);
|
| 377 |
+
els.historyList.appendChild(row);
|
| 378 |
}
|
| 379 |
}
|
| 380 |
|
assets/server.css
CHANGED
|
@@ -697,24 +697,69 @@ button:disabled {
|
|
| 697 |
font-size: 0.9rem;
|
| 698 |
}
|
| 699 |
|
| 700 |
-
.history-item {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
display: flex;
|
| 702 |
flex-direction: column;
|
| 703 |
align-items: flex-start;
|
| 704 |
gap: 4px;
|
| 705 |
-
|
|
|
|
| 706 |
text-align: left;
|
| 707 |
padding: 10px 12px;
|
|
|
|
|
|
|
|
|
|
| 708 |
}
|
| 709 |
|
| 710 |
-
.history-item
|
| 711 |
-
|
| 712 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
}
|
| 714 |
|
| 715 |
.history-item-title {
|
| 716 |
font-weight: 600;
|
| 717 |
color: #f9fafb;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
}
|
| 719 |
|
| 720 |
.history-item-meta {
|
|
@@ -722,6 +767,18 @@ button:disabled {
|
|
| 722 |
color: #9ca3af;
|
| 723 |
}
|
| 724 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
@media (max-width: 900px) {
|
| 726 |
.app-shell {
|
| 727 |
height: auto;
|
|
|
|
| 697 |
font-size: 0.9rem;
|
| 698 |
}
|
| 699 |
|
| 700 |
+
.history-item-row {
|
| 701 |
+
display: flex;
|
| 702 |
+
align-items: stretch;
|
| 703 |
+
gap: 8px;
|
| 704 |
+
border-radius: 8px;
|
| 705 |
+
border: 1px solid rgba(148, 163, 184, 0.25);
|
| 706 |
+
background: rgba(17, 24, 39, 0.9);
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
.history-item-row.is-active {
|
| 710 |
+
border-color: #f59e0b;
|
| 711 |
+
background: rgba(245, 158, 11, 0.08);
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
.history-item-main {
|
| 715 |
display: flex;
|
| 716 |
flex-direction: column;
|
| 717 |
align-items: flex-start;
|
| 718 |
gap: 4px;
|
| 719 |
+
flex: 1;
|
| 720 |
+
min-width: 0;
|
| 721 |
text-align: left;
|
| 722 |
padding: 10px 12px;
|
| 723 |
+
border: none;
|
| 724 |
+
background: transparent;
|
| 725 |
+
color: inherit;
|
| 726 |
}
|
| 727 |
|
| 728 |
+
.history-item-main:hover {
|
| 729 |
+
background: rgba(148, 163, 184, 0.08);
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
.history-item-actions {
|
| 733 |
+
display: flex;
|
| 734 |
+
flex-direction: column;
|
| 735 |
+
justify-content: center;
|
| 736 |
+
gap: 4px;
|
| 737 |
+
flex-shrink: 0;
|
| 738 |
+
padding: 6px 8px 6px 0;
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.history-item-action {
|
| 742 |
+
font-size: 0.75rem;
|
| 743 |
+
padding: 4px 8px;
|
| 744 |
+
white-space: nowrap;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.history-item-action-delete {
|
| 748 |
+
color: #fca5a5;
|
| 749 |
+
border-color: rgba(248, 113, 113, 0.35);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
.history-item-action-delete:hover {
|
| 753 |
+
background: rgba(248, 113, 113, 0.12);
|
| 754 |
}
|
| 755 |
|
| 756 |
.history-item-title {
|
| 757 |
font-weight: 600;
|
| 758 |
color: #f9fafb;
|
| 759 |
+
width: 100%;
|
| 760 |
+
overflow: hidden;
|
| 761 |
+
text-overflow: ellipsis;
|
| 762 |
+
white-space: nowrap;
|
| 763 |
}
|
| 764 |
|
| 765 |
.history-item-meta {
|
|
|
|
| 767 |
color: #9ca3af;
|
| 768 |
}
|
| 769 |
|
| 770 |
+
.history-item-rename-input {
|
| 771 |
+
width: 100%;
|
| 772 |
+
font-weight: 600;
|
| 773 |
+
font-size: inherit;
|
| 774 |
+
font-family: inherit;
|
| 775 |
+
color: #f9fafb;
|
| 776 |
+
background: rgba(2, 6, 23, 0.6);
|
| 777 |
+
border: 1px solid rgba(245, 158, 11, 0.5);
|
| 778 |
+
border-radius: 4px;
|
| 779 |
+
padding: 2px 6px;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
@media (max-width: 900px) {
|
| 783 |
.app-shell {
|
| 784 |
height: auto;
|