mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-17 04:01:13 +00:00
Import the chat input history navigation from neurocis PR #1573 while keeping the change scoped to the chat input files. Adjust Down navigation so it only triggers when the caret is collapsed at end-of-text, preserving normal textarea movement from the start of multiline input.
406 lines
12 KiB
JavaScript
406 lines
12 KiB
JavaScript
import { createStore } from "/js/AlpineStore.js";
|
|
import * as shortcuts from "/js/shortcuts.js";
|
|
import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
|
|
import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
|
|
import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
|
|
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
|
|
|
|
const model = {
|
|
paused: false,
|
|
message: "",
|
|
_history: [],
|
|
_historyIndex: null,
|
|
_draft: "",
|
|
_historyCtxid: null,
|
|
/** Composer + menu (bottom actions moved into dropdown) */
|
|
chatMoreMenuOpen: false,
|
|
progressText: "",
|
|
progressActive: false,
|
|
|
|
toggleChatMoreMenu() {
|
|
this.chatMoreMenuOpen = !this.chatMoreMenuOpen;
|
|
},
|
|
|
|
closeChatMoreMenu() {
|
|
this.chatMoreMenuOpen = false;
|
|
},
|
|
|
|
_getSendState() {
|
|
const hasInput = this.message.trim() || attachmentsStore?.attachments?.length > 0;
|
|
const hasQueue = !!messageQueueStore?.hasQueue;
|
|
const running = !!chatsStore.selectedContext?.running;
|
|
|
|
if (hasQueue && !hasInput) return "all";
|
|
if ((running || hasQueue) && hasInput) return "queue";
|
|
return "normal";
|
|
},
|
|
|
|
get inputPlaceholder() {
|
|
const state = this._getSendState();
|
|
if (state === "all") return "Press Enter to send queued messages";
|
|
// Show progress as ghost text when agent is working and input is empty
|
|
if (this.progressText && !this.message) {
|
|
return "|> " + this.progressText;
|
|
}
|
|
return "Type your message here...";
|
|
},
|
|
|
|
// Computed: send button icon type
|
|
get sendButtonIcon() {
|
|
const state = this._getSendState();
|
|
if (state === "all") return "send_and_archive";
|
|
if (state === "queue") return "schedule_send";
|
|
return "send";
|
|
},
|
|
|
|
// Computed: send button CSS class
|
|
get sendButtonClass() {
|
|
const state = this._getSendState();
|
|
if (state === "all") return "send-queue send-all";
|
|
if (state === "queue") return "send-queue queue";
|
|
return "";
|
|
},
|
|
|
|
// Computed: send button title
|
|
get sendButtonTitle() {
|
|
const state = this._getSendState();
|
|
if (state === "all") return "Send all queued messages";
|
|
if (state === "queue") return "Add to queue";
|
|
return "Send message";
|
|
},
|
|
|
|
init() {
|
|
console.log("Input store initialized");
|
|
// Event listeners are now handled via Alpine directives in the component
|
|
},
|
|
|
|
async sendMessage() {
|
|
// Capture sent prompt to per-chat history (bash-style)
|
|
try { this._pushHistory(this.message); } catch (_e) { /* ignore */ }
|
|
// Delegate to the global function
|
|
if (globalThis.sendMessage) {
|
|
await globalThis.sendMessage();
|
|
}
|
|
},
|
|
|
|
adjustTextareaHeight() {
|
|
const chatInput = document.getElementById("chat-input");
|
|
if (chatInput) {
|
|
if (!this.message) chatInput.value = "";
|
|
chatInput.style.height = "auto";
|
|
chatInput.style.height = chatInput.scrollHeight + "px";
|
|
// pick up any layout shift triggered by the height assignment
|
|
chatInput.style.height = Math.max(chatInput.scrollHeight, parseInt(chatInput.style.height)) + "px";
|
|
}
|
|
},
|
|
|
|
async pauseAgent(paused) {
|
|
const prev = this.paused;
|
|
this.paused = paused;
|
|
try {
|
|
const context = globalThis.getContext?.();
|
|
if (!globalThis.sendJsonData)
|
|
throw new Error("sendJsonData not available");
|
|
await globalThis.sendJsonData("/pause", { paused, context });
|
|
} catch (e) {
|
|
this.paused = prev;
|
|
if (globalThis.toastFetchError) {
|
|
globalThis.toastFetchError("Error pausing agent", e);
|
|
}
|
|
}
|
|
},
|
|
|
|
async nudge() {
|
|
try {
|
|
const context = globalThis.getContext();
|
|
await globalThis.sendJsonData("/nudge", { ctxid: context });
|
|
} catch (e) {
|
|
if (globalThis.toastFetchError) {
|
|
globalThis.toastFetchError("Error nudging agent", e);
|
|
}
|
|
}
|
|
},
|
|
|
|
async loadKnowledge() {
|
|
try {
|
|
const resp = await shortcuts.callJsonApi(
|
|
"/plugins/_memory/knowledge_path_get",
|
|
{ ctxid: shortcuts.getCurrentContextId() }
|
|
);
|
|
if (!resp.ok) throw new Error("Error getting knowledge path");
|
|
const path = resp.path;
|
|
|
|
// open file browser and wait for it to close
|
|
await fileBrowserStore.open(path);
|
|
|
|
// progress notification
|
|
shortcuts.frontendNotification({
|
|
type: shortcuts.NotificationType.PROGRESS,
|
|
message: "Loading knowledge...",
|
|
priority: shortcuts.NotificationPriority.NORMAL,
|
|
displayTime: 999,
|
|
group: "knowledge_load",
|
|
frontendOnly: true,
|
|
});
|
|
|
|
// then reindex knowledge
|
|
await globalThis.sendJsonData("/plugins/_memory/knowledge_reindex", {
|
|
ctxid: shortcuts.getCurrentContextId(),
|
|
});
|
|
|
|
// finished notification
|
|
shortcuts.frontendNotification({
|
|
type: shortcuts.NotificationType.SUCCESS,
|
|
message: "Knowledge loaded successfully",
|
|
priority: shortcuts.NotificationPriority.NORMAL,
|
|
displayTime: 2,
|
|
group: "knowledge_load",
|
|
frontendOnly: true,
|
|
});
|
|
} catch (e) {
|
|
// error notification
|
|
shortcuts.frontendNotification({
|
|
type: shortcuts.NotificationType.ERROR,
|
|
message: "Error loading knowledge",
|
|
priority: shortcuts.NotificationPriority.NORMAL,
|
|
displayTime: 5,
|
|
group: "knowledge_load",
|
|
frontendOnly: true,
|
|
});
|
|
}
|
|
},
|
|
|
|
// previous implementation without projects
|
|
async _loadKnowledge() {
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = ".txt,.pdf,.csv,.html,.json,.md";
|
|
input.multiple = true;
|
|
|
|
input.onchange = async () => {
|
|
try {
|
|
const formData = new FormData();
|
|
for (let file of input.files) {
|
|
formData.append("files[]", file);
|
|
}
|
|
|
|
formData.append("ctxid", globalThis.getContext());
|
|
|
|
const response = await globalThis.fetchApi("/import_knowledge", {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (globalThis.toast)
|
|
globalThis.toast(await response.text(), "error");
|
|
} else {
|
|
const data = await response.json();
|
|
if (globalThis.toast) {
|
|
globalThis.toast(
|
|
"Knowledge files imported: " + data.filenames.join(", "),
|
|
"success"
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (globalThis.toastFetchError) {
|
|
globalThis.toastFetchError("Error loading knowledge", e);
|
|
}
|
|
}
|
|
};
|
|
|
|
input.click();
|
|
},
|
|
|
|
async browseFiles(path) {
|
|
if (!path) {
|
|
const ctxid = shortcuts.getCurrentContextId();
|
|
|
|
if (ctxid) {
|
|
try {
|
|
const resp = await shortcuts.callJsonApi("/chat_files_path_get", {
|
|
ctxid,
|
|
});
|
|
if (resp.ok) path = resp.path;
|
|
} catch (_e) {
|
|
console.error("Error getting chat files path", _e);
|
|
}
|
|
}
|
|
}
|
|
await fileBrowserStore.open(path);
|
|
},
|
|
|
|
focus() {
|
|
const chatInput = document.getElementById("chat-input");
|
|
if (chatInput) {
|
|
chatInput.focus();
|
|
}
|
|
},
|
|
|
|
_loadHistory() {
|
|
let ctxid = null;
|
|
try { ctxid = shortcuts.getCurrentContextId(); } catch (_e) { ctxid = null; }
|
|
this._historyCtxid = ctxid;
|
|
this._history = [];
|
|
this._historyIndex = null;
|
|
this._draft = "";
|
|
if (!ctxid) return;
|
|
let raw = null;
|
|
try { raw = localStorage.getItem("a0:chat-history:" + ctxid); } catch (_e) { raw = null; }
|
|
if (raw !== null) {
|
|
try {
|
|
const arr = JSON.parse(raw);
|
|
if (Array.isArray(arr)) {
|
|
this._history = arr.filter((s) => typeof s === "string");
|
|
}
|
|
} catch (_e) { /* ignore */ }
|
|
return;
|
|
}
|
|
// No entry yet for this chat: seed from rendered chat DOM (one-time bootstrap)
|
|
try {
|
|
const seeded = this._seedFromChatDom();
|
|
if (seeded.length > 0) {
|
|
this._history = seeded;
|
|
this._saveHistory();
|
|
} else {
|
|
// Persist an empty array so we don't re-seed on every nav; respects user clearing
|
|
this._saveHistory();
|
|
}
|
|
} catch (_e) { /* ignore */ }
|
|
},
|
|
|
|
_seedFromChatDom() {
|
|
const out = [];
|
|
let nodes;
|
|
try {
|
|
nodes = document.querySelectorAll(".user-container .message-user .message-text pre");
|
|
} catch (_e) {
|
|
return out;
|
|
}
|
|
for (const pre of nodes) {
|
|
const text = (pre.textContent || "").trim();
|
|
if (!text) continue;
|
|
if (out.length > 0 && out[out.length - 1] === text) continue; // ignoredups
|
|
out.push(text);
|
|
}
|
|
if (out.length > 50) return out.slice(-50);
|
|
return out;
|
|
},
|
|
|
|
_saveHistory() {
|
|
if (!this._historyCtxid) return;
|
|
try {
|
|
localStorage.setItem(
|
|
"a0:chat-history:" + this._historyCtxid,
|
|
JSON.stringify(this._history)
|
|
);
|
|
} catch (_e) { /* ignore quota / disabled */ }
|
|
},
|
|
|
|
_ensureHistoryLoaded() {
|
|
let ctxid = null;
|
|
try { ctxid = shortcuts.getCurrentContextId(); } catch (_e) { ctxid = null; }
|
|
if (ctxid !== this._historyCtxid) {
|
|
this._loadHistory();
|
|
}
|
|
},
|
|
|
|
_pushHistory(text) {
|
|
if (typeof text !== "string") return;
|
|
const trimmed = text.trim();
|
|
if (!trimmed) return;
|
|
this._ensureHistoryLoaded();
|
|
if (this._history.length > 0 && this._history[this._history.length - 1] === trimmed) {
|
|
this._historyIndex = null;
|
|
this._draft = "";
|
|
return;
|
|
}
|
|
this._history.push(trimmed);
|
|
if (this._history.length > 50) {
|
|
this._history = this._history.slice(-50);
|
|
}
|
|
this._saveHistory();
|
|
this._historyIndex = null;
|
|
this._draft = "";
|
|
},
|
|
|
|
_setCaretStart() {
|
|
queueMicrotask(() => {
|
|
const ta = document.getElementById("chat-input");
|
|
if (ta) {
|
|
try { ta.setSelectionRange(0, 0); } catch (_e) { /* ignore */ }
|
|
try { ta.scrollTop = 0; } catch (_e) { /* ignore */ }
|
|
}
|
|
this.adjustTextareaHeight();
|
|
});
|
|
},
|
|
|
|
_setCaretEnd() {
|
|
queueMicrotask(() => {
|
|
const ta = document.getElementById("chat-input");
|
|
if (ta) {
|
|
const end = ta.value.length;
|
|
try { ta.setSelectionRange(end, end); } catch (_e) { /* ignore */ }
|
|
try { ta.scrollTop = ta.scrollHeight; } catch (_e) { /* ignore */ }
|
|
}
|
|
this.adjustTextareaHeight();
|
|
});
|
|
},
|
|
|
|
historyPrev($event) {
|
|
if ($event && ($event.isComposing || $event.keyCode === 229)) return;
|
|
const ta = ($event && $event.target) ? $event.target : document.getElementById("chat-input");
|
|
if (!ta) return;
|
|
const start = ta.selectionStart;
|
|
const end = ta.selectionEnd;
|
|
if (start !== 0 || end !== 0) return;
|
|
$event.preventDefault();
|
|
this._ensureHistoryLoaded();
|
|
if (this._history.length === 0) return;
|
|
if (this._historyIndex === null) {
|
|
this._draft = this.message || "";
|
|
this._historyIndex = this._history.length - 1;
|
|
} else if (this._historyIndex > 0) {
|
|
this._historyIndex -= 1;
|
|
} else {
|
|
return;
|
|
}
|
|
this.message = this._history[this._historyIndex];
|
|
this._setCaretStart();
|
|
},
|
|
|
|
historyNext($event) {
|
|
if ($event && ($event.isComposing || $event.keyCode === 229)) return;
|
|
const ta = ($event && $event.target) ? $event.target : document.getElementById("chat-input");
|
|
if (!ta) return;
|
|
const value = ta.value;
|
|
const start = ta.selectionStart;
|
|
const end = ta.selectionEnd;
|
|
if (start !== value.length || end !== value.length) return;
|
|
$event.preventDefault();
|
|
if (this._historyIndex === null) return;
|
|
if (this._historyIndex < this._history.length - 1) {
|
|
this._historyIndex += 1;
|
|
this.message = this._history[this._historyIndex];
|
|
} else {
|
|
this._historyIndex = null;
|
|
this.message = this._draft || "";
|
|
this._draft = "";
|
|
}
|
|
this._setCaretEnd();
|
|
},
|
|
|
|
reset() {
|
|
this.message = "";
|
|
attachmentsStore.clearAttachments();
|
|
this.chatMoreMenuOpen = false;
|
|
this._historyIndex = null;
|
|
this._draft = "";
|
|
this.adjustTextareaHeight();
|
|
}
|
|
};
|
|
|
|
const store = createStore("chatInput", model);
|
|
|
|
export { store };
|