add more features

This commit is contained in:
2026-04-10 00:34:05 +02:00
parent 6e2aa2b353
commit 67d8693654
6 changed files with 792 additions and 64 deletions
-1
View File
@@ -216,7 +216,6 @@
<div class="input-shell input-shell-focus">
<div class="input-meta">
<p class="composer-hint">Enter to send · Shift+Enter for new line</p>
<p id="appNotice" class="status inline-status hidden" aria-live="polite"></p>
</div>
<div id="quickPrompts" class="quick-prompts" aria-label="Suggested starters"></div>
<div class="input-container">
+4
View File
@@ -108,6 +108,10 @@
<span>You in this roleplay</span>
<select id="marketPreviewUserPersonaSelect"></select>
</label>
<label id="marketPreviewScenarioField" class="field market-preview-scenario-field hidden">
<span>Scenario (optional)</span>
<textarea id="marketPreviewScenario" rows="3" maxlength="240" placeholder="Set the opening scene you want the AI character to start with."></textarea>
</label>
<button id="marketPreviewConfirm" type="button" class="primary-action">Take persona</button>
</div>
</div>
+53 -8
View File
@@ -18,6 +18,8 @@ const marketPreviewDetails = $("marketPreviewDetails");
const marketPreviewExampleDialogues = $("marketPreviewExampleDialogues");
const marketPreviewUserPersonaField = $("marketPreviewUserPersonaField");
const marketPreviewUserPersonaSelect = $("marketPreviewUserPersonaSelect");
const marketPreviewScenarioField = $("marketPreviewScenarioField");
const marketPreviewScenario = $("marketPreviewScenario");
const marketPreviewConfirm = $("marketPreviewConfirm");
const marketPreviewClose = $("marketPreviewClose");
const marketPersonaModal = $("marketPersonaModal");
@@ -112,10 +114,45 @@ function formatPersonaField(value) {
return value && value.trim() ? value.trim() : "Not provided.";
}
function setCollapsiblePreviewText(element, value, maxLength = 220) {
const text = formatPersonaField(value);
element.dataset.fullText = text;
let toggle = element.nextElementSibling;
if (!toggle || !toggle.classList.contains("market-preview-toggle")) {
toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "market-preview-toggle hidden";
element.insertAdjacentElement("afterend", toggle);
toggle.addEventListener("click", () => {
const isExpanded = element.dataset.expanded === "true";
element.dataset.expanded = isExpanded ? "false" : "true";
if (element.dataset.expanded === "true") {
element.textContent = element.dataset.fullText;
toggle.textContent = "Show less";
} else {
element.textContent = `${element.dataset.fullText.slice(0, maxLength).trimEnd()}...`;
toggle.textContent = "Show more";
}
});
}
const shouldCollapse = text !== "Not provided." && text.length > maxLength;
element.dataset.expanded = "false";
if (shouldCollapse) {
element.textContent = `${text.slice(0, maxLength).trimEnd()}...`;
toggle.textContent = "Show more";
toggle.classList.remove("hidden");
} else {
element.textContent = text;
toggle.classList.add("hidden");
}
}
function closeMarketPreview() {
marketPreviewModal.classList.add("hidden");
pendingMarketPersona = null;
marketPreviewUserPersonaSelect.innerHTML = "";
marketPreviewScenario.value = "";
}
function populateUserPersonaChoices() {
@@ -151,14 +188,15 @@ function openMarketPreview(persona) {
pendingMarketPersona = persona;
marketPreviewName.textContent = persona.name;
marketPreviewMeta.textContent = buildPersonaMeta(persona);
marketPreviewPronouns.textContent = formatPersonaField(persona.pronouns);
marketPreviewAppearance.textContent = formatPersonaField(persona.appearance);
marketPreviewBackground.textContent = formatPersonaField(persona.background);
marketPreviewDetails.textContent = formatPersonaField(persona.details);
marketPreviewExampleDialogues.textContent = formatPersonaField(persona.example_dialogues);
setCollapsiblePreviewText(marketPreviewPronouns, persona.pronouns, 80);
setCollapsiblePreviewText(marketPreviewAppearance, persona.appearance);
setCollapsiblePreviewText(marketPreviewBackground, persona.background);
setCollapsiblePreviewText(marketPreviewDetails, persona.details);
setCollapsiblePreviewText(marketPreviewExampleDialogues, persona.example_dialogues);
const isAssistant = persona.persona_type === "assistant";
marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant);
marketPreviewScenarioField.classList.toggle("hidden", !isAssistant);
marketPreviewConfirm.textContent = isAssistant ? "Start roleplay" : "Collect persona";
if (isAssistant) {
populateUserPersonaChoices();
@@ -303,7 +341,7 @@ async function collectMarketPersona(marketId) {
setMarketStatus("User Persona collected.", "success");
}
async function startMarketPersonaChat(marketId, userPersonaSelection) {
async function startMarketPersonaChat(marketId, userPersonaSelection, scenarioPrompt = "") {
if (!userPersonaSelection) {
setMarketStatus("Choose who you are before starting roleplay.", "error");
return;
@@ -315,7 +353,10 @@ async function startMarketPersonaChat(marketId, userPersonaSelection) {
setMarketStatus("Starting AI Character chat...");
const userPersonaId = userPersonaSelection === "self" ? null : Number(userPersonaSelection);
const res = await post(`/personas/market/${marketId}/chat`, {userPersonaId});
const res = await post(`/personas/market/${marketId}/chat`, {
userPersonaId,
scenarioPrompt: scenarioPrompt.trim()
});
if (res.error || !res.chat) {
setMarketStatus(res.error || "Unable to start chat.", "error");
return;
@@ -385,7 +426,11 @@ marketPreviewConfirm.addEventListener("click", async () => {
if (!pendingMarketPersona) return;
const marketId = pendingMarketPersona.id;
if (pendingMarketPersona.persona_type === "assistant") {
await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "self");
await startMarketPersonaChat(
marketId,
marketPreviewUserPersonaSelect.value || "self",
marketPreviewScenario.value || ""
);
closeMarketPreview();
return;
}
+322 -23
View File
@@ -10,6 +10,7 @@ const registerUsernameInput = $("registerUsername"), registerPasswordInput = $("
const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
const startRoleplayBtn = $("startRoleplay");
const chatListLoading = $("chatListLoading"), chatListLoadingText = $("chatListLoadingText");
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
const contextChipButton = $("contextChipButton"), contextPopover = $("contextPopover"), contextSummaryLabel = $("contextSummaryLabel");
@@ -46,6 +47,8 @@ let roleplayPresetCharacterId = null;
let noticeTimer = null;
let workspaceMode = localStorage.getItem("krishd-workspace-mode") || "basic";
let onboardingStep = 1, onboardingIntent = "ask";
let chatLoadingDepth = 0;
let messageRetryState = new Map();
const onboardingPrompts = {
ask: ["Explain this topic in simple terms.", "Compare these options and recommend one."],
@@ -113,6 +116,21 @@ function setPersonaFormNotice(message = "", state = "") {
personaFormNotice.className = state ? `status ${state}` : "status";
}
function setChatLoading(active, message = "Loading chats...") {
if (!chatListLoading || !chatListLoadingText) return;
if (active) {
chatLoadingDepth += 1;
chatListLoadingText.textContent = message;
chatListLoading.classList.remove("hidden");
return;
}
chatLoadingDepth = Math.max(0, chatLoadingDepth - 1);
if (chatLoadingDepth === 0) {
chatListLoading.classList.add("hidden");
chatListLoadingText.textContent = "Loading chats...";
}
}
function applyWorkspaceMode(nextMode, persist = true) {
workspaceMode = nextMode === "advanced" ? "advanced" : "basic";
document.body.dataset.workspaceMode = workspaceMode;
@@ -245,16 +263,6 @@ function updateRoleplaySuggestions() {
});
}
function getQuickPromptsForChat(chat) {
if (chat?.assistant_persona_id) {
const characterName = chat.assistant_persona_name || chat.title || "the character";
const userName = chat.user_persona_name || "me";
return [
`Stay in character and describe what ${characterName} notices about ${userName} right now.`,
`Push the scene forward with a new complication.`,
`Respond with sharper tension and more specific detail.`,
`Let ${characterName} reveal what they actually want from me.`
];
}
return [];
}
function renderQuickPrompts() {
@@ -295,7 +303,195 @@ function updateSendState() {
sendBtn.disabled = isProcessing || !hasText;
}
function addMessage(content, isUser = false, isLoading = false) {
function getNewestMessage() {
return currentMessages.length ? currentMessages[currentMessages.length - 1] : null;
}
function getMessageIndexById(messageId) {
return currentMessages.findIndex((message) => message.id === messageId);
}
async function resolveMessageTarget(messageId, fallbackIndex = -1) {
if (messageId) {
const index = getMessageIndexById(messageId);
if (index >= 0) {
return {index, message: currentMessages[index]};
}
}
if (activeChatId) {
await setActiveChat(activeChatId);
}
if (messageId) {
const index = getMessageIndexById(messageId);
if (index >= 0) {
return {index, message: currentMessages[index]};
}
}
if (fallbackIndex >= 0 && fallbackIndex < currentMessages.length) {
return {index: fallbackIndex, message: currentMessages[fallbackIndex]};
}
return null;
}
function getMessageEndpoint(target, suffix = "") {
if (!activeChatId || !target) return "";
if (target.message?.id) return `/chats/${activeChatId}/messages/${target.message.id}${suffix}`;
if (target.index >= 0) return `/chats/${activeChatId}/messages/by-index/${target.index}${suffix}`;
return "";
}
function getPreviousUserMessageId(messageId) {
const index = getMessageIndexById(messageId);
if (index <= 0) return null;
for (let i = index - 1; i >= 0; i -= 1) {
if (currentMessages[i].role === "user" && currentMessages[i].id) {
return currentMessages[i].id;
}
}
return null;
}
function ensureRetryState(message) {
if (!message?.id) return null;
let state = messageRetryState.get(message.id);
if (!state) {
state = {
variants: [message.content || ""],
activeIndex: 0,
retriesUsed: 0,
promptMessageId: getPreviousUserMessageId(message.id)
};
messageRetryState.set(message.id, state);
}
return state;
}
async function editChatMessage(messageId, fallbackIndex = -1) {
const target = await resolveMessageTarget(messageId, fallbackIndex);
if (!target) return setNotice("Message is not ready yet. Try again.", "error");
const {index, message} = target;
const nextContent = await promptPopup({
eyebrow: message.role === "user" ? "Your message" : "Assistant message",
title: "Edit message",
description: "Update this message.",
label: "Message",
value: message.content,
confirmLabel: "Save"
});
const content = (nextContent || "").trim();
if (!content) return;
const endpoint = getMessageEndpoint(target);
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
const res = await put(endpoint, {content});
if (res.error || !res.message) {
return setNotice(res.error || "Unable to edit message.", "error");
}
currentMessages[index].id = res.message.id || currentMessages[index].id;
currentMessages[index].content = res.message.content;
const retryState = message.id ? messageRetryState.get(message.id) : null;
if (retryState) {
retryState.variants[retryState.activeIndex] = res.message.content;
}
renderMessages();
setNotice("Message updated.", "success");
}
async function deleteChatMessage(messageId, fallbackIndex = -1) {
const target = await resolveMessageTarget(messageId, fallbackIndex);
if (!target) return setNotice("Message is not ready yet. Try again.", "error");
const {index, message} = target;
const confirmed = await confirmPopup({
eyebrow: message.role === "user" ? "Your message" : "Assistant message",
title: "Delete message",
description: "Delete this message from the chat?",
confirmLabel: "Delete",
danger: true
});
if (!confirmed) return;
const endpoint = getMessageEndpoint(target);
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
const res = await del(endpoint);
if (res.error) return setNotice(res.error, "error");
currentMessages = currentMessages.filter((item) => item.id !== message.id);
if (message.id) {
messageRetryState.delete(message.id);
for (const [key, state] of messageRetryState.entries()) {
if (state.promptMessageId === message.id) {
messageRetryState.delete(key);
}
}
}
renderMessages();
await loadSummary();
setNotice("Message deleted.", "success");
}
async function retryLatestAssistantMessage(messageId, fallbackIndex = -1) {
const target = await resolveMessageTarget(messageId, fallbackIndex);
if (!target) return setNotice("Message is not ready yet. Try again.", "error");
const newest = getNewestMessage();
const sameNewest = target.index === currentMessages.length - 1;
if (!newest || !sameNewest || newest.role !== "bot") {
return setNotice("Only the newest assistant message can be retried.", "error");
}
const state = ensureRetryState(newest) || {variants: [newest.content || ""], activeIndex: 0, retriesUsed: 0, promptMessageId: null};
if (!state || state.retriesUsed >= 5) {
return setNotice("Retry limit reached (5).", "error");
}
setLoadingState(true);
setNotice("Regenerating reply...");
try {
const endpoint = getMessageEndpoint(target, "/retry");
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
const res = await post(endpoint, {model: modelSelect.value});
if (res.error || !res.message?.content) {
return setNotice(res.error || "Unable to retry message.", "error");
}
state.retriesUsed += 1;
state.promptMessageId = res.promptMessageId || state.promptMessageId;
state.variants.push(res.message.content);
state.activeIndex = state.variants.length - 1;
const updateIndex = target.message?.id ? getMessageIndexById(target.message.id) : target.index;
if (updateIndex >= 0) {
currentMessages[updateIndex].id = res.message.id || currentMessages[updateIndex].id;
currentMessages[updateIndex].content = res.message.content;
}
renderMessages();
setNotice(`Reply regenerated (${state.retriesUsed}/5).`, "success");
} finally {
setLoadingState(false);
}
}
async function switchRetryVariant(messageId, direction, fallbackIndex = -1) {
const target = await resolveMessageTarget(messageId, fallbackIndex);
if (!target) return setNotice("Message is not ready yet. Try again.", "error");
const newest = getNewestMessage();
const sameNewest = target.index === currentMessages.length - 1;
if (!newest || !sameNewest || newest.role !== "bot") {
return setNotice("Variants can only be switched on the newest assistant message.", "error");
}
const state = target.message?.id ? messageRetryState.get(target.message.id) : null;
if (!state || state.variants.length < 2) return;
const nextIndex = state.activeIndex + direction;
if (nextIndex < 0 || nextIndex >= state.variants.length) return;
const targetContent = state.variants[nextIndex];
const endpoint = getMessageEndpoint(target);
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
const res = await put(endpoint, {content: targetContent});
if (res.error || !res.message) {
return setNotice(res.error || "Unable to switch variant.", "error");
}
state.activeIndex = nextIndex;
const updateIndex = target.message?.id ? getMessageIndexById(target.message.id) : target.index;
if (updateIndex >= 0) {
currentMessages[updateIndex].id = res.message.id || currentMessages[updateIndex].id;
currentMessages[updateIndex].content = res.message.content;
}
renderMessages();
}
function addMessage(contentOrMessage, isUser = false, isLoading = false, options = {}) {
const msgDiv = document.createElement("div");
msgDiv.className = `msg ${isUser ? "user" : "bot"}${isLoading ? " loading" : ""}`;
if (isLoading) {
@@ -312,12 +508,78 @@ function addMessage(content, isUser = false, isLoading = false) {
</div>
</div>`;
} else {
const header = document.createElement("div"), body = document.createElement("div");
const message = typeof contentOrMessage === "object" && contentOrMessage
? contentOrMessage
: {id: null, role: isUser ? "user" : "bot", content: String(contentOrMessage || "")};
const isMessageUser = message.role === "user";
msgDiv.className = `msg ${isMessageUser ? "user" : "bot"}`;
const header = document.createElement("div"), body = document.createElement("div"), actions = document.createElement("div");
header.className = "msg-header"; body.className = "msg-content";
actions.className = "msg-actions";
const activeChat = getChatById(activeChatId);
header.textContent = isUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
setMessageContent(body, content);
header.textContent = isMessageUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
setMessageContent(body, message.content);
const editBtn = document.createElement("button");
editBtn.type = "button";
editBtn.className = "msg-action-btn";
editBtn.textContent = "Edit";
editBtn.addEventListener("click", () => {
void editChatMessage(message.id, options.messageIndex ?? -1);
});
actions.appendChild(editBtn);
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "msg-action-btn";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
void deleteChatMessage(message.id, options.messageIndex ?? -1);
});
actions.appendChild(deleteBtn);
if (options.isNewest && message.role === "bot") {
const state = message.id ? ensureRetryState(message) : {retriesUsed: 0, variants: [], activeIndex: 0};
const retryBtn = document.createElement("button");
retryBtn.type = "button";
retryBtn.className = "msg-action-btn";
retryBtn.textContent = `Retry (${state.retriesUsed}/5)`;
retryBtn.disabled = state.retriesUsed >= 5 || isProcessing;
retryBtn.addEventListener("click", () => {
void retryLatestAssistantMessage(message.id, options.messageIndex ?? -1);
});
actions.appendChild(retryBtn);
if (state.variants.length > 1) {
const prevBtn = document.createElement("button");
prevBtn.type = "button";
prevBtn.className = "msg-action-btn";
prevBtn.textContent = "Prev";
prevBtn.disabled = state.activeIndex <= 0;
prevBtn.addEventListener("click", () => {
void switchRetryVariant(message.id, -1, options.messageIndex ?? -1);
});
const nextBtn = document.createElement("button");
nextBtn.type = "button";
nextBtn.className = "msg-action-btn";
nextBtn.textContent = "Next";
nextBtn.disabled = state.activeIndex >= state.variants.length - 1;
nextBtn.addEventListener("click", () => {
void switchRetryVariant(message.id, 1, options.messageIndex ?? -1);
});
const indexBadge = document.createElement("span");
indexBadge.className = "msg-variant-index";
indexBadge.textContent = `${state.activeIndex + 1}/${state.variants.length}`;
actions.append(prevBtn, indexBadge, nextBtn);
}
}
msgDiv.append(header, body);
if (actions.childElementCount) {
msgDiv.appendChild(actions);
}
}
messagesDiv.appendChild(msgDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
@@ -330,7 +592,10 @@ function renderMessages() {
if (!currentMessages.length && (!chat || chat.assistant_persona_id)) {
renderMessageEmptyState();
} else {
currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
currentMessages.forEach((msg, index) => addMessage(msg, msg.role === "user", false, {
messageIndex: index,
isNewest: index === currentMessages.length - 1
}));
}
updateChatParticipants();
updateChatActionState();
@@ -889,22 +1154,33 @@ function showAuthScreen(target) {
}
async function setActiveChat(id) {
setChatLoading(true, "Opening chat...");
activeChatId = id;
messageRetryState = new Map();
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
const res = await get(`/chats/${id}/messages`);
if (res.error) return setNotice(res.error, "error");
if (res.error) {
setChatLoading(false);
return setNotice(res.error, "error");
}
const existing = getChatById(id);
if (res.chat && existing) Object.assign(existing, res.chat);
currentMessages = res.messages || []; renderMessages();
updateSendState();
setChatLoading(false);
}
async function loadChatSessions() {
setChatLoading(true, "Loading chats...");
const res = await get("/chats");
if (res.error) return setNotice(res.error, "error");
if (res.error) {
setChatLoading(false);
return setNotice(res.error, "error");
}
chatSessions = res.chats || []; renderChatList();
if (chatSessions.length) {
const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id;
setChatLoading(false);
return setActiveChat(preferredChatId);
}
activeChatId = null;
@@ -912,21 +1188,29 @@ async function loadChatSessions() {
renderMessages();
updateWorkspaceCopy();
updateSendState();
setChatLoading(false);
return setNotice("Pick a starting action to begin.", "success");
}
async function createNewChat() {
setChatLoading(true, "Creating chat...");
const res = await post("/chats", {title: "New chat"});
if (res.error || !res.chat) return setNotice(res.error || "Unable to create chat.", "error");
if (res.error || !res.chat) {
setChatLoading(false);
return setNotice(res.error || "Unable to create chat.", "error");
}
chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
msgInput.focus();
setChatLoading(false);
}
async function startRoleplay() {
setChatLoading(true, "Starting roleplay...");
const assistantPersonaId = Number(roleplayCharacterSelect.value);
const userPersonaId = roleplayUserPersonaSelect.value ? Number(roleplayUserPersonaSelect.value) : null;
const scenarioPrompt = roleplayScenarioInput?.value.trim() || "";
if (!assistantPersonaId) {
setChatLoading(false);
return setRoleplayStarterNotice("Choose a character to start the roleplay.", "error");
}
roleplayStarterConfirm.disabled = true;
@@ -939,12 +1223,14 @@ async function startRoleplay() {
});
roleplayStarterConfirm.disabled = false;
if (res.error || !res.chat) {
setChatLoading(false);
return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error");
}
closeRoleplayStarter();
await loadChatSessions();
await setActiveChat(res.chat.id);
setNotice(res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.", "success");
setChatLoading(false);
}
async function renameChat(id) {
@@ -978,6 +1264,7 @@ async function clearChat(id) {
if (!confirmed) return;
const res = await post(`/chats/${id}/clear`, {});
if (res.error) return setNotice(res.error, "error");
messageRetryState = new Map();
currentMessages = []; renderMessages(); await loadSummary(); setNotice("Chat cleared.", "success");
}
@@ -992,8 +1279,12 @@ function exportCurrentChat() {
}
async function deleteChat(id) {
setChatLoading(true, "Deleting chat...");
const chat = getChatById(id);
if (!chat) return;
if (!chat) {
setChatLoading(false);
return;
}
const confirmed = await confirmPopup({
eyebrow: "Conversation",
title: "Delete chat",
@@ -1001,14 +1292,22 @@ async function deleteChat(id) {
confirmLabel: "Delete",
danger: true
});
if (!confirmed) return;
if (!confirmed) {
setChatLoading(false);
return;
}
const res = await del(`/chats/${id}`);
if (res.error) return setNotice(res.error, "error");
if (res.error) {
setChatLoading(false);
return setNotice(res.error, "error");
}
chatSessions = chatSessions.filter((item) => item.id !== id);
if (activeChatId === id) { activeChatId = null; currentMessages = []; renderMessages(); }
messageRetryState = new Map();
await loadSummary(); renderChatList();
if (chatSessions.length) await setActiveChat(chatSessions[0].id); else await createNewChat();
setNotice("Chat deleted.", "success");
setChatLoading(false);
}
async function displayModels() {
@@ -1073,8 +1372,7 @@ async function sendMessage() {
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
if (res.reply) {
currentMessages.push({role: "bot", content: res.reply});
addMessage(res.reply);
await setActiveChat(activeChatId);
await loadSummary();
markOnboardingCompleted();
maybeShowPersonaNudge();
@@ -1138,6 +1436,7 @@ registerForm.addEventListener("submit", (event) => { event.preventDefault(); voi
$("logout").addEventListener("click", async () => {
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
messageRetryState = new Map();
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
updateComposerPlaceholder();
updateSendState();
+48
View File
@@ -1264,6 +1264,35 @@ select:focus {
opacity: 0.86;
}
.msg-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.msg-action-btn {
min-height: 30px;
padding: 4px 10px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.26);
background: rgba(255, 255, 255, 0.12);
color: inherit;
font-size: 0.78rem;
font-weight: 700;
cursor: pointer;
}
.msg-action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.msg-variant-index {
font-size: 0.74rem;
opacity: 0.86;
}
.typing-indicator {
display: flex;
align-items: center;
@@ -1508,6 +1537,10 @@ select:focus {
width: 100%;
}
.market-preview-scenario-field {
width: 100%;
}
.persona-item-actions button {
padding: 8px 11px;
font-size: 0.84rem;
@@ -1729,6 +1762,21 @@ select:focus {
white-space: pre-wrap;
}
.market-preview-toggle {
margin-top: 6px;
padding: 0;
border: none;
background: transparent;
color: var(--brand-strong);
font-size: 0.82rem;
font-weight: 700;
text-align: left;
}
.market-preview-toggle:hover {
text-decoration: underline;
}
.settings-shell {
width: min(1180px, 100%);
margin: 0 auto;