';
metadata.uploaded_images.forEach((image, index) => {
- const imageId = `image-${messageId || Date.now()}-${index}`;
+ const imageId = `image-${metadata.message_details?.message_id || Date.now()}-${index}`;
content += `
`;
content += `
`;
content += `
`;
@@ -2514,6 +3507,11 @@ function loadMessageMetadataForDisplay(messageId, container) {
thread_attempt: metadata.thread_attempt
};
const historyContext = metadata.metadata?.history_context || null;
+ const collaborationInfo = metadata.metadata?.collaboration || null;
+ const replyContext = metadata.metadata?.reply_context || null;
+ const mentionList = Array.isArray(metadata.metadata?.mentions)
+ ? metadata.metadata.mentions
+ : [];
if (threadInfo.thread_id) {
html += '
';
@@ -2533,8 +3531,40 @@ function loadMessageMetadataForDisplay(messageId, container) {
if (metadata.id) html += `
Message ID: ${metadata.id}
`;
if (metadata.conversation_id) html += `
Conversation ID: ${metadata.conversation_id}
`;
if (metadata.role) html += `
Role: ${metadata.role}
`;
+ if (metadata.message_kind) html += `
Message Kind: ${metadata.message_kind}
`;
+ if (metadata.metadata?.source_role) html += `
Original Role: ${metadata.metadata.source_role}
`;
if (metadata.timestamp) html += `
Timestamp: ${new Date(metadata.timestamp).toLocaleString()}
`;
html += '
';
+
+ if (replyContext) {
+ html += '
';
+ html += '
Reply Context
';
+ html += '
';
+ if (replyContext.message_id) html += `
Reply Message ID: ${escapeHtml(replyContext.message_id)}
`;
+ if (replyContext.sender_display_name) html += `
Replying To: ${escapeHtml(replyContext.sender_display_name)}
`;
+ if (replyContext.content_preview) html += `
Preview: ${escapeHtml(replyContext.content_preview)}
`;
+ html += '
';
+ }
+
+ if (mentionList.length > 0) {
+ html += '
';
+ html += '
Tagged Participants
';
+ html += '
';
+ mentionList.forEach(participant => {
+ html += `@${escapeHtml(participant.display_name || participant.email || participant.user_id || 'Participant')} `;
+ });
+ html += '
';
+ }
+
+ if (collaborationInfo) {
+ html += '
';
+ html += '
Shared Conversation
';
+ html += '
';
+ if (collaborationInfo.conversation_title) html += `
Conversation: ${escapeHtml(collaborationInfo.conversation_title)}
`;
+ if (collaborationInfo.chat_type) html += `
Collaboration Type: ${escapeHtml(collaborationInfo.chat_type)}
`;
+ if (collaborationInfo.participant_count !== undefined) html += `
Participants: ${escapeHtml(collaborationInfo.participant_count)}
`;
+ html += '
';
+ }
// Image/File specific info
if (metadata.role === 'image') {
@@ -3114,16 +4144,23 @@ function unmaskMessage(messageDiv, messageId, maskBtn) {
*/
function handleDeleteButtonClick(messageDiv, messageId, messageType) {
console.log(`Delete button clicked for ${messageType} message: ${messageId}`);
+
+ const conversationId = window.chatConversations?.getCurrentConversationId?.() || window.currentConversationId || '';
+ const isCollaborativeConversation = Boolean(
+ conversationId && window.chatCollaboration?.isCollaborationConversation?.(conversationId)
+ );
// Store message info for deletion confirmation
window.pendingMessageDeletion = {
messageDiv,
messageId,
- messageType
+ messageType,
+ conversationId,
+ isCollaborativeConversation,
};
// Show appropriate confirmation modal
- if (messageType === 'user') {
+ if (messageType === 'user' && !isCollaborativeConversation) {
// User message - offer thread deletion option
const modal = document.getElementById('delete-message-modal');
if (modal) {
@@ -3137,7 +4174,9 @@ function handleDeleteButtonClick(messageDiv, messageId, messageType) {
// Update modal text based on message type
const modalBody = modal.querySelector('.modal-body p');
if (modalBody) {
- if (messageType === 'assistant') {
+ if (isCollaborativeConversation && messageType === 'user') {
+ modalBody.textContent = 'Are you sure you want to delete this shared message? This action cannot be undone.';
+ } else if (messageType === 'assistant') {
modalBody.textContent = 'Are you sure you want to delete this AI response? This action cannot be undone.';
} else if (messageType === 'image') {
modalBody.textContent = 'Are you sure you want to delete this image? This action cannot be undone.';
@@ -3161,20 +4200,31 @@ function executeMessageDeletion(deleteThread = false) {
return;
}
- const { messageDiv, messageId, messageType } = pendingDeletion;
+ const {
+ messageDiv,
+ messageId,
+ messageType,
+ conversationId,
+ isCollaborativeConversation,
+ } = pendingDeletion;
+ const shouldDeleteThread = Boolean(deleteThread && !isCollaborativeConversation);
+ const deleteEndpoint = isCollaborativeConversation && conversationId
+ ? `/api/collaboration/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}`
+ : `/api/message/${encodeURIComponent(messageId)}`;
- console.log(`Executing deletion for message ${messageId}, deleteThread: ${deleteThread}`);
+ console.log(`Executing deletion for message ${messageId}, deleteThread: ${shouldDeleteThread}`);
console.log(`Message div:`, messageDiv);
console.log(`Message ID from DOM:`, messageDiv ? messageDiv.getAttribute('data-message-id') : 'N/A');
+ console.log(`Delete endpoint:`, deleteEndpoint);
// Call delete API
- fetch(`/api/message/${messageId}`, {
+ fetch(deleteEndpoint, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
- delete_thread: deleteThread
+ delete_thread: shouldDeleteThread
})
})
.then(response => {
diff --git a/application/single_app/static/js/chat/chat-sidebar-conversations.js b/application/single_app/static/js/chat/chat-sidebar-conversations.js
index 02d401640..34b47512c 100644
--- a/application/single_app/static/js/chat/chat-sidebar-conversations.js
+++ b/application/single_app/static/js/chat/chat-sidebar-conversations.js
@@ -5,6 +5,13 @@ import { showToast } from "./chat-toast.js";
const sidebarConversationsList = document.getElementById("sidebar-conversations-list");
const sidebarNewChatBtn = document.getElementById("sidebar-new-chat-btn");
+const sidebarWorkflowSection = document.getElementById("sidebar-workflow-section");
+const sidebarWorkflowsToggle = document.getElementById("sidebar-workflows-toggle");
+const sidebarWorkflowsCaret = document.getElementById("sidebar-workflows-caret");
+const sidebarWorkflowListContainer = document.getElementById("sidebar-workflow-list-container");
+const sidebarWorkflowConversationsList = document.getElementById("sidebar-workflow-conversations-list");
+const sidebarWorkflowShowMoreBtn = document.getElementById("sidebar-workflow-show-more-btn");
+const DEFAULT_WORKFLOW_SECTION_LIMIT = 5;
function dispatchSidebarConversationsLoaded(details = {}) {
document.dispatchEvent(new CustomEvent('chat:sidebar-conversations-loaded', {
@@ -16,6 +23,9 @@ let currentActiveConversationId = null;
let sidebarShowHiddenConversations = false; // Track if hidden conversations should be shown in sidebar
let isLoadingSidebarConversations = false; // Prevent concurrent sidebar loads
let pendingSidebarReload = false; // Track if a reload is pending
+let sidebarWorkflowSectionExpanded = false;
+let sidebarWorkflowSectionCollapsed = false;
+let sidebarVisibleConversations = [];
function getShortGroupLabel(name) {
const normalizedName = (name || '').trim();
@@ -166,6 +176,107 @@ function closeSidebarConversationDropdown(dropdownBtn, dropdownInstance) {
}
}
+function isWorkflowConversation(conversation = {}) {
+ return String(conversation?.chat_type || '').trim().toLowerCase() === 'workflow';
+}
+
+function isSidebarQuickSearchActive() {
+ const searchTerm = window.chatConversations?.getQuickSearchTerm?.();
+ return Boolean(searchTerm && searchTerm.trim() !== '');
+}
+
+function appendSidebarConversationItems(container, conversations = []) {
+ if (!container) {
+ return;
+ }
+
+ conversations.forEach(conversation => {
+ container.appendChild(createSidebarConversationItem(conversation));
+ });
+}
+
+function setSidebarListMessage(container, message) {
+ if (!container) {
+ return;
+ }
+
+ container.innerHTML = `
${message}
`;
+}
+
+function applyWorkflowSectionCollapsedState(isCollapsed = false) {
+ if (!sidebarWorkflowListContainer || !sidebarWorkflowsCaret || !sidebarWorkflowsToggle) {
+ return;
+ }
+
+ sidebarWorkflowListContainer.classList.toggle('d-none', isCollapsed);
+ sidebarWorkflowsCaret.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
+ sidebarWorkflowsToggle.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
+}
+
+function renderWorkflowConversations(workflowConversations = [], isSearchActive = false) {
+ if (!sidebarWorkflowSection || !sidebarWorkflowConversationsList) {
+ return;
+ }
+
+ sidebarWorkflowConversationsList.innerHTML = '';
+
+ if (workflowConversations.length === 0) {
+ sidebarWorkflowSection.classList.add('d-none');
+ if (sidebarWorkflowShowMoreBtn) {
+ sidebarWorkflowShowMoreBtn.classList.add('d-none');
+ sidebarWorkflowShowMoreBtn.setAttribute('aria-expanded', 'false');
+ }
+ return;
+ }
+
+ sidebarWorkflowSection.classList.remove('d-none');
+ applyWorkflowSectionCollapsedState(isSearchActive ? false : sidebarWorkflowSectionCollapsed);
+
+ const showAllWorkflowConversations = isSearchActive
+ || sidebarWorkflowSectionExpanded
+ || workflowConversations.length <= DEFAULT_WORKFLOW_SECTION_LIMIT;
+ const visibleWorkflowConversations = showAllWorkflowConversations
+ ? workflowConversations
+ : workflowConversations.slice(0, DEFAULT_WORKFLOW_SECTION_LIMIT);
+
+ appendSidebarConversationItems(sidebarWorkflowConversationsList, visibleWorkflowConversations);
+
+ if (!sidebarWorkflowShowMoreBtn) {
+ return;
+ }
+
+ const showExpandButton = !isSearchActive && workflowConversations.length > DEFAULT_WORKFLOW_SECTION_LIMIT;
+ sidebarWorkflowShowMoreBtn.classList.toggle('d-none', !showExpandButton);
+ sidebarWorkflowShowMoreBtn.textContent = sidebarWorkflowSectionExpanded ? 'Show less' : 'Show more';
+ sidebarWorkflowShowMoreBtn.setAttribute('aria-expanded', sidebarWorkflowSectionExpanded ? 'true' : 'false');
+}
+
+function renderSidebarConversationSections(visibleConversations = [], totalConversationCount = 0) {
+ if (!sidebarConversationsList) {
+ return;
+ }
+
+ const regularConversations = visibleConversations.filter(conversation => !isWorkflowConversation(conversation));
+ const workflowConversations = visibleConversations.filter(conversation => isWorkflowConversation(conversation));
+ const isSearchActive = isSidebarQuickSearchActive();
+
+ sidebarConversationsList.innerHTML = '';
+
+ if (regularConversations.length > 0) {
+ appendSidebarConversationItems(sidebarConversationsList, regularConversations);
+ } else if (visibleConversations.length === 0 && totalConversationCount > 0) {
+ setSidebarListMessage(sidebarConversationsList, 'No matching conversations.');
+ } else if (workflowConversations.length > 0 && isSearchActive) {
+ setSidebarListMessage(sidebarConversationsList, 'No matching standard conversations.');
+ } else if (workflowConversations.length > 0) {
+ setSidebarListMessage(sidebarConversationsList, 'No standard conversations yet.');
+ } else {
+ setSidebarListMessage(sidebarConversationsList, 'No conversations yet.');
+ }
+
+ renderWorkflowConversations(workflowConversations, isSearchActive);
+}
+
export function setConversationUnreadState(conversationId, hasUnread) {
const sidebarItem = document.querySelector(`.sidebar-conversation-item[data-conversation-id="${conversationId}"]`);
if (!sidebarItem) {
@@ -203,14 +314,37 @@ export function loadSidebarConversations() {
isLoadingSidebarConversations = true;
pendingSidebarReload = false; // Clear any pending reload flag
+ sidebarVisibleConversations = [];
sidebarConversationsList.innerHTML = '
Loading conversations...
';
+ if (sidebarWorkflowConversationsList) {
+ sidebarWorkflowConversationsList.innerHTML = '';
+ }
+ if (sidebarWorkflowSection) {
+ sidebarWorkflowSection.classList.add('d-none');
+ }
+ if (sidebarWorkflowShowMoreBtn) {
+ sidebarWorkflowShowMoreBtn.classList.add('d-none');
+ sidebarWorkflowShowMoreBtn.setAttribute('aria-expanded', 'false');
+ }
+
+ const legacyConversationsRequest = fetch("/api/get_conversations")
+ .then(response => response.ok ? response.json() : response.json().then(err => Promise.reject(err)));
+ const collaborationConversationsRequest = window.chatCollaboration?.fetchCollaborationConversationList
+ ? window.chatCollaboration.fetchCollaborationConversationList().catch(error => {
+ console.warn('Failed to load collaborative sidebar conversations:', error);
+ return [];
+ })
+ : Promise.resolve([]);
+
+ Promise.all([legacyConversationsRequest, collaborationConversationsRequest])
+ .then(([data, collaborationConversations]) => {
+ const mergedConversations = [
+ ...(Array.isArray(data.conversations) ? data.conversations : []),
+ ...(Array.isArray(collaborationConversations) ? collaborationConversations : []),
+ ];
- fetch("/api/get_conversations")
- .then(response => response.ok ? response.json() : response.json().then(err => Promise.reject(err)))
- .then(data => {
- sidebarConversationsList.innerHTML = "";
- if (!data.conversations || data.conversations.length === 0) {
- sidebarConversationsList.innerHTML = '
No conversations yet.
';
+ if (mergedConversations.length === 0) {
+ renderSidebarConversationSections([], 0);
dispatchSidebarConversationsLoaded({ loaded: true, count: 0, hasVisibleConversations: false, isError: false });
// Reset loading flag even when no conversations
@@ -225,7 +359,7 @@ export function loadSidebarConversations() {
}
// Sort conversations: pinned first (by last_updated), then unpinned (by last_updated)
- const sortedConversations = [...data.conversations].sort((a, b) => {
+ const sortedConversations = [...mergedConversations].sort((a, b) => {
const aPinned = a.is_pinned || false;
const bPinned = b.is_pinned || false;
@@ -259,14 +393,13 @@ export function loadSidebarConversations() {
});
}
}
-
- visibleConversations.forEach(convo => {
- sidebarConversationsList.appendChild(createSidebarConversationItem(convo));
- });
+
+ sidebarVisibleConversations = visibleConversations;
+ renderSidebarConversationSections(visibleConversations, mergedConversations.length);
dispatchSidebarConversationsLoaded({
loaded: true,
- count: data.conversations.length,
+ count: mergedConversations.length,
hasVisibleConversations: visibleConversations.length > 0,
isError: false
});
@@ -313,28 +446,74 @@ function createSidebarConversationItem(convo) {
convoItem.classList.add("sidebar-conversation-item");
convoItem.setAttribute("data-conversation-id", convo.id);
convoItem.dataset.hasUnreadAssistantResponse = convo.has_unread_assistant_response ? 'true' : 'false';
+ const isCollaborativeConversation = convo.conversation_kind === 'collaborative';
+ const normalizedChatType = normalizeSidebarChatType(convo.chat_type || '', convo.context || []);
+ const canManageMembers = isCollaborativeConversation
+ ? Boolean(convo.can_manage_members)
+ : ['personal_single_user', 'group-single-user'].includes(normalizedChatType);
+ const canManageRoles = isCollaborativeConversation ? Boolean(convo.can_manage_roles) : false;
+ const canEditCollaborativeTitle = !isCollaborativeConversation || canManageRoles;
+ const canShowAddParticipants = ['personal_single_user', 'personal_multi_user', 'group-single-user', 'group_multi_user'].includes(normalizedChatType)
+ && canManageMembers;
+ const canDeleteCollaborativeConversation = Boolean(convo.can_delete_conversation);
+ const canLeaveCollaborativeConversation = Boolean(convo.can_leave_conversation);
+ const collaborativeDeleteLabel = canDeleteCollaborativeConversation ? 'Delete / Leave' : 'Leave';
+
+ if (isCollaborativeConversation) {
+ convoItem.dataset.conversationKind = 'collaborative';
+ }
+ if (convo.membership_status) {
+ convoItem.dataset.membershipStatus = convo.membership_status;
+ }
+ convoItem.dataset.canManageMembers = canManageMembers ? 'true' : 'false';
+ convoItem.dataset.canManageRoles = canManageRoles ? 'true' : 'false';
+ convoItem.dataset.canAcceptInvite = convo.can_accept_invite ? 'true' : 'false';
+ convoItem.dataset.canPostMessages = convo.can_post_messages === false ? 'false' : 'true';
+ convoItem.dataset.canDeleteConversation = canDeleteCollaborativeConversation ? 'true' : 'false';
+ convoItem.dataset.canLeaveConversation = canLeaveCollaborativeConversation ? 'true' : 'false';
+ convoItem.dataset.currentUserRole = convo.current_user_role || '';
applySidebarConversationContextAttributes(convoItem, convo.chat_type || '', convo.context || []);
const isPinned = convo.is_pinned || false;
const isHidden = convo.is_hidden || false;
const pinIcon = isPinned ? '
' : '';
const hiddenIcon = isHidden ? '
' : '';
+ const collaborationIcon = isCollaborativeConversation ? '
' : '';
+ const titleTooltip = isCollaborativeConversation
+ ? convo.title
+ : `${convo.title} (Double-click to edit)`;
+ const addParticipantsItemHtml = canShowAddParticipants
+ ? '
Add participants'
+ : '';
+ const legacyActionsHtml = isCollaborativeConversation
+ ? `
+
${isPinned ? 'Unpin' : 'Pin'}
+
${isHidden ? 'Unhide' : 'Hide'}
+
Select
+
Export
+ ${canEditCollaborativeTitle ? '
Edit title' : ''}
+ ${(canDeleteCollaborativeConversation || canLeaveCollaborativeConversation) ? `
${collaborativeDeleteLabel}` : ''}
+ `
+ : `
+
${isPinned ? 'Unpin' : 'Pin'}
+
${isHidden ? 'Unhide' : 'Hide'}
+
Select
+
Export
+
Edit title
+
Delete
+ `;
convoItem.innerHTML = `
@@ -395,6 +574,9 @@ function createSidebarConversationItem(convo) {
const titleElement = convoItem.querySelector('.sidebar-conversation-title');
if (titleElement) {
titleElement.addEventListener('dblclick', (e) => {
+ if (!canEditCollaborativeTitle) {
+ return;
+ }
e.preventDefault();
e.stopPropagation();
enableSidebarTitleEdit(convo.id);
@@ -457,6 +639,7 @@ function createSidebarConversationItem(convo) {
// Add dropdown menu event handlers
const detailsBtn = convoItem.querySelector('.details-btn');
+ const addParticipantsBtn = convoItem.querySelector('.add-participants-btn');
const pinBtn = convoItem.querySelector('.pin-btn');
const hideBtn = convoItem.querySelector('.hide-btn');
const selectBtn = convoItem.querySelector('.select-btn');
@@ -478,6 +661,15 @@ function createSidebarConversationItem(convo) {
}
});
}
+
+ if (addParticipantsBtn) {
+ addParticipantsBtn.addEventListener('click', e => {
+ e.preventDefault();
+ e.stopPropagation();
+ closeSidebarConversationDropdown(dropdownBtn, dropdownInstance);
+ window.chatCollaboration?.openParticipantPicker?.({ conversationId: convo.id });
+ });
+ }
if (pinBtn) {
pinBtn.addEventListener('click', async (e) => {
@@ -487,7 +679,7 @@ function createSidebarConversationItem(convo) {
closeSidebarConversationDropdown(dropdownBtn, dropdownInstance);
// Toggle pin status
try {
- const response = await fetch(`/api/conversations/${convo.id}/pin`, {
+ const response = await fetch(isCollaborativeConversation ? `/api/collaboration/conversations/${convo.id}/pin` : `/api/conversations/${convo.id}/pin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
@@ -518,7 +710,7 @@ function createSidebarConversationItem(convo) {
closeSidebarConversationDropdown(dropdownBtn, dropdownInstance);
// Toggle hide status
try {
- const response = await fetch(`/api/conversations/${convo.id}/hide`, {
+ const response = await fetch(isCollaborativeConversation ? `/api/collaboration/conversations/${convo.id}/hide` : `/api/conversations/${convo.id}/hide`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
@@ -797,8 +989,12 @@ export function updateSidebarConversationTitle(conversationId, newTitle) {
if (sidebarItem) {
const titleElement = sidebarItem.querySelector('.sidebar-conversation-title');
if (titleElement) {
- titleElement.textContent = newTitle;
- titleElement.title = `${newTitle} (Double-click to edit)`;
+ const existingIcons = Array.from(titleElement.querySelectorAll('i')).map(icon => icon.cloneNode(true));
+ titleElement.innerHTML = '';
+ existingIcons.forEach(icon => titleElement.appendChild(icon));
+ titleElement.appendChild(document.createTextNode(newTitle));
+ const isCollaborativeConversation = sidebarItem.dataset.conversationKind === 'collaborative';
+ titleElement.title = isCollaborativeConversation ? newTitle : `${newTitle} (Double-click to edit)`;
}
}
@@ -818,6 +1014,34 @@ export function applySidebarConversationMetadataUpdate(conversationId, updates =
updateSidebarConversationTitle(conversationId, updates.title);
}
+ if (updates.conversation_kind) {
+ sidebarItem.dataset.conversationKind = updates.conversation_kind;
+ }
+ if (Object.prototype.hasOwnProperty.call(updates, 'membership_status') && updates.membership_status) {
+ sidebarItem.dataset.membershipStatus = updates.membership_status;
+ }
+ if (Object.prototype.hasOwnProperty.call(updates, 'can_manage_members')) {
+ sidebarItem.dataset.canManageMembers = updates.can_manage_members ? 'true' : 'false';
+ }
+ if (Object.prototype.hasOwnProperty.call(updates, 'can_manage_roles')) {
+ sidebarItem.dataset.canManageRoles = updates.can_manage_roles ? 'true' : 'false';
+ }
+ if (Object.prototype.hasOwnProperty.call(updates, 'can_accept_invite')) {
+ sidebarItem.dataset.canAcceptInvite = updates.can_accept_invite ? 'true' : 'false';
+ }
+ if (Object.prototype.hasOwnProperty.call(updates, 'can_post_messages')) {
+ sidebarItem.dataset.canPostMessages = updates.can_post_messages === false ? 'false' : 'true';
+ }
+ if (Object.prototype.hasOwnProperty.call(updates, 'can_delete_conversation')) {
+ sidebarItem.dataset.canDeleteConversation = updates.can_delete_conversation ? 'true' : 'false';
+ }
+ if (Object.prototype.hasOwnProperty.call(updates, 'can_leave_conversation')) {
+ sidebarItem.dataset.canLeaveConversation = updates.can_leave_conversation ? 'true' : 'false';
+ }
+ if (Object.prototype.hasOwnProperty.call(updates, 'current_user_role')) {
+ sidebarItem.dataset.currentUserRole = updates.current_user_role || '';
+ }
+
if (Object.prototype.hasOwnProperty.call(updates, 'chat_type') || Array.isArray(updates.context)) {
applySidebarConversationContextAttributes(sidebarItem, updates.chat_type || '', updates.context || []);
renderSidebarConversationScopeBadge(sidebarItem, updates.chat_type || '', updates.context || []);
@@ -871,7 +1095,8 @@ export function enableSidebarTitleEdit(conversationId) {
try {
// Call the update function from main module
- const response = await fetch(`/api/conversations/${conversationId}`, {
+ const isCollaborativeConversation = sidebarItem.dataset.conversationKind === 'collaborative';
+ const response = await fetch(isCollaborativeConversation ? `/api/collaboration/conversations/${conversationId}` : `/api/conversations/${conversationId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@@ -965,6 +1190,39 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
}
+
+ if (sidebarWorkflowShowMoreBtn) {
+ sidebarWorkflowShowMoreBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ sidebarWorkflowSectionExpanded = !sidebarWorkflowSectionExpanded;
+ renderWorkflowConversations(
+ sidebarVisibleConversations.filter(conversation => isWorkflowConversation(conversation)),
+ isSidebarQuickSearchActive()
+ );
+ });
+ }
+
+ if (sidebarWorkflowsToggle) {
+ const toggleWorkflowSection = () => {
+ sidebarWorkflowSectionCollapsed = !sidebarWorkflowSectionCollapsed;
+ renderWorkflowConversations(
+ sidebarVisibleConversations.filter(conversation => isWorkflowConversation(conversation)),
+ isSidebarQuickSearchActive()
+ );
+ };
+
+ sidebarWorkflowsToggle.addEventListener('click', (e) => {
+ e.preventDefault();
+ toggleWorkflowSection();
+ });
+
+ sidebarWorkflowsToggle.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ toggleWorkflowSection();
+ }
+ });
+ }
// Handle sidebar pin selected button click
const sidebarPinBtn = document.getElementById('sidebar-pin-selected-btn');
diff --git a/application/single_app/static/js/chat/chat-streaming.js b/application/single_app/static/js/chat/chat-streaming.js
index 2fe1b845b..59d6492f6 100644
--- a/application/single_app/static/js/chat/chat-streaming.js
+++ b/application/single_app/static/js/chat/chat-streaming.js
@@ -388,11 +388,12 @@ function consumeStreamingResponse(requestFactory, tempAiMessageId, tempUserMessa
}
export function sendMessageWithStreaming(messageData, tempUserMessageId, currentConversationId, options = {}) {
+ const { endpoint = '/api/chat/stream' } = options;
const tempAiMessageId = createStreamingPlaceholder('Streaming...');
const recoveryConversationId = currentConversationId || messageData?.conversation_id || window.currentConversationId || null;
return consumeStreamingResponse(
- signal => fetch('/api/chat/stream', {
+ signal => fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -517,10 +518,23 @@ function finalizeStreamingMessage(messageId, userMessageId, finalData) {
// Remove the temporary streaming message
messageElement.remove();
+ const existingFinalMessage = finalData.message_id
+ ? document.querySelector(`[data-message-id="${finalData.message_id}"]`)
+ : null;
+
if (finalData.kernel_fallback_notice) {
showToast(finalData.kernel_fallback_notice, 'warning');
}
+ if (existingFinalMessage) {
+ if (finalData.conversation_id) {
+ markConversationRead(finalData.conversation_id, { force: true, suppressErrorToast: true }).catch(error => {
+ console.warn('Failed to clear unread state after live streaming completion:', error);
+ });
+ }
+ return;
+ }
+
if (finalData.image_url) {
appendMessage(
'image',
diff --git a/application/single_app/static/js/notifications.js b/application/single_app/static/js/notifications.js
index 3728f7a22..51d9fe282 100644
--- a/application/single_app/static/js/notifications.js
+++ b/application/single_app/static/js/notifications.js
@@ -21,6 +21,29 @@
let currentSearch = '';
let pollTimeout = null;
let isPolling = false;
+ let workflowAlertQueue = [];
+ let activeWorkflowAlert = null;
+ let activeWorkflowAlertTargets = [];
+ let isLoadingWorkflowAlerts = false;
+ const shownWorkflowAlertsStorageKey = 'simplechat-shown-workflow-alerts';
+ const workflowAlertModalEl = document.getElementById('workflowAlertModal');
+ const workflowAlertModal = workflowAlertModalEl && window.bootstrap
+ ? bootstrap.Modal.getOrCreateInstance(workflowAlertModalEl)
+ : null;
+ const workflowAlertModalContent = workflowAlertModalEl?.querySelector('.workflow-alert-modal-content') || null;
+ const workflowAlertPriorityBadge = document.getElementById('workflow-alert-priority-badge');
+ const workflowAlertTitle = document.getElementById('workflowAlertModalLabel');
+ const workflowAlertTriggeredAt = document.getElementById('workflow-alert-triggered-at');
+ const workflowAlertTypeCard = document.getElementById('workflow-alert-type-card');
+ const workflowAlertTypeValue = document.getElementById('workflow-alert-type-value');
+ const workflowAlertSummaryIcon = document.getElementById('workflow-alert-summary-icon');
+ const workflowAlertMeta = document.getElementById('workflow-alert-meta');
+ const workflowAlertMessage = document.getElementById('workflow-alert-message');
+ const workflowAlertResponsePreview = document.getElementById('workflow-alert-response-preview');
+ const workflowAlertResponsePreviewCard = document.getElementById('workflow-alert-response-preview-card');
+ const workflowAlertLinks = document.getElementById('workflow-alert-links');
+ const workflowAlertMarkReadBtn = document.getElementById('workflow-alert-mark-read-btn');
+ const workflowAlertDismissBtn = document.getElementById('workflow-alert-dismiss-btn');
/**
* Get randomized poll interval
@@ -28,6 +51,451 @@
function getRandomPollInterval() {
return Math.floor(Math.random() * (MAX_POLL_INTERVAL - MIN_POLL_INTERVAL + 1)) + MIN_POLL_INTERVAL;
}
+
+ function getShownWorkflowAlertIds() {
+ try {
+ const storedValue = sessionStorage.getItem(shownWorkflowAlertsStorageKey);
+ const parsedValue = storedValue ? JSON.parse(storedValue) : [];
+ return Array.isArray(parsedValue) ? parsedValue : [];
+ } catch (error) {
+ console.warn('Failed to read shown workflow alerts from session storage:', error);
+ return [];
+ }
+ }
+
+ function setShownWorkflowAlertIds(alertIds) {
+ try {
+ sessionStorage.setItem(
+ shownWorkflowAlertsStorageKey,
+ JSON.stringify((alertIds || []).slice(-50)),
+ );
+ } catch (error) {
+ console.warn('Failed to persist shown workflow alerts to session storage:', error);
+ }
+ }
+
+ function hasShownWorkflowAlert(notificationId) {
+ return getShownWorkflowAlertIds().includes(notificationId);
+ }
+
+ function rememberShownWorkflowAlert(notificationId) {
+ if (!notificationId) {
+ return;
+ }
+
+ const shownAlertIds = getShownWorkflowAlertIds();
+ if (shownAlertIds.includes(notificationId)) {
+ return;
+ }
+
+ shownAlertIds.push(notificationId);
+ setShownWorkflowAlertIds(shownAlertIds);
+ }
+
+ function getWorkflowAlertPriorityBadgeClass(priority) {
+ const normalizedPriority = String(priority || '').trim().toLowerCase();
+ if (normalizedPriority === 'high') {
+ return 'text-bg-danger';
+ }
+ if (normalizedPriority === 'medium') {
+ return 'text-bg-warning';
+ }
+ if (normalizedPriority === 'low') {
+ return 'text-bg-info';
+ }
+ return 'text-bg-secondary';
+ }
+
+ function formatWorkflowAlertPriorityLabel(priority) {
+ const normalizedPriority = String(priority || '').trim().toLowerCase();
+ if (!normalizedPriority) {
+ return 'ALERT';
+ }
+ return `${normalizedPriority.toUpperCase()} PRIORITY`;
+ }
+
+ function formatWorkflowTriggeredTime(isoString) {
+ if (!isoString) {
+ return 'Triggered: --:--:--';
+ }
+
+ const date = new Date(isoString);
+ return `Triggered: ${date.toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ })}`;
+ }
+
+ function normalizeWorkflowAlertText(text) {
+ return String(text || '').replace(/\s+/g, ' ').trim();
+ }
+
+ function buildWorkflowAlertSummary(text, maxLength = 140) {
+ const normalizedText = normalizeWorkflowAlertText(text);
+ if (!normalizedText) {
+ return '';
+ }
+
+ const sentenceMatch = normalizedText.match(/^(.+?[.!?])(?:\s|$)/);
+ if (sentenceMatch && sentenceMatch[1].trim().length >= 24 && sentenceMatch[1].trim().length <= maxLength) {
+ return sentenceMatch[1].trim();
+ }
+
+ const numberedSplit = normalizedText.split(/\s+\d+\.\s+/)[0].trim();
+ if (numberedSplit.length >= 24 && numberedSplit.length <= maxLength) {
+ return numberedSplit;
+ }
+
+ const dashSplit = normalizedText.split(/\s+-\s+/)[0].trim();
+ if (dashSplit.length >= 24 && dashSplit.length <= maxLength) {
+ return dashSplit;
+ }
+
+ if (normalizedText.length <= maxLength) {
+ return normalizedText;
+ }
+
+ return `${normalizedText.slice(0, maxLength - 3).trimEnd()}...`;
+ }
+
+ function normalizeWorkflowAlertTargetLabel(label) {
+ const normalizedLabel = String(label || '').trim();
+ const lowerLabel = normalizedLabel.toLowerCase();
+ if (lowerLabel === 'open workflow conversation' || lowerLabel === 'open workflow') {
+ return 'Open workflow';
+ }
+ if (lowerLabel.startsWith('open created')) {
+ return 'Open created conversation';
+ }
+ if (lowerLabel.startsWith('open updated')) {
+ return 'Open conversation';
+ }
+ return normalizedLabel || 'Open conversation';
+ }
+
+ function isWorkflowAlertWorkflowTarget(target) {
+ return String(target?.label || '').trim().toLowerCase() === 'open workflow';
+ }
+
+ function getWorkflowAlertTargetPriority(target) {
+ const label = String(target?.label || '').trim().toLowerCase();
+ const linkContext = target?.link_context || {};
+ const workspaceType = String(linkContext.workspace_type || '').trim().toLowerCase();
+ const chatType = String(linkContext.chat_type || '').trim().toLowerCase();
+ const conversationKind = String(linkContext.conversation_kind || '').trim().toLowerCase();
+
+ let priority = 0;
+ if (label.startsWith('open created')) {
+ priority += 100;
+ } else if (label.startsWith('open conversation')) {
+ priority += 60;
+ } else {
+ priority += 20;
+ }
+
+ if (workspaceType === 'group' || chatType.startsWith('group')) {
+ priority += 40;
+ } else if (workspaceType === 'personal' && (chatType === 'personal_multi_user' || conversationKind === 'collaboration')) {
+ priority += 20;
+ } else if (workspaceType === 'personal') {
+ priority += 10;
+ }
+
+ return priority;
+ }
+
+ function selectPreferredWorkflowAlertTargets(targets) {
+ const workflowTarget = targets.find(target => isWorkflowAlertWorkflowTarget(target)) || null;
+ const nonWorkflowTargets = targets.filter(target => !isWorkflowAlertWorkflowTarget(target));
+ const selectedTargets = [];
+
+ if (nonWorkflowTargets.length) {
+ const preferredTarget = [...nonWorkflowTargets].sort((leftTarget, rightTarget) => (
+ getWorkflowAlertTargetPriority(rightTarget) - getWorkflowAlertTargetPriority(leftTarget)
+ ))[0];
+ selectedTargets.push(preferredTarget);
+ }
+
+ if (workflowTarget && (!selectedTargets.length || selectedTargets[0]?.link_context?.conversation_id !== workflowTarget?.link_context?.conversation_id)) {
+ selectedTargets.push(workflowTarget);
+ }
+
+ return selectedTargets.length ? selectedTargets : targets.slice(0, 1);
+ }
+
+ function extractWorkflowEventTitle(text) {
+ const normalizedText = normalizeWorkflowAlertText(text);
+ if (!normalizedText) {
+ return '';
+ }
+
+ const numberedMatch = normalizedText.match(/(?:^|\s)\d+\.\s*([^\-:.]{3,90}?)(?=\s+-|\.|:|$)/);
+ if (numberedMatch) {
+ return numberedMatch[1].trim();
+ }
+
+ const headingMatch = normalizedText.match(/^([A-Z][A-Za-z0-9/&()'\s]{5,90}?)(?=\s+-|:|\.)/);
+ if (headingMatch) {
+ return headingMatch[1].trim();
+ }
+
+ return '';
+ }
+
+ function buildWorkflowAlertTitle(notification, summaryText, detailText) {
+ const metadata = notification?.metadata || {};
+ const explicitTitle = normalizeWorkflowAlertText(metadata.event_title || metadata.alert_title || '');
+ if (explicitTitle) {
+ return explicitTitle;
+ }
+
+ const extractedTitle = extractWorkflowEventTitle(detailText) || extractWorkflowEventTitle(summaryText);
+ if (extractedTitle) {
+ return extractedTitle;
+ }
+
+ const normalizedNotificationTitle = normalizeWorkflowAlertText(notification?.title || '');
+ const strippedNotificationTitle = normalizedNotificationTitle.replace(/^(low|medium|high)\s+priority\s+workflow\s+alert:\s*/i, '').trim();
+ if (strippedNotificationTitle) {
+ return strippedNotificationTitle;
+ }
+
+ return normalizeWorkflowAlertText(metadata.workflow_name || '') || 'Workflow alert';
+ }
+
+ function buildWorkflowAlertType(notification, alertTitle) {
+ const metadata = notification?.metadata || {};
+ const workflowName = normalizeWorkflowAlertText(metadata.workflow_name || '');
+ if (workflowName && workflowName.toLowerCase() !== String(alertTitle || '').trim().toLowerCase()) {
+ return workflowName;
+ }
+
+ const runnerType = String(metadata.runner_type || '').trim().toLowerCase();
+ if (runnerType === 'agent') {
+ return 'Agent Workflow';
+ }
+ if (runnerType === 'model') {
+ return 'Model Workflow';
+ }
+ return '';
+ }
+
+ function getWorkflowAlertTargetButtonClass(target, index) {
+ const label = String(target?.label || '').trim().toLowerCase();
+ if (label.startsWith('open created')) {
+ return 'btn btn-success';
+ }
+ if (label === 'open workflow') {
+ return 'btn btn-outline-secondary';
+ }
+ return `btn ${index === 0 ? 'btn-primary' : 'btn-outline-primary'}`;
+ }
+
+ function normalizeWorkflowAlertTargets(notification) {
+ const metadata = notification?.metadata || {};
+ const rawTargets = Array.isArray(metadata.link_targets) ? metadata.link_targets : [];
+ const normalizedTargets = [];
+ const seenTargetKeys = new Set();
+
+ rawTargets.forEach(target => {
+ if (!target || !target.link_url) {
+ return;
+ }
+
+ const linkContext = target.link_context || {};
+ const dedupeKey = linkContext.conversation_id || target.link_url;
+ if (!dedupeKey || seenTargetKeys.has(dedupeKey)) {
+ return;
+ }
+
+ seenTargetKeys.add(dedupeKey);
+ normalizedTargets.push({
+ label: normalizeWorkflowAlertTargetLabel(target.label),
+ link_url: target.link_url,
+ link_context: linkContext,
+ });
+ });
+
+ if (!normalizedTargets.length && notification?.link_url) {
+ normalizedTargets.push({
+ label: 'Open conversation',
+ link_url: notification.link_url,
+ link_context: notification.link_context || {},
+ });
+ }
+
+ return selectPreferredWorkflowAlertTargets(normalizedTargets);
+ }
+
+ function populateWorkflowAlertModal(notification) {
+ if (!workflowAlertModal) {
+ return;
+ }
+
+ const metadata = notification?.metadata || {};
+ const priority = String(metadata.priority || notification?.priority || 'medium').trim().toLowerCase();
+ const workflowName = metadata.workflow_name || notification?.title || 'Workflow';
+ const triggerSource = metadata.trigger_source || '';
+ const detailText = normalizeWorkflowAlertText(metadata.response_preview || metadata.error || '');
+ const summaryText = buildWorkflowAlertSummary(
+ normalizeWorkflowAlertText(notification?.message || '') || detailText || 'Workflow update available.',
+ );
+ const alertTitle = buildWorkflowAlertTitle(notification, summaryText, detailText);
+ const alertType = buildWorkflowAlertType(notification, alertTitle);
+ const metaParts = [];
+ if (triggerSource) {
+ metaParts.push(`Trigger: ${triggerSource}`);
+ }
+ if (metadata.runner_type) {
+ metaParts.push(`Runner: ${String(metadata.runner_type).trim()}`);
+ }
+ metaParts.push(formatRelativeTime(notification.created_at));
+
+ if (workflowAlertModalContent) {
+ workflowAlertModalContent.dataset.priority = priority || 'medium';
+ }
+ if (workflowAlertPriorityBadge) {
+ workflowAlertPriorityBadge.className = `badge text-uppercase ${getWorkflowAlertPriorityBadgeClass(priority)}`;
+ workflowAlertPriorityBadge.textContent = formatWorkflowAlertPriorityLabel(priority);
+ }
+ if (workflowAlertTitle) {
+ workflowAlertTitle.textContent = alertTitle || workflowName || 'Workflow alert';
+ }
+ if (workflowAlertTriggeredAt) {
+ workflowAlertTriggeredAt.innerHTML = `
Triggered: ${escapeHtml(formatWorkflowTriggeredTime(notification.created_at).replace(/^Triggered:\s*/, ''))} `;
+ }
+ if (workflowAlertTypeCard && workflowAlertTypeValue) {
+ if (alertType) {
+ workflowAlertTypeValue.textContent = alertType;
+ workflowAlertTypeCard.classList.remove('d-none');
+ } else {
+ workflowAlertTypeValue.textContent = '';
+ workflowAlertTypeCard.classList.add('d-none');
+ }
+ }
+ if (workflowAlertSummaryIcon) {
+ const iconClass = String(notification?.type_config?.icon || 'bi-bell-fill').trim();
+ workflowAlertSummaryIcon.className = `bi ${iconClass}`;
+ }
+ if (workflowAlertMeta) {
+ workflowAlertMeta.innerHTML = metaParts.filter(Boolean).map(item => `
+
${escapeHtml(item)}
+ `).join('');
+ }
+ if (workflowAlertMessage) {
+ workflowAlertMessage.textContent = summaryText || 'Workflow update available.';
+ }
+
+ if (workflowAlertResponsePreview && workflowAlertResponsePreviewCard) {
+ if (detailText && detailText !== normalizeWorkflowAlertText(summaryText)) {
+ workflowAlertResponsePreview.textContent = detailText;
+ workflowAlertResponsePreviewCard.classList.remove('d-none');
+ } else {
+ workflowAlertResponsePreview.textContent = '';
+ workflowAlertResponsePreviewCard.classList.add('d-none');
+ }
+ }
+
+ activeWorkflowAlertTargets = normalizeWorkflowAlertTargets(notification);
+ if (workflowAlertLinks) {
+ if (!activeWorkflowAlertTargets.length) {
+ workflowAlertLinks.innerHTML = '
No linked conversation is available for this alert.
';
+ } else {
+ workflowAlertLinks.innerHTML = activeWorkflowAlertTargets.map((target, index) => `
+
+ ${escapeHtml(target.label || 'Open conversation')}
+
+ `).join('');
+ }
+ }
+ }
+
+ function enqueueWorkflowAlerts(notifications) {
+ if (!Array.isArray(notifications) || !notifications.length) {
+ return;
+ }
+
+ const queuedIds = new Set(workflowAlertQueue.map(notification => notification.id));
+ notifications.forEach(notification => {
+ if (!notification?.id) {
+ return;
+ }
+ if (queuedIds.has(notification.id) || activeWorkflowAlert?.id === notification.id || hasShownWorkflowAlert(notification.id)) {
+ return;
+ }
+
+ queuedIds.add(notification.id);
+ workflowAlertQueue.push(notification);
+ });
+
+ showNextWorkflowAlert();
+ }
+
+ function showNextWorkflowAlert() {
+ if (!workflowAlertModal || activeWorkflowAlert || !workflowAlertQueue.length) {
+ return;
+ }
+
+ activeWorkflowAlert = workflowAlertQueue.shift();
+ activeWorkflowAlertTargets = [];
+ rememberShownWorkflowAlert(activeWorkflowAlert.id);
+ populateWorkflowAlertModal(activeWorkflowAlert);
+ workflowAlertModal.show();
+ }
+
+ function loadWorkflowAlerts() {
+ if (!workflowAlertModal || isLoadingWorkflowAlerts) {
+ return Promise.resolve();
+ }
+
+ isLoadingWorkflowAlerts = true;
+
+ return fetch('/api/notifications/workflow-alerts?limit=5')
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ enqueueWorkflowAlerts(data.notifications || []);
+ }
+ })
+ .catch(error => {
+ console.error('Error loading workflow alerts:', error);
+ })
+ .finally(() => {
+ isLoadingWorkflowAlerts = false;
+ });
+ }
+
+ function fetchNotificationCount() {
+ return fetch('/api/notifications/count')
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ updateNotificationBadge(data.count);
+ if (data.count > 0) {
+ return loadWorkflowAlerts();
+ }
+ }
+ return undefined;
+ })
+ .catch(error => {
+ console.error('Error polling notification count:', error);
+ });
+ }
+
+ function refreshNotificationsUi() {
+ if (document.getElementById('notifications-container')) {
+ loadNotifications();
+ return;
+ }
+
+ fetchNotificationCount();
+ }
/**
* Update notification badge in top nav and sidebar
@@ -98,17 +566,8 @@
if (isPolling) return;
isPolling = true;
-
- fetch('/api/notifications/count')
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- updateNotificationBadge(data.count);
- }
- })
- .catch(error => {
- console.error('Error polling notification count:', error);
- })
+
+ fetchNotificationCount()
.finally(() => {
isPolling = false;
@@ -311,7 +770,7 @@
attachPaginationListeners();
// Update badge
- pollNotificationCount();
+ fetchNotificationCount();
})
.catch(error => {
console.error('Error loading notifications:', error);
@@ -391,12 +850,11 @@
async function handleNotificationClick(notification) {
// Mark as read
if (!notification.is_read) {
- markNotificationRead(notification.id);
+ await markNotificationRead(notification.id);
}
-
- // Check if this is a group notification - set active group before navigating
- const groupId = notification.metadata?.group_id;
- if (groupId && notification.link_url === '/group_workspaces') {
+
+ const groupId = notification.link_context?.group_id || notification.metadata?.group_id;
+ if (groupId) {
try {
const response = await fetch('/api/groups/setActive', {
method: 'PATCH',
@@ -424,7 +882,7 @@
* Mark notification as read
*/
function markNotificationRead(notificationId) {
- fetch(`/api/notifications/${notificationId}/read`, {
+ return fetch(`/api/notifications/${notificationId}/read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -433,11 +891,14 @@
.then(response => response.json())
.then(data => {
if (data.success) {
- loadNotifications();
+ refreshNotificationsUi();
+ return true;
}
+ return false;
})
.catch(error => {
console.error('Error marking notification as read:', error);
+ return false;
});
}
@@ -445,17 +906,87 @@
* Dismiss notification
*/
function dismissNotification(notificationId) {
- fetch(`/api/notifications/${notificationId}/dismiss`, {
+ return fetch(`/api/notifications/${notificationId}/dismiss`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
- loadNotifications();
+ refreshNotificationsUi();
+ return true;
}
+ return false;
})
.catch(error => {
console.error('Error dismissing notification:', error);
+ return false;
+ });
+ }
+
+ function initializeWorkflowAlertModal() {
+ if (!workflowAlertModalEl || !workflowAlertModal) {
+ return;
+ }
+
+ workflowAlertLinks?.addEventListener('click', async function(event) {
+ const linkButton = event.target.closest('[data-workflow-alert-link]');
+ if (!linkButton || !activeWorkflowAlert) {
+ return;
+ }
+
+ const linkIndex = Number.parseInt(linkButton.getAttribute('data-workflow-alert-link') || '-1', 10);
+ const target = activeWorkflowAlertTargets[linkIndex];
+ if (!target?.link_url) {
+ return;
+ }
+
+ await markNotificationRead(activeWorkflowAlert.id);
+
+ const groupId = target.link_context?.group_id || '';
+ if (groupId) {
+ try {
+ await fetch('/api/groups/setActive', {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ groupId: groupId })
+ });
+ } catch (error) {
+ console.error('Error aligning group context for workflow alert:', error);
+ }
+ }
+
+ window.location.href = target.link_url;
+ });
+
+ workflowAlertMarkReadBtn?.addEventListener('click', async function() {
+ if (!activeWorkflowAlert) {
+ return;
+ }
+
+ await markNotificationRead(activeWorkflowAlert.id);
+ workflowAlertModal.hide();
+ });
+
+ workflowAlertDismissBtn?.addEventListener('click', async function() {
+ if (!activeWorkflowAlert) {
+ return;
+ }
+
+ await dismissNotification(activeWorkflowAlert.id);
+ workflowAlertModal.hide();
+ });
+
+ workflowAlertModalEl.addEventListener('hidden.bs.modal', function() {
+ activeWorkflowAlert = null;
+ activeWorkflowAlertTargets = [];
+ showNextWorkflowAlert();
+ });
+
+ window.addEventListener('workflow-alert-refresh-requested', function() {
+ fetchNotificationCount();
+ loadWorkflowAlerts();
});
}
@@ -552,10 +1083,12 @@
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
pollNotificationCount();
+ initializeWorkflowAlertModal();
initNotificationsPage();
});
} else {
pollNotificationCount();
+ initializeWorkflowAlertModal();
initNotificationsPage();
}
diff --git a/application/single_app/static/js/plugin_common.js b/application/single_app/static/js/plugin_common.js
index e50278d4d..dea7efff1 100644
--- a/application/single_app/static/js/plugin_common.js
+++ b/application/single_app/static/js/plugin_common.js
@@ -337,6 +337,15 @@ export async function getErrorMessageFromResponse(response, fallbackMessage = 'R
return fallbackMessage;
}
+ const trimmedResponseText = responseText.trim();
+ const normalizedResponseText = trimmedResponseText.toLowerCase();
+ if (normalizedResponseText.startsWith(' { // Load schema on initialization
this._populateGenericAuthTypeDropdown(); // Dynamically populate generic auth type dropdown after schema loads (will be called again after schema loads)
@@ -132,12 +240,18 @@ export class PluginModalStepper {
});
document.getElementById('sql-auth-type').addEventListener('change', () => this.handleSqlAuthTypeChange());
+ document.getElementById('cosmos-auth-type').addEventListener('change', () => this.handleCosmosAuthTypeChange());
// Test SQL connection button
const testConnBtn = document.getElementById('sql-test-connection-btn');
if (testConnBtn) {
testConnBtn.addEventListener('click', () => this.testSqlConnection());
}
+
+ const testCosmosBtn = document.getElementById('cosmos-test-connection-btn');
+ if (testCosmosBtn) {
+ testCosmosBtn.addEventListener('click', () => this.testCosmosConnection());
+ }
// Set up display name to generated name conversion
this.setupNameGeneration();
@@ -487,33 +601,330 @@ export class PluginModalStepper {
}
}
+ isOpenApiType(type = this.selectedType) {
+ return !!(type && type.toLowerCase().includes('openapi'));
+ }
+
+ isSqlType(type = this.selectedType) {
+ return !!(
+ type && (
+ type.toLowerCase().includes('sql') ||
+ type.toLowerCase() === 'sql_schema' ||
+ type.toLowerCase() === 'sql_query'
+ )
+ );
+ }
+
+ isCosmosType(type = this.selectedType) {
+ return !!(type && type.toLowerCase() === 'cosmos_query');
+ }
+
+ isDocumentSearchType(type = this.selectedType) {
+ return !!(type && ['search', 'document_search'].includes(type.toLowerCase()));
+ }
+
+ isSimpleChatType(type = this.selectedType) {
+ return !!(type && type.toLowerCase() === 'simplechat');
+ }
+
+ isMsGraphType(type = this.selectedType) {
+ return !!(type && type.toLowerCase() === 'msgraph');
+ }
+
+ getDefaultSimpleChatCapabilities() {
+ const defaults = {};
+ SIMPLECHAT_CAPABILITY_DEFINITIONS.forEach(definition => {
+ defaults[definition.key] = true;
+ });
+ return defaults;
+ }
+
+ normalizeSimpleChatCapabilities(rawCapabilities = null) {
+ const defaults = this.getDefaultSimpleChatCapabilities();
+ if (!rawCapabilities || typeof rawCapabilities !== 'object' || Array.isArray(rawCapabilities)) {
+ return defaults;
+ }
+
+ SIMPLECHAT_CAPABILITY_DEFINITIONS.forEach(definition => {
+ if (Object.prototype.hasOwnProperty.call(rawCapabilities, definition.key)) {
+ defaults[definition.key] = Boolean(rawCapabilities[definition.key]);
+ }
+ });
+
+ return defaults;
+ }
+
+ renderSimpleChatConfiguration() {
+ const list = document.getElementById('simplechat-capabilities-list');
+ if (!list) {
+ return;
+ }
+
+ list.innerHTML = '';
+ SIMPLECHAT_CAPABILITY_DEFINITIONS.forEach(definition => {
+ const wrapper = document.createElement('div');
+ wrapper.className = 'form-check mb-3';
+
+ const checkbox = document.createElement('input');
+ checkbox.className = 'form-check-input';
+ checkbox.type = 'checkbox';
+ checkbox.id = `simplechat-capability-${definition.key}`;
+ checkbox.checked = Boolean(this.simpleChatCapabilityState?.[definition.key]);
+
+ const label = document.createElement('label');
+ label.className = 'form-check-label';
+ label.setAttribute('for', checkbox.id);
+ label.innerHTML = `
${this.escapeHtml(definition.label)} ${this.escapeHtml(definition.description)} `;
+
+ checkbox.addEventListener('change', () => {
+ this.simpleChatCapabilityState = {
+ ...this.simpleChatCapabilityState,
+ [definition.key]: checkbox.checked
+ };
+ });
+
+ wrapper.appendChild(checkbox);
+ wrapper.appendChild(label);
+ list.appendChild(wrapper);
+ });
+ }
+
+ setSimpleChatCapabilities(rawCapabilities = null) {
+ this.simpleChatCapabilityState = this.normalizeSimpleChatCapabilities(rawCapabilities);
+ this.renderSimpleChatConfiguration();
+ }
+
+ getSelectedSimpleChatCapabilities() {
+ return this.normalizeSimpleChatCapabilities(this.simpleChatCapabilityState);
+ }
+
+ getDefaultMsGraphCapabilities() {
+ const defaults = {};
+ MSGRAPH_CAPABILITY_DEFINITIONS.forEach(definition => {
+ defaults[definition.key] = true;
+ });
+ return defaults;
+ }
+
+ normalizeMsGraphCapabilities(rawCapabilities = null) {
+ const defaults = this.getDefaultMsGraphCapabilities();
+ if (!rawCapabilities || typeof rawCapabilities !== 'object' || Array.isArray(rawCapabilities)) {
+ return defaults;
+ }
+
+ MSGRAPH_CAPABILITY_DEFINITIONS.forEach(definition => {
+ if (Object.prototype.hasOwnProperty.call(rawCapabilities, definition.key)) {
+ defaults[definition.key] = Boolean(rawCapabilities[definition.key]);
+ }
+ });
+
+ return defaults;
+ }
+
+ renderMsGraphConfiguration() {
+ const list = document.getElementById('msgraph-capabilities-list');
+ if (!list) {
+ return;
+ }
+
+ list.innerHTML = '';
+ MSGRAPH_CAPABILITY_DEFINITIONS.forEach(definition => {
+ const wrapper = document.createElement('div');
+ wrapper.className = 'form-check mb-3';
+
+ const checkbox = document.createElement('input');
+ checkbox.className = 'form-check-input';
+ checkbox.type = 'checkbox';
+ checkbox.id = `msgraph-capability-${definition.key}`;
+ checkbox.checked = Boolean(this.msGraphCapabilityState?.[definition.key]);
+
+ const label = document.createElement('label');
+ label.className = 'form-check-label';
+ label.setAttribute('for', checkbox.id);
+ label.innerHTML = `
${this.escapeHtml(definition.label)} ${this.escapeHtml(definition.description)} `;
+
+ checkbox.addEventListener('change', () => {
+ this.msGraphCapabilityState = {
+ ...this.msGraphCapabilityState,
+ [definition.key]: checkbox.checked
+ };
+ });
+
+ wrapper.appendChild(checkbox);
+ wrapper.appendChild(label);
+ list.appendChild(wrapper);
+ });
+ }
+
+ setMsGraphCapabilities(rawCapabilities = null) {
+ this.msGraphCapabilityState = this.normalizeMsGraphCapabilities(rawCapabilities);
+ this.renderMsGraphConfiguration();
+ }
+
+ getSelectedMsGraphCapabilities() {
+ return this.normalizeMsGraphCapabilities(this.msGraphCapabilityState);
+ }
+
+ initializeDocumentSearchConfiguration() {
+ const defaults = {
+ 'document-search-scope': 'all',
+ 'document-search-top-n': '12',
+ 'document-search-window-unit': 'pages',
+ 'document-search-window-target-length': '2 pages',
+ 'document-search-final-target-length': '2 pages'
+ };
+
+ Object.entries(defaults).forEach(([id, value]) => {
+ const element = document.getElementById(id);
+ if (element && !String(element.value || '').trim()) {
+ element.value = value;
+ }
+ });
+ }
+
+ getDocumentSearchAdditionalFields() {
+ const topNValue = parseInt(document.getElementById('document-search-top-n')?.value, 10);
+ const windowSizeValue = parseInt(document.getElementById('document-search-window-size')?.value, 10);
+ const windowPercentValue = parseInt(document.getElementById('document-search-window-percent')?.value, 10);
+ const focusInstructions = document.getElementById('document-search-focus-instructions')?.value.trim() || '';
+ const windowTargetLength = document.getElementById('document-search-window-target-length')?.value.trim() || '2 pages';
+ const finalTargetLength = document.getElementById('document-search-final-target-length')?.value.trim() || '2 pages';
+
+ const additionalFields = {
+ default_doc_scope: document.getElementById('document-search-scope')?.value || 'all',
+ default_top_n: !Number.isNaN(topNValue) && topNValue > 0 ? topNValue : 12,
+ default_window_unit: document.getElementById('document-search-window-unit')?.value || 'pages',
+ default_window_target_length: windowTargetLength,
+ default_final_target_length: finalTargetLength
+ };
+
+ if (focusInstructions) {
+ additionalFields.default_focus_instructions = focusInstructions;
+ }
+ if (!Number.isNaN(windowSizeValue) && windowSizeValue > 0) {
+ additionalFields.default_window_size = windowSizeValue;
+ }
+ if (!Number.isNaN(windowPercentValue) && windowPercentValue > 0) {
+ additionalFields.default_window_percent = windowPercentValue;
+ }
+
+ return additionalFields;
+ }
+
+ populateDocumentSearchForm(additionalFields = {}) {
+ document.getElementById('document-search-scope').value = additionalFields.default_doc_scope || 'all';
+ document.getElementById('document-search-top-n').value = additionalFields.default_top_n || 12;
+ document.getElementById('document-search-window-unit').value = additionalFields.default_window_unit || 'pages';
+ document.getElementById('document-search-window-size').value = additionalFields.default_window_size || '';
+ document.getElementById('document-search-window-percent').value = additionalFields.default_window_percent || '';
+ document.getElementById('document-search-focus-instructions').value = additionalFields.default_focus_instructions || '';
+ document.getElementById('document-search-window-target-length').value = additionalFields.default_window_target_length || '2 pages';
+ document.getElementById('document-search-final-target-length').value = additionalFields.default_final_target_length || '2 pages';
+ }
+
+ formatDocumentScope(scope) {
+ const scopeMap = {
+ all: 'All Accessible Content',
+ personal: 'Personal Workspace',
+ group: 'Group Workspaces',
+ public: 'Public Workspaces'
+ };
+
+ return scopeMap[scope] || scope || '-';
+ }
+
+ formatDocumentSearchWindowing(config = {}) {
+ const unit = config.default_window_unit === 'chunks' ? 'Chunks' : 'Pages';
+ if (config.default_window_size) {
+ return `${unit} (${config.default_window_size} per window)`;
+ }
+ if (config.default_window_percent) {
+ return `${unit} (${config.default_window_percent}% of document)`;
+ }
+ return `${unit} (automatic sizing)`;
+ }
+
+ isStructuredConfigType(type = this.selectedType) {
+ return this.isSqlType(type) || this.isCosmosType(type) || this.isDocumentSearchType(type) || this.isSimpleChatType(type) || this.isMsGraphType(type);
+ }
+
showConfigSectionForType() {
const openApiSection = document.getElementById('openapi-config-section');
const genericSection = document.getElementById('generic-config-section');
const sqlSection = document.getElementById('sql-config-section');
-
- // Determine plugin type
- const isOpenApiType = this.selectedType && this.selectedType.toLowerCase().includes('openapi');
- const isSqlType = this.selectedType && (
- this.selectedType.toLowerCase().includes('sql') ||
- this.selectedType.toLowerCase() === 'sql_schema' ||
- this.selectedType.toLowerCase() === 'sql_query'
- );
+ const cosmosSection = document.getElementById('cosmos-config-section');
+ const documentSearchSection = document.getElementById('document-search-config-section');
+ const simpleChatSection = document.getElementById('simplechat-config-section');
+ const msGraphSection = document.getElementById('msgraph-config-section');
+ const isOpenApiType = this.isOpenApiType();
+ const isSqlType = this.isSqlType();
+ const isCosmosType = this.isCosmosType();
+ const isDocumentSearchType = this.isDocumentSearchType();
+ const isSimpleChatType = this.isSimpleChatType();
+ const isMsGraphType = this.isMsGraphType();
if (isOpenApiType) {
openApiSection.classList.remove('d-none');
genericSection.classList.add('d-none');
sqlSection.classList.add('d-none');
+ cosmosSection.classList.add('d-none');
+ documentSearchSection.classList.add('d-none');
+ simpleChatSection.classList.add('d-none');
+ msGraphSection.classList.add('d-none');
} else if (isSqlType) {
openApiSection.classList.add('d-none');
genericSection.classList.add('d-none');
sqlSection.classList.remove('d-none');
+ cosmosSection.classList.add('d-none');
+ documentSearchSection.classList.add('d-none');
+ simpleChatSection.classList.add('d-none');
+ msGraphSection.classList.add('d-none');
// Initialize SQL plugin configuration
this.initializeSqlConfiguration();
+ } else if (isCosmosType) {
+ openApiSection.classList.add('d-none');
+ genericSection.classList.add('d-none');
+ sqlSection.classList.add('d-none');
+ cosmosSection.classList.remove('d-none');
+ documentSearchSection.classList.add('d-none');
+ simpleChatSection.classList.add('d-none');
+ msGraphSection.classList.add('d-none');
+ this.initializeCosmosConfiguration();
+ } else if (isDocumentSearchType) {
+ openApiSection.classList.add('d-none');
+ genericSection.classList.add('d-none');
+ sqlSection.classList.add('d-none');
+ cosmosSection.classList.add('d-none');
+ documentSearchSection.classList.remove('d-none');
+ simpleChatSection.classList.add('d-none');
+ msGraphSection.classList.add('d-none');
+ this.initializeDocumentSearchConfiguration();
+ } else if (isSimpleChatType) {
+ openApiSection.classList.add('d-none');
+ genericSection.classList.add('d-none');
+ sqlSection.classList.add('d-none');
+ cosmosSection.classList.add('d-none');
+ documentSearchSection.classList.add('d-none');
+ simpleChatSection.classList.remove('d-none');
+ msGraphSection.classList.add('d-none');
+ this.renderSimpleChatConfiguration();
+ } else if (isMsGraphType) {
+ openApiSection.classList.add('d-none');
+ genericSection.classList.add('d-none');
+ sqlSection.classList.add('d-none');
+ cosmosSection.classList.add('d-none');
+ documentSearchSection.classList.add('d-none');
+ simpleChatSection.classList.add('d-none');
+ msGraphSection.classList.remove('d-none');
+ this.renderMsGraphConfiguration();
} else {
openApiSection.classList.add('d-none');
genericSection.classList.remove('d-none');
sqlSection.classList.add('d-none');
+ cosmosSection.classList.add('d-none');
+ documentSearchSection.classList.add('d-none');
+ simpleChatSection.classList.add('d-none');
+ msGraphSection.classList.add('d-none');
}
}
@@ -537,17 +948,23 @@ export class PluginModalStepper {
if (stepNumber === 3) {
const titleEl = document.getElementById('step-3-title');
if (titleEl) {
- const isOpenApiType = this.selectedType && this.selectedType.toLowerCase().includes('openapi');
- const isSqlType = this.selectedType && (
- this.selectedType.toLowerCase().includes('sql') ||
- this.selectedType.toLowerCase() === 'sql_schema' ||
- this.selectedType.toLowerCase() === 'sql_query'
- );
+ const isOpenApiType = this.isOpenApiType();
+ const isSqlType = this.isSqlType();
+ const isCosmosType = this.isCosmosType();
+ const isDocumentSearchType = this.isDocumentSearchType();
if (isOpenApiType) {
titleEl.textContent = 'API Configuration';
} else if (isSqlType) {
titleEl.textContent = 'Database Configuration';
+ } else if (isCosmosType) {
+ titleEl.textContent = 'Cosmos Configuration';
+ } else if (isDocumentSearchType) {
+ titleEl.textContent = 'Document Search Configuration';
+ } else if (this.isSimpleChatType()) {
+ titleEl.textContent = 'SimpleChat Configuration';
+ } else if (this.isMsGraphType()) {
+ titleEl.textContent = 'Microsoft Graph Configuration';
} else {
titleEl.textContent = 'Configuration';
}
@@ -555,11 +972,11 @@ export class PluginModalStepper {
}
if (stepNumber === 4) {
- const isSqlType = this.selectedType === 'sql_query' || this.selectedType === 'sql_schema';
+ const isStructuredConfigType = this.isStructuredConfigType();
const additionalFieldsDiv = document.getElementById('plugin-additional-fields-div');
- // For SQL types, hide additional fields entirely since Step 3 covers all SQL config
- if (isSqlType && additionalFieldsDiv) {
+ // For SQL and Cosmos types, hide additional fields entirely since Step 3 covers config.
+ if (isStructuredConfigType && additionalFieldsDiv) {
additionalFieldsDiv.innerHTML = '';
additionalFieldsDiv.classList.add('d-none');
this.lastAdditionalFieldsType = this.selectedType;
@@ -722,8 +1139,16 @@ export class PluginModalStepper {
// Validate based on which config section is visible
const openApiSection = document.getElementById('openapi-config-section');
const sqlSection = document.getElementById('sql-config-section');
+ const cosmosSection = document.getElementById('cosmos-config-section');
+ const documentSearchSection = document.getElementById('document-search-config-section');
+ const simpleChatSection = document.getElementById('simplechat-config-section');
+ const msGraphSection = document.getElementById('msgraph-config-section');
const isOpenApiVisible = !openApiSection.classList.contains('d-none');
const isSqlVisible = !sqlSection.classList.contains('d-none');
+ const isCosmosVisible = !cosmosSection.classList.contains('d-none');
+ const isDocumentSearchVisible = !documentSearchSection.classList.contains('d-none');
+ const isSimpleChatVisible = !simpleChatSection.classList.contains('d-none');
+ const isMsGraphVisible = !msGraphSection.classList.contains('d-none');
if (isOpenApiVisible) {
// Validate OpenAPI fields
@@ -835,6 +1260,79 @@ export class PluginModalStepper {
}
}
}
+ } else if (isCosmosVisible) {
+ const endpoint = document.getElementById('cosmos-endpoint').value.trim();
+ const databaseName = document.getElementById('cosmos-database-name').value.trim();
+ const containerName = document.getElementById('cosmos-container-name').value.trim();
+ const partitionKeyPath = document.getElementById('cosmos-partition-key-path').value.trim();
+ const authType = document.getElementById('cosmos-auth-type').value;
+ const authKey = document.getElementById('cosmos-auth-key').value.trim();
+ const maxItems = parseInt(document.getElementById('cosmos-max-items').value, 10);
+ const timeout = parseInt(document.getElementById('cosmos-timeout').value, 10);
+
+ if (!endpoint) {
+ this.showError('Cosmos DB account endpoint is required.');
+ return false;
+ }
+ if (!databaseName) {
+ this.showError('Cosmos DB database name is required.');
+ return false;
+ }
+ if (!containerName) {
+ this.showError('Cosmos DB container name is required.');
+ return false;
+ }
+ if (!partitionKeyPath || !partitionKeyPath.startsWith('/')) {
+ this.showError('Partition key path is required and must start with /.');
+ return false;
+ }
+ if (authType === 'key' && !authKey) {
+ this.showError('Cosmos DB account key is required when using account key authentication.');
+ return false;
+ }
+ if (Number.isNaN(maxItems) || maxItems < 1 || maxItems > 1000) {
+ this.showError('Max items must be between 1 and 1000.');
+ return false;
+ }
+ if (Number.isNaN(timeout) || timeout < 1 || timeout > 120) {
+ this.showError('Timeout must be between 1 and 120 seconds.');
+ return false;
+ }
+ } else if (isSimpleChatVisible) {
+ const capabilityValues = Object.values(this.getSelectedSimpleChatCapabilities());
+ if (!capabilityValues.some(Boolean)) {
+ this.showError('Enable at least one SimpleChat capability before continuing.');
+ return false;
+ }
+ } else if (isMsGraphVisible) {
+ const capabilityValues = Object.values(this.getSelectedMsGraphCapabilities());
+ if (!capabilityValues.some(Boolean)) {
+ this.showError('Enable at least one Microsoft Graph capability before continuing.');
+ return false;
+ }
+ } else if (isDocumentSearchVisible) {
+ const topN = parseInt(document.getElementById('document-search-top-n').value, 10);
+ const windowSizeValue = document.getElementById('document-search-window-size').value.trim();
+ const windowPercentValue = document.getElementById('document-search-window-percent').value.trim();
+
+ if (Number.isNaN(topN) || topN < 1 || topN > 500) {
+ this.showError('Default search result limit must be between 1 and 500.');
+ return false;
+ }
+ if (windowSizeValue) {
+ const windowSize = parseInt(windowSizeValue, 10);
+ if (Number.isNaN(windowSize) || windowSize < 1 || windowSize > 100) {
+ this.showError('Preferred window size must be between 1 and 100.');
+ return false;
+ }
+ }
+ if (windowPercentValue) {
+ const windowPercent = parseInt(windowPercentValue, 10);
+ if (Number.isNaN(windowPercent) || windowPercent < 1 || windowPercent > 100) {
+ this.showError('Preferred window percent must be between 1 and 100.');
+ return false;
+ }
+ }
} else {
// Validate generic endpoint field
const endpoint = document.getElementById('plugin-endpoint-generic').value.trim();
@@ -1098,6 +1596,143 @@ export class PluginModalStepper {
}
// SQL Plugin Configuration Methods
+ initializeCosmosConfiguration() {
+ const authTypeField = document.getElementById('cosmos-auth-type');
+ const maxItemsField = document.getElementById('cosmos-max-items');
+ const timeoutField = document.getElementById('cosmos-timeout');
+ if (authTypeField && !authTypeField.value) {
+ authTypeField.value = 'identity';
+ }
+ if (maxItemsField && !maxItemsField.value) {
+ maxItemsField.value = '100';
+ }
+ if (timeoutField && !timeoutField.value) {
+ timeoutField.value = '30';
+ }
+
+ const resultDiv = document.getElementById('cosmos-test-connection-result');
+ if (resultDiv) {
+ resultDiv.classList.add('d-none');
+ }
+
+ this.handleCosmosAuthTypeChange();
+ }
+
+ handleCosmosAuthTypeChange() {
+ const authType = document.getElementById('cosmos-auth-type')?.value || 'identity';
+ const keyGroup = document.getElementById('cosmos-auth-key-group');
+ const keyInput = document.getElementById('cosmos-auth-key');
+
+ if (keyGroup) {
+ keyGroup.classList.toggle('d-none', authType !== 'key');
+ }
+
+ if (keyInput) {
+ keyInput.required = authType === 'key';
+ }
+
+ this.updateCosmosAuthInfo(authType);
+ }
+
+ updateCosmosAuthInfo(authType = document.getElementById('cosmos-auth-type')?.value || 'identity') {
+ const infoDiv = document.getElementById('cosmos-auth-info');
+ const infoText = document.getElementById('cosmos-auth-info-text');
+
+ if (!infoDiv || !infoText) {
+ return;
+ }
+
+ let message = 'Managed Identity uses Azure AD authentication without storing credentials. Assign an Azure Cosmos DB built-in data reader role to the application identity for the target account.';
+ if (authType === 'key') {
+ message = 'Account Key uses a primary or secondary Azure Cosmos DB account key. When Key Vault secret storage is enabled, the key is stored in Key Vault and edit forms preserve the stored secret if you leave the masked value unchanged.';
+ }
+
+ infoText.textContent = message;
+ infoDiv.classList.remove('d-none');
+ }
+
+ getCosmosFieldHints() {
+ const rawValue = document.getElementById('cosmos-field-hints')?.value || '';
+ return rawValue
+ .split(/[,\n]/)
+ .map(value => value.trim())
+ .filter(Boolean);
+ }
+
+ async testCosmosConnection() {
+ const btn = document.getElementById('cosmos-test-connection-btn');
+ const resultDiv = document.getElementById('cosmos-test-connection-result');
+ const alertDiv = document.getElementById('cosmos-test-connection-alert');
+ if (!btn || !resultDiv || !alertDiv) return;
+
+ const endpoint = document.getElementById('cosmos-endpoint')?.value?.trim() || '';
+ const databaseName = document.getElementById('cosmos-database-name')?.value?.trim() || '';
+ const containerName = document.getElementById('cosmos-container-name')?.value?.trim() || '';
+ const authType = document.getElementById('cosmos-auth-type')?.value || 'identity';
+ const authKey = document.getElementById('cosmos-auth-key')?.value?.trim() || '';
+ const timeout = parseInt(document.getElementById('cosmos-timeout')?.value, 10) || 10;
+
+ if (!endpoint || !databaseName || !containerName) {
+ resultDiv.classList.remove('d-none');
+ alertDiv.className = 'alert alert-warning mb-0 py-2 px-3 small';
+ alertDiv.textContent = 'Endpoint, database name, and container name are required before testing the Cosmos connection.';
+ return;
+ }
+ if (authType === 'key' && !authKey) {
+ resultDiv.classList.remove('d-none');
+ alertDiv.className = 'alert alert-warning mb-0 py-2 px-3 small';
+ alertDiv.textContent = 'Account key is required before testing a key-based Cosmos connection.';
+ return;
+ }
+
+ const originalText = btn.innerHTML;
+ btn.innerHTML = '
Testing...';
+ btn.disabled = true;
+ resultDiv.classList.add('d-none');
+
+ try {
+ const payload = {
+ endpoint,
+ database_name: databaseName,
+ container_name: containerName,
+ auth_type: authType,
+ timeout
+ };
+
+ if (authType === 'key') {
+ payload.auth_key = authKey;
+ }
+
+ const existingPluginContext = this.getTestPluginContext();
+ if (existingPluginContext) {
+ payload.existing_plugin = existingPluginContext;
+ }
+
+ const response = await fetch('/api/plugins/test-cosmos-connection', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ const data = await response.json();
+
+ resultDiv.classList.remove('d-none');
+ if (data.success) {
+ alertDiv.className = 'alert alert-success mb-0 py-2 px-3 small';
+ alertDiv.innerHTML = '
' + (data.message || 'Connection successful!');
+ } else {
+ alertDiv.className = 'alert alert-danger mb-0 py-2 px-3 small';
+ alertDiv.innerHTML = '
' + (data.error || 'Connection failed.');
+ }
+ } catch (error) {
+ resultDiv.classList.remove('d-none');
+ alertDiv.className = 'alert alert-danger mb-0 py-2 px-3 small';
+ alertDiv.innerHTML = '
Test failed: ' + (error.message || 'Network error');
+ } finally {
+ btn.innerHTML = originalText;
+ btn.disabled = false;
+ }
+ }
+
initializeSqlConfiguration() {
// Set default values
document.getElementById('sql-read-only').value = 'true';
@@ -1256,7 +1891,7 @@ export class PluginModalStepper {
this.updateSqlAuthInfo();
}
- getSqlTestPluginContext() {
+ getTestPluginContext() {
if (!this.isEditMode || !this.originalPlugin) {
return null;
}
@@ -1323,7 +1958,7 @@ export class PluginModalStepper {
payload.timeout = parseInt(document.getElementById('sql-timeout')?.value) || 10;
- const existingPluginContext = this.getSqlTestPluginContext();
+ const existingPluginContext = this.getTestPluginContext();
if (existingPluginContext) {
payload.existing_plugin = existingPluginContext;
}
@@ -1559,7 +2194,7 @@ export class PluginModalStepper {
}
document.getElementById('plugin-auth-type').value = authType;
- } else if (plugin.type && (plugin.type.toLowerCase().includes('sql') || plugin.type.toLowerCase() === 'sql_schema' || plugin.type.toLowerCase() === 'sql_query')) {
+ } else if (this.isSqlType(plugin.type)) {
// Populate SQL fields
const additionalFields = plugin.additionalFields || {};
const auth = plugin.auth || {};
@@ -1611,6 +2246,31 @@ export class PluginModalStepper {
this.handleSqlDatabaseTypeChange();
this.handleSqlConnectionMethodChange();
this.handleSqlAuthTypeChange();
+ } else if (this.isCosmosType(plugin.type)) {
+ const additionalFields = plugin.additionalFields || {};
+ const auth = plugin.auth || {};
+
+ document.getElementById('cosmos-endpoint').value = plugin.endpoint || '';
+ document.getElementById('cosmos-database-name').value = additionalFields.database_name || '';
+ document.getElementById('cosmos-container-name').value = additionalFields.container_name || '';
+ document.getElementById('cosmos-partition-key-path').value = additionalFields.partition_key_path || '';
+ document.getElementById('cosmos-field-hints').value = Array.isArray(additionalFields.field_hints)
+ ? additionalFields.field_hints.join('\n')
+ : '';
+ document.getElementById('cosmos-max-items').value = additionalFields.max_items || 100;
+ document.getElementById('cosmos-timeout').value = additionalFields.timeout || 30;
+ document.getElementById('cosmos-auth-type').value = auth.type || 'identity';
+ document.getElementById('cosmos-auth-key').value = auth.key || '';
+ this.initializeCosmosConfiguration();
+ } else if (this.isDocumentSearchType(plugin.type)) {
+ this.populateDocumentSearchForm(plugin.additionalFields || {});
+ this.initializeDocumentSearchConfiguration();
+ } else if (this.isSimpleChatType(plugin.type)) {
+ const additionalFields = plugin.additionalFields || plugin.additional_fields || {};
+ this.setSimpleChatCapabilities(additionalFields.simplechat_capabilities || plugin.simplechat_capabilities || null);
+ } else if (this.isMsGraphType(plugin.type)) {
+ const additionalFields = plugin.additionalFields || plugin.additional_fields || {};
+ this.setMsGraphCapabilities(additionalFields.msgraph_capabilities || plugin.msgraph_capabilities || null);
} else {
// Populate generic fields
document.getElementById('plugin-endpoint-generic').value = plugin.endpoint || '';
@@ -1643,8 +2303,12 @@ export class PluginModalStepper {
// Determine which configuration section is active
const openApiSection = document.getElementById('openapi-config-section');
const sqlSection = document.getElementById('sql-config-section');
+ const cosmosSection = document.getElementById('cosmos-config-section');
+ const documentSearchSection = document.getElementById('document-search-config-section');
const isOpenApiVisible = !openApiSection.classList.contains('d-none');
const isSqlVisible = !sqlSection.classList.contains('d-none');
+ const isCosmosVisible = !cosmosSection.classList.contains('d-none');
+ const isDocumentSearchVisible = !documentSearchSection.classList.contains('d-none');
let auth = {};
let endpoint = '';
@@ -1820,6 +2484,45 @@ export class PluginModalStepper {
// For SQL plugins, endpoint is not applicable
endpoint = '';
+ } else if (isCosmosVisible) {
+ endpoint = document.getElementById('cosmos-endpoint').value.trim();
+ const databaseName = document.getElementById('cosmos-database-name').value.trim();
+ const containerName = document.getElementById('cosmos-container-name').value.trim();
+ const partitionKeyPath = document.getElementById('cosmos-partition-key-path').value.trim();
+ const authType = document.getElementById('cosmos-auth-type').value;
+
+ if (!endpoint || !databaseName || !containerName || !partitionKeyPath) {
+ throw new Error('Please complete the Cosmos DB endpoint, database, container, and partition key path.');
+ }
+
+ auth.type = authType;
+ if (authType === 'key') {
+ const authKey = document.getElementById('cosmos-auth-key').value.trim();
+ if (!authKey) {
+ throw new Error('Please enter the Cosmos DB account key.');
+ }
+ auth.key = authKey;
+ } else {
+ auth.identity = 'managed_identity';
+ }
+ additionalFields.database_name = databaseName;
+ additionalFields.container_name = containerName;
+ additionalFields.partition_key_path = partitionKeyPath;
+ additionalFields.field_hints = this.getCosmosFieldHints();
+ additionalFields.max_items = parseInt(document.getElementById('cosmos-max-items').value, 10) || 100;
+ additionalFields.timeout = parseInt(document.getElementById('cosmos-timeout').value, 10) || 30;
+ } else if (isDocumentSearchVisible) {
+ endpoint = INTERNAL_DOCUMENT_SEARCH_ENDPOINT;
+ auth.type = 'NoAuth';
+ additionalFields = this.getDocumentSearchAdditionalFields();
+ } else if (this.isSimpleChatType()) {
+ endpoint = '';
+ auth.type = 'user';
+ additionalFields.simplechat_capabilities = this.getSelectedSimpleChatCapabilities();
+ } else if (this.isMsGraphType()) {
+ endpoint = MSGRAPH_DEFAULT_ENDPOINT;
+ auth.type = 'user';
+ additionalFields.msgraph_capabilities = this.getSelectedMsGraphCapabilities();
} else {
// Collect generic plugin data
console.log("Collecting generic plugin data");
@@ -1843,8 +2546,7 @@ export class PluginModalStepper {
// This preserves OpenAPI spec content and other auto-populated fields
// For SQL types, Step 3 already provides all necessary config — skip dynamic field merge
// to prevent empty Step 4 fields from overwriting populated Step 3 values
- const isSqlType = this.selectedType === 'sql_query' || this.selectedType === 'sql_schema';
- if (!isSqlType) {
+ if (!this.isStructuredConfigType()) {
try {
const dynamicFields = this.collectAdditionalFields();
// Merge dynamicFields into additionalFields (preserving existing values)
@@ -1897,23 +2599,44 @@ export class PluginModalStepper {
document.getElementById('summary-plugin-type').textContent = type;
document.getElementById('summary-plugin-description').textContent = description || '-';
- // Configuration Section - Handle endpoint vs SQL configuration
- const isSqlType = this.selectedType && (
- this.selectedType.toLowerCase().includes('sql') ||
- this.selectedType.toLowerCase() === 'sql_schema' ||
- this.selectedType.toLowerCase() === 'sql_query'
- );
+ // Configuration Section - Handle endpoint vs SQL/Cosmos configuration
+ const isSqlType = this.isSqlType();
+ const isCosmosType = this.isCosmosType();
+ const isDocumentSearchType = this.isDocumentSearchType();
+ const isSimpleChatType = this.isSimpleChatType();
const endpointRow = document.getElementById('summary-plugin-endpoint-row');
+ const databaseTypeRow = document.getElementById('summary-plugin-database-type-row');
if (isSqlType) {
// Hide endpoint for SQL plugins since they don't use endpoints
endpointRow.style.display = 'none';
+ document.getElementById('summary-plugin-database-type').textContent = this.getSqlDatabaseType() || '-';
+ databaseTypeRow.style.display = '';
+ } else if (isCosmosType) {
+ const endpoint = this.getEndpointValue();
+ document.getElementById('summary-plugin-endpoint').textContent = endpoint || '-';
+ endpointRow.style.display = '';
+ document.getElementById('summary-plugin-database-type').textContent = 'Cosmos DB for NoSQL';
+ databaseTypeRow.style.display = '';
+ } else if (isDocumentSearchType) {
+ endpointRow.style.display = 'none';
+ document.getElementById('summary-plugin-database-type').textContent = 'Internal document search';
+ databaseTypeRow.style.display = '';
+ } else if (isSimpleChatType) {
+ endpointRow.style.display = 'none';
+ document.getElementById('summary-plugin-database-type').textContent = 'Built-in SimpleChat action';
+ databaseTypeRow.style.display = '';
+ } else if (isMsGraphType) {
+ endpointRow.style.display = 'none';
+ document.getElementById('summary-plugin-database-type').textContent = 'Built-in Microsoft Graph action';
+ databaseTypeRow.style.display = '';
} else {
// Show endpoint for non-SQL plugins (OpenAPI, generic, etc.)
const endpoint = this.getEndpointValue();
document.getElementById('summary-plugin-endpoint').textContent = endpoint || '-';
endpointRow.style.display = '';
+ databaseTypeRow.style.display = 'none';
}
const authType = this.getAuthTypeValue();
@@ -1930,34 +2653,42 @@ export class PluginModalStepper {
}
const databaseType = this.getSqlDatabaseType();
- const databaseTypeRow = document.getElementById('summary-plugin-database-type-row');
- if (databaseType) {
+ if (!isSqlType && !isCosmosType && !isDocumentSearchType && !isSimpleChatType && !isMsGraphType && databaseType) {
document.getElementById('summary-plugin-database-type').textContent = databaseType;
databaseTypeRow.style.display = '';
- } else {
+ } else if (!isSqlType && !isCosmosType && !isDocumentSearchType && !isSimpleChatType && !isMsGraphType) {
databaseTypeRow.style.display = 'none';
}
// Show/hide type-specific sections
this.populateOpenApiSummary();
this.populateSqlSummary();
+ this.populateCosmosSummary();
+ this.populateDocumentSearchSummary();
+ this.populateSimpleChatSummary();
+ this.populateMsGraphSummary();
this.populateAdvancedSummary();
this.populateChangesSummary();
}
getEndpointValue() {
// Check different endpoint fields based on plugin type
- const isOpenApiType = this.selectedType && this.selectedType.toLowerCase().includes('openapi');
- const isSqlType = this.selectedType && (
- this.selectedType.toLowerCase().includes('sql') ||
- this.selectedType.toLowerCase() === 'sql_schema' ||
- this.selectedType.toLowerCase() === 'sql_query'
- );
+ const isOpenApiType = this.isOpenApiType();
+ const isSqlType = this.isSqlType();
+ const isCosmosType = this.isCosmosType();
+ const isDocumentSearchType = this.isDocumentSearchType();
+ const isMsGraphType = this.isMsGraphType();
if (isOpenApiType) {
return document.getElementById('plugin-endpoint').value.trim();
} else if (isSqlType) {
return document.getElementById('sql-connection-string').value.trim();
+ } else if (isCosmosType) {
+ return document.getElementById('cosmos-endpoint').value.trim();
+ } else if (isDocumentSearchType) {
+ return INTERNAL_DOCUMENT_SEARCH_ENDPOINT;
+ } else if (isMsGraphType) {
+ return MSGRAPH_DEFAULT_ENDPOINT;
} else {
return document.getElementById('plugin-endpoint-generic').value.trim();
}
@@ -1965,12 +2696,11 @@ export class PluginModalStepper {
getAuthTypeValue() {
// Check different auth fields based on plugin type
- const isOpenApiType = this.selectedType && this.selectedType.toLowerCase().includes('openapi');
- const isSqlType = this.selectedType && (
- this.selectedType.toLowerCase().includes('sql') ||
- this.selectedType.toLowerCase() === 'sql_schema' ||
- this.selectedType.toLowerCase() === 'sql_query'
- );
+ const isOpenApiType = this.isOpenApiType();
+ const isSqlType = this.isSqlType();
+ const isCosmosType = this.isCosmosType();
+ const isDocumentSearchType = this.isDocumentSearchType();
+ const isMsGraphType = this.isMsGraphType();
if (isOpenApiType) {
const authType = document.getElementById('plugin-auth-type').value;
@@ -1978,6 +2708,13 @@ export class PluginModalStepper {
} else if (isSqlType) {
const authType = document.getElementById('sql-auth-type').value;
return this.formatAuthType(authType);
+ } else if (isCosmosType) {
+ const authType = document.getElementById('cosmos-auth-type')?.value || 'identity';
+ return authType === 'key' ? 'Account Key' : 'Managed Identity';
+ } else if (isDocumentSearchType) {
+ return 'Internal user context';
+ } else if (isMsGraphType) {
+ return 'User';
} else {
const authType = document.getElementById('plugin-auth-type-generic').value;
return this.formatAuthType(authType);
@@ -1998,6 +2735,9 @@ export class PluginModalStepper {
'user': 'User',
'servicePrincipal': 'Service Principal',
'connection_string': 'Connection String',
+ 'connection_string_only': 'Connection String Only',
+ 'managed_identity': 'Managed Identity',
+ 'integrated': 'Integrated Authentication',
'basic': 'Basic',
'NoAuth': 'No Authentication'
};
@@ -2051,11 +2791,7 @@ export class PluginModalStepper {
}
populateSqlSummary() {
- const isSqlType = this.selectedType && (
- this.selectedType.toLowerCase().includes('sql') ||
- this.selectedType.toLowerCase() === 'sql_schema' ||
- this.selectedType.toLowerCase() === 'sql_query'
- );
+ const isSqlType = this.isSqlType();
const sqlSection = document.getElementById('summary-sql-section');
if (isSqlType) {
@@ -2076,6 +2812,111 @@ export class PluginModalStepper {
}
}
+ populateCosmosSummary() {
+ const cosmosSection = document.getElementById('summary-cosmos-section');
+ if (!cosmosSection) {
+ return;
+ }
+
+ if (this.isCosmosType()) {
+ const fieldHints = this.getCosmosFieldHints();
+ document.getElementById('summary-cosmos-auth-type').textContent = this.getAuthTypeValue() || '-';
+ document.getElementById('summary-cosmos-database-name').textContent = document.getElementById('cosmos-database-name').value.trim() || '-';
+ document.getElementById('summary-cosmos-container-name').textContent = document.getElementById('cosmos-container-name').value.trim() || '-';
+ document.getElementById('summary-cosmos-partition-key-path').textContent = document.getElementById('cosmos-partition-key-path').value.trim() || '-';
+ document.getElementById('summary-cosmos-max-items').textContent = document.getElementById('cosmos-max-items').value.trim() || '-';
+
+ const timeoutValue = document.getElementById('cosmos-timeout').value.trim();
+ document.getElementById('summary-cosmos-timeout').textContent = timeoutValue ? `${timeoutValue} seconds` : '-';
+ document.getElementById('summary-cosmos-field-hints').textContent = fieldHints.length ? fieldHints.join(', ') : 'None configured';
+ cosmosSection.style.display = '';
+ } else {
+ cosmosSection.style.display = 'none';
+ }
+ }
+
+ populateDocumentSearchSummary() {
+ const searchSection = document.getElementById('summary-document-search-section');
+ if (!searchSection) {
+ return;
+ }
+
+ if (!this.isDocumentSearchType()) {
+ searchSection.style.display = 'none';
+ return;
+ }
+
+ const config = this.getDocumentSearchAdditionalFields();
+ document.getElementById('summary-search-scope').textContent = this.formatDocumentScope(config.default_doc_scope);
+ document.getElementById('summary-search-top-n').textContent = String(config.default_top_n || 12);
+ document.getElementById('summary-search-chunk-behavior').textContent = 'Returns all chunks by default';
+ document.getElementById('summary-search-windowing').textContent = this.formatDocumentSearchWindowing(config);
+ document.getElementById('summary-search-window-target-length').textContent = config.default_window_target_length || '2 pages';
+ document.getElementById('summary-search-final-target-length').textContent = config.default_final_target_length || '2 pages';
+ document.getElementById('summary-search-focus-instructions').textContent = config.default_focus_instructions || 'Uses caller-provided focus instructions';
+ searchSection.style.display = '';
+ }
+
+ populateSimpleChatSummary() {
+ const simpleChatSection = document.getElementById('summary-simplechat-section');
+ const enabledList = document.getElementById('summary-simplechat-enabled-list');
+ const disabledList = document.getElementById('summary-simplechat-disabled-list');
+ if (!simpleChatSection || !enabledList || !disabledList) {
+ return;
+ }
+
+ if (!this.isSimpleChatType()) {
+ simpleChatSection.style.display = 'none';
+ return;
+ }
+
+ const capabilities = this.getSelectedSimpleChatCapabilities();
+ const enabledLabels = [];
+ const disabledLabels = [];
+
+ SIMPLECHAT_CAPABILITY_DEFINITIONS.forEach(definition => {
+ if (capabilities[definition.key]) {
+ enabledLabels.push(definition.label);
+ } else {
+ disabledLabels.push(definition.label);
+ }
+ });
+
+ enabledList.textContent = enabledLabels.length ? enabledLabels.join(', ') : 'None';
+ disabledList.textContent = disabledLabels.length ? disabledLabels.join(', ') : 'None';
+ simpleChatSection.style.display = '';
+ }
+
+ populateMsGraphSummary() {
+ const msGraphSection = document.getElementById('summary-msgraph-section');
+ const enabledList = document.getElementById('summary-msgraph-enabled-list');
+ const disabledList = document.getElementById('summary-msgraph-disabled-list');
+ if (!msGraphSection || !enabledList || !disabledList) {
+ return;
+ }
+
+ if (!this.isMsGraphType()) {
+ msGraphSection.style.display = 'none';
+ return;
+ }
+
+ const capabilities = this.getSelectedMsGraphCapabilities();
+ const enabledLabels = [];
+ const disabledLabels = [];
+
+ MSGRAPH_CAPABILITY_DEFINITIONS.forEach(definition => {
+ if (capabilities[definition.key]) {
+ enabledLabels.push(definition.label);
+ } else {
+ disabledLabels.push(definition.label);
+ }
+ });
+
+ enabledList.textContent = enabledLabels.length ? enabledLabels.join(', ') : 'None';
+ disabledList.textContent = disabledLabels.length ? disabledLabels.join(', ') : 'None';
+ msGraphSection.style.display = '';
+ }
+
populateSqlOptionalSetting(inputId, summaryId, rowId) {
const input = document.getElementById(inputId);
const summaryElement = document.getElementById(summaryId);
@@ -2119,25 +2960,35 @@ export class PluginModalStepper {
// Get endpoint from the appropriate field based on plugin type
let currentEndpoint = '';
- const isOpenApiType = this.selectedType && this.selectedType.toLowerCase().includes('openapi');
- const isSqlType = this.selectedType && (
- this.selectedType.toLowerCase().includes('sql') ||
- this.selectedType.toLowerCase() === 'sql_schema' ||
- this.selectedType.toLowerCase() === 'sql_query'
- );
+ const isOpenApiType = this.isOpenApiType();
+ const isSqlType = this.isSqlType();
+ const isCosmosType = this.isCosmosType();
+ const isDocumentSearchType = this.isDocumentSearchType();
+ const isSimpleChatType = this.isSimpleChatType();
+ const isMsGraphType = this.isMsGraphType();
if (isOpenApiType) {
currentEndpoint = document.getElementById('plugin-endpoint')?.value || '';
} else if (isSqlType) {
currentEndpoint = document.getElementById('sql-connection-string')?.value || '';
+ } else if (isCosmosType) {
+ currentEndpoint = document.getElementById('cosmos-endpoint')?.value || '';
+ } else if (isDocumentSearchType) {
+ currentEndpoint = INTERNAL_DOCUMENT_SEARCH_ENDPOINT;
+ } else if (isSimpleChatType) {
+ currentEndpoint = '';
+ } else if (isMsGraphType) {
+ currentEndpoint = MSGRAPH_DEFAULT_ENDPOINT;
} else {
currentEndpoint = document.getElementById('plugin-endpoint-generic')?.value || '';
}
// Get authentication information
let currentAuthKey = '';
+ let currentAuthType = '';
if (isOpenApiType) {
const authType = document.getElementById('plugin-auth-type')?.value || 'none';
+ currentAuthType = authType;
if (authType === 'api_key') {
currentAuthKey = document.getElementById('plugin-auth-api-key-value')?.value || '';
} else if (authType === 'bearer') {
@@ -2149,13 +3000,46 @@ export class PluginModalStepper {
} else if (authType === 'oauth2') {
currentAuthKey = document.getElementById('plugin-auth-oauth2-token')?.value || '';
}
+ } else if (isCosmosType) {
+ currentAuthType = document.getElementById('cosmos-auth-type')?.value || 'identity';
+ if (currentAuthType === 'key') {
+ currentAuthKey = document.getElementById('cosmos-auth-key')?.value || '';
+ }
+ } else if (isDocumentSearchType) {
+ currentAuthType = 'NoAuth';
+ } else if (isSimpleChatType) {
+ currentAuthType = 'user';
+ } else if (isMsGraphType) {
+ currentAuthType = 'user';
} else {
+ currentAuthType = document.getElementById('plugin-auth-type-generic')?.value || '';
currentAuthKey = document.getElementById('plugin-auth-key')?.value || '';
}
// Get metadata and additional fields
const currentMetadata = document.getElementById('plugin-metadata')?.value || '{}';
- const currentAdditionalFields = document.getElementById('plugin-additional-fields')?.value || '{}';
+ let currentAdditionalFields = document.getElementById('plugin-additional-fields')?.value || '{}';
+
+ if (isCosmosType) {
+ currentAdditionalFields = JSON.stringify({
+ database_name: document.getElementById('cosmos-database-name')?.value?.trim() || '',
+ container_name: document.getElementById('cosmos-container-name')?.value?.trim() || '',
+ partition_key_path: document.getElementById('cosmos-partition-key-path')?.value?.trim() || '',
+ field_hints: this.getCosmosFieldHints(),
+ max_items: parseInt(document.getElementById('cosmos-max-items')?.value, 10) || 100,
+ timeout: parseInt(document.getElementById('cosmos-timeout')?.value, 10) || 30
+ }, null, 2);
+ } else if (isDocumentSearchType) {
+ currentAdditionalFields = JSON.stringify(this.getDocumentSearchAdditionalFields(), null, 2);
+ } else if (isSimpleChatType) {
+ currentAdditionalFields = JSON.stringify({
+ simplechat_capabilities: this.getSelectedSimpleChatCapabilities()
+ }, null, 2);
+ } else if (isMsGraphType) {
+ currentAdditionalFields = JSON.stringify({
+ msgraph_capabilities: this.getSelectedMsGraphCapabilities()
+ }, null, 2);
+ }
// Compare basic fields
if (currentDisplayName !== (this.originalPlugin.displayName || '')) {
@@ -2194,6 +3078,14 @@ export class PluginModalStepper {
after: currentAuthKey ? '***' + currentAuthKey.slice(-4) : '(empty)'
};
}
+
+ const originalAuthType = (this.originalPlugin.auth && this.originalPlugin.auth.type) || '';
+ if (currentAuthType !== originalAuthType) {
+ changes.authType = {
+ before: originalAuthType || '(empty)',
+ after: currentAuthType || '(empty)'
+ };
+ }
// Compare metadata
try {
@@ -2232,7 +3124,7 @@ export class PluginModalStepper {
populateAdvancedSummary() {
const advancedSection = document.getElementById('summary-advanced-section');
- const isSqlType = this.selectedType === 'sql_query' || this.selectedType === 'sql_schema';
+ const isStructuredConfigType = this.isStructuredConfigType();
// Check if there's any metadata or additional fields
const metadata = document.getElementById('plugin-metadata').value.trim();
@@ -2250,9 +3142,9 @@ export class PluginModalStepper {
hasMetadata = metadata.length > 0 && metadata !== '{}';
}
- // For SQL types, additional fields are already shown in the SQL Database Configuration
+ // For SQL and Cosmos types, additional fields are already shown in dedicated configuration
// summary section, so skip showing them again in Advanced to avoid redundancy
- if (!isSqlType) {
+ if (!isStructuredConfigType) {
// DRY: Use private helper to collect additional fields
let additionalFieldsObj = this.collectAdditionalFields();
hasAdditionalFields = Object.keys(additionalFieldsObj).length > 0;
@@ -2272,7 +3164,7 @@ export class PluginModalStepper {
additionalFieldsPreview.style.display = 'none';
}
} else {
- // Hide additional fields for SQL types
+ // Hide additional fields for structured config types
const additionalFieldsPreview = document.getElementById('summary-additional-fields-preview');
if (additionalFieldsPreview) additionalFieldsPreview.style.display = 'none';
hasAdditionalFields = false;
@@ -2431,6 +3323,32 @@ export class PluginModalStepper {
safeSetValue('sql-password');
safeSetValue('sql-auth-type', 'username_password');
safeSetValue('sql-database-type', 'sql_server');
+
+ // Step 3 fields - Cosmos Plugin
+ safeSetValue('cosmos-endpoint');
+ safeSetValue('cosmos-database-name');
+ safeSetValue('cosmos-container-name');
+ safeSetValue('cosmos-partition-key-path');
+ safeSetValue('cosmos-field-hints');
+ safeSetValue('cosmos-max-items', '100');
+ safeSetValue('cosmos-timeout', '30');
+ safeSetValue('cosmos-auth-type', 'identity');
+ safeSetValue('cosmos-auth-key');
+
+ // Step 3 fields - Document Search Plugin
+ safeSetValue('document-search-scope', 'all');
+ safeSetValue('document-search-top-n', '12');
+ safeSetValue('document-search-window-unit', 'pages');
+ safeSetValue('document-search-window-size');
+ safeSetValue('document-search-window-percent');
+ safeSetValue('document-search-focus-instructions');
+ safeSetValue('document-search-window-target-length', '2 pages');
+ safeSetValue('document-search-final-target-length', '2 pages');
+
+ this.simpleChatCapabilityState = this.getDefaultSimpleChatCapabilities();
+ this.renderSimpleChatConfiguration();
+ this.msGraphCapabilityState = this.getDefaultMsGraphCapabilities();
+ this.renderMsGraphConfiguration();
// Clear any type selection
this.selectedType = null;
@@ -2441,6 +3359,7 @@ export class PluginModalStepper {
this.toggleOpenApiAuthFields();
this.toggleGenericAuthFields();
this.handleSqlAuthTypeChange();
+ this.handleCosmosAuthTypeChange();
} catch (e) {
console.log('Some auth field toggles not available:', e.message);
}
diff --git a/application/single_app/static/js/workflow/workflow-activity.js b/application/single_app/static/js/workflow/workflow-activity.js
new file mode 100644
index 000000000..8826bfbad
--- /dev/null
+++ b/application/single_app/static/js/workflow/workflow-activity.js
@@ -0,0 +1,523 @@
+// workflow-activity.js
+
+const pageState = {
+ snapshot: null,
+ selectedActivityId: "",
+ eventSource: null,
+};
+
+const titleEl = document.getElementById("workflow-activity-title");
+const statusEl = document.getElementById("workflow-activity-status");
+const captionEl = document.getElementById("workflow-activity-caption");
+const conversationLinkEl = document.getElementById("workflow-activity-conversation-link");
+const refreshBtn = document.getElementById("workflow-activity-refresh-btn");
+const responseEl = document.getElementById("workflow-activity-response");
+const emptyEl = document.getElementById("workflow-activity-empty");
+const timelineEl = document.getElementById("workflow-activity-timeline");
+const detailTitleEl = document.getElementById("workflow-activity-detail-title");
+const detailMetaEl = document.getElementById("workflow-activity-detail-meta");
+const detailSummaryEl = document.getElementById("workflow-activity-detail-summary");
+const detailTextEl = document.getElementById("workflow-activity-detail-text");
+const eventHistoryEl = document.getElementById("workflow-activity-event-history");
+const statRunEl = document.getElementById("workflow-activity-stat-run");
+const statTotalEl = document.getElementById("workflow-activity-stat-total");
+const statToolsEl = document.getElementById("workflow-activity-stat-tools");
+const statStartedEl = document.getElementById("workflow-activity-stat-started");
+
+function normalizeText(value) {
+ return String(value || "").trim();
+}
+
+function escapeHtml(value) {
+ return String(value || "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function formatDateTime(value) {
+ if (!value) {
+ return "--";
+ }
+
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ return value;
+ }
+
+ return new Intl.DateTimeFormat(undefined, {
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(date);
+}
+
+function formatDuration(value) {
+ const numericValue = Number(value || 0);
+ if (!Number.isFinite(numericValue) || numericValue <= 0) {
+ return "";
+ }
+
+ if (numericValue < 1000) {
+ return `${Math.round(numericValue)} ms`;
+ }
+
+ const seconds = numericValue / 1000;
+ if (seconds < 60) {
+ return `${seconds.toFixed(seconds >= 10 ? 0 : 1)} s`;
+ }
+
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = Math.round(seconds % 60);
+ return `${minutes}m ${remainingSeconds}s`;
+}
+
+function getQueryParam(name) {
+ const params = new URLSearchParams(window.location.search);
+ return normalizeText(params.get(name));
+}
+
+function buildApiUrl(path) {
+ const url = new URL(path, window.location.origin);
+ const conversationId = getQueryParam("conversationId");
+ const workflowId = getQueryParam("workflowId");
+ const runId = getQueryParam("runId");
+
+ if (conversationId) {
+ url.searchParams.set("conversation_id", conversationId);
+ }
+ if (workflowId) {
+ url.searchParams.set("workflow_id", workflowId);
+ }
+ if (runId) {
+ url.searchParams.set("run_id", runId);
+ }
+
+ return url.toString();
+}
+
+function buildConversationUrl(conversationId) {
+ const normalizedConversationId = normalizeText(conversationId);
+ if (!normalizedConversationId) {
+ return "";
+ }
+
+ return `/chats?conversationId=${encodeURIComponent(normalizedConversationId)}`;
+}
+
+function buildStatusBadge(status) {
+ const normalizedStatus = normalizeText(status).toLowerCase() || "idle";
+ const label = normalizedStatus === "running"
+ ? "Running"
+ : normalizedStatus === "failed"
+ ? "Failed"
+ : normalizedStatus === "completed"
+ ? "Completed"
+ : normalizedStatus;
+ const className = normalizedStatus === "running"
+ ? "text-bg-primary"
+ : normalizedStatus === "failed"
+ ? "text-bg-danger"
+ : normalizedStatus === "completed"
+ ? "text-bg-success"
+ : "text-bg-secondary";
+ return `
${escapeHtml(label)} `;
+}
+
+function applyStatusBadge(element, status) {
+ if (!element) {
+ return;
+ }
+
+ const normalizedStatus = normalizeText(status).toLowerCase() || "idle";
+ const className = normalizedStatus === "running"
+ ? "text-bg-primary"
+ : normalizedStatus === "failed"
+ ? "text-bg-danger"
+ : normalizedStatus === "completed"
+ ? "text-bg-success"
+ : "text-bg-secondary";
+ const label = normalizedStatus === "running"
+ ? "Running"
+ : normalizedStatus === "failed"
+ ? "Failed"
+ : normalizedStatus === "completed"
+ ? "Completed"
+ : normalizedStatus;
+
+ element.className = `badge ${className}`;
+ element.textContent = label;
+}
+
+function updateResponseBlock(run) {
+ const errorText = normalizeText(run?.error);
+ const previewText = normalizeText(run?.response_preview);
+ if (!responseEl) {
+ return;
+ }
+
+ if (!errorText && !previewText) {
+ responseEl.classList.add("d-none");
+ responseEl.innerHTML = "";
+ return;
+ }
+
+ const heading = errorText ? "Run Error" : "Response Preview";
+ responseEl.innerHTML = `
+
${escapeHtml(heading)}
+
${escapeHtml(errorText || previewText)}
+ `;
+ responseEl.classList.remove("d-none");
+}
+
+function renderHeader(snapshot) {
+ const workflow = snapshot.workflow || {};
+ const conversation = snapshot.conversation || {};
+ const run = snapshot.run || null;
+ const workflowName = normalizeText(workflow.name) || normalizeText(conversation.title) || "Workflow activity";
+ const runStatus = normalizeText(run?.status).toLowerCase();
+ const runCaption = run
+ ? `${normalizeText(run.trigger_source) || "manual"} run ${runStatus || "captured"}`
+ : "Waiting for a captured workflow run.";
+
+ if (titleEl) {
+ titleEl.textContent = workflowName;
+ }
+ if (statusEl) {
+ applyStatusBadge(statusEl, runStatus || "idle");
+ }
+ if (captionEl) {
+ const modelOrAgent = normalizeText(run?.agent_display_name || run?.agent_name || run?.model_deployment_name);
+ captionEl.textContent = modelOrAgent ? `${runCaption} using ${modelOrAgent}.` : `${runCaption}.`;
+ }
+
+ const conversationUrl = buildConversationUrl(conversation.id || run?.conversation_id);
+ if (conversationLinkEl) {
+ conversationLinkEl.classList.toggle("d-none", !conversationUrl);
+ conversationLinkEl.href = conversationUrl || "#";
+ }
+
+ if (statRunEl) {
+ statRunEl.textContent = run ? normalizeText(run.id).slice(0, 8) || "Captured" : "Pending";
+ }
+ if (statTotalEl) {
+ statTotalEl.textContent = String(Array.isArray(snapshot.activities) ? snapshot.activities.length : 0);
+ }
+ if (statToolsEl) {
+ const toolCount = Array.isArray(snapshot.activities)
+ ? snapshot.activities.filter(activity => normalizeText(activity.kind) === "tool_invocation").length
+ : 0;
+ statToolsEl.textContent = String(toolCount);
+ }
+ if (statStartedEl) {
+ statStartedEl.textContent = formatDateTime(run?.started_at);
+ }
+
+ updateResponseBlock(run);
+}
+
+function buildActivityMeta(activity) {
+ const parts = [];
+ if (normalizeText(activity.lane_label)) {
+ parts.push(`
${escapeHtml(activity.lane_label)}`);
+ }
+ if (normalizeText(activity.started_at)) {
+ parts.push(`
${escapeHtml(formatDateTime(activity.started_at))}`);
+ }
+ const durationLabel = formatDuration(activity.duration_ms);
+ if (durationLabel) {
+ parts.push(`
${escapeHtml(durationLabel)}`);
+ }
+ return parts.join("");
+}
+
+function renderTimeline(snapshot) {
+ const activities = Array.isArray(snapshot.activities) ? snapshot.activities : [];
+ if (!timelineEl || !emptyEl) {
+ return;
+ }
+
+ const showEmptyState = !activities.length;
+ emptyEl.classList.toggle("d-none", !showEmptyState);
+ timelineEl.classList.toggle("d-none", showEmptyState);
+
+ if (showEmptyState) {
+ timelineEl.innerHTML = "";
+ return;
+ }
+
+ timelineEl.innerHTML = activities.map(activity => {
+ const activityId = normalizeText(activity.id);
+ const selectedClass = pageState.selectedActivityId === activityId ? "is-selected" : "";
+ const laneIndex = Number(activity.lane_index || 0);
+ const summary = normalizeText(activity.summary);
+ const metaHtml = buildActivityMeta(activity);
+ return `
+
+ `;
+ }).join("");
+
+ timelineEl.querySelectorAll("[data-activity-id]").forEach(button => {
+ button.addEventListener("click", () => {
+ selectActivity(button.getAttribute("data-activity-id"));
+ });
+ });
+}
+
+function renderDetailMeta(activity) {
+ if (!detailMetaEl) {
+ return;
+ }
+
+ const parts = [];
+ parts.push(buildStatusBadge(activity.status || "completed"));
+ if (normalizeText(activity.lane_label)) {
+ parts.push(`
${escapeHtml(activity.lane_label)} `);
+ }
+ if (normalizeText(activity.started_at)) {
+ parts.push(`
${escapeHtml(formatDateTime(activity.started_at))} `);
+ }
+ const durationLabel = formatDuration(activity.duration_ms);
+ if (durationLabel) {
+ parts.push(`
${escapeHtml(durationLabel)} `);
+ }
+ detailMetaEl.innerHTML = parts.join("");
+}
+
+function renderEventHistory(activity) {
+ if (!eventHistoryEl) {
+ return;
+ }
+
+ const events = Array.isArray(activity.events) ? activity.events : [];
+ if (!events.length) {
+ eventHistoryEl.innerHTML = "No event history is available for this activity.";
+ return;
+ }
+
+ eventHistoryEl.innerHTML = events.map(event => `
+
+
+
${escapeHtml(normalizeText(event.detail) || "No additional technical detail recorded.")}
+
+ `).join("");
+}
+
+function renderSelectedActivity(activity) {
+ if (!activity) {
+ if (detailTitleEl) {
+ detailTitleEl.textContent = "Select an activity";
+ }
+ if (detailSummaryEl) {
+ detailSummaryEl.textContent = "Choose a card on the timeline to inspect the event stream, timing, and captured technical detail.";
+ }
+ if (detailTextEl) {
+ detailTextEl.textContent = "No activity selected.";
+ }
+ if (detailMetaEl) {
+ detailMetaEl.innerHTML = "";
+ }
+ if (eventHistoryEl) {
+ eventHistoryEl.innerHTML = "Select an activity to inspect its event history.";
+ }
+ return;
+ }
+
+ if (detailTitleEl) {
+ detailTitleEl.textContent = normalizeText(activity.title) || "Workflow activity";
+ }
+ if (detailSummaryEl) {
+ detailSummaryEl.textContent = normalizeText(activity.summary) || "No summary available.";
+ }
+ if (detailTextEl) {
+ detailTextEl.textContent = normalizeText(activity.detail) || "No additional technical detail recorded.";
+ }
+ renderDetailMeta(activity);
+ renderEventHistory(activity);
+}
+
+function getSelectedActivity(snapshot) {
+ const activities = Array.isArray(snapshot.activities) ? snapshot.activities : [];
+ if (!activities.length) {
+ return null;
+ }
+
+ const selectedActivity = activities.find(activity => normalizeText(activity.id) === pageState.selectedActivityId);
+ if (selectedActivity) {
+ return selectedActivity;
+ }
+
+ const runningActivity = activities.find(activity => normalizeText(activity.status).toLowerCase() === "running");
+ if (runningActivity) {
+ pageState.selectedActivityId = normalizeText(runningActivity.id);
+ return runningActivity;
+ }
+
+ const fallbackActivity = activities[activities.length - 1];
+ pageState.selectedActivityId = normalizeText(fallbackActivity.id);
+ return fallbackActivity;
+}
+
+function applySnapshot(snapshot) {
+ pageState.snapshot = snapshot || {};
+ renderHeader(pageState.snapshot);
+ renderTimeline(pageState.snapshot);
+ renderSelectedActivity(getSelectedActivity(pageState.snapshot));
+ toggleEventStream();
+}
+
+function selectActivity(activityId) {
+ pageState.selectedActivityId = normalizeText(activityId);
+ if (!pageState.snapshot) {
+ return;
+ }
+
+ renderTimeline(pageState.snapshot);
+ renderSelectedActivity(getSelectedActivity(pageState.snapshot));
+}
+
+async function loadSnapshot() {
+ const response = await fetch(buildApiUrl("/api/user/workflows/activity"), {
+ credentials: "same-origin",
+ });
+ const payload = await response.json().catch(() => ({}));
+ if (!response.ok) {
+ throw new Error(payload.error || "Unable to load workflow activity.");
+ }
+
+ applySnapshot(payload);
+}
+
+function shouldListenForUpdates(snapshot) {
+ const hasConversationId = Boolean(getQueryParam("conversationId"));
+ const hasExplicitRunId = Boolean(getQueryParam("runId"));
+ if (!hasConversationId || hasExplicitRunId || typeof EventSource === "undefined") {
+ return false;
+ }
+
+ const run = snapshot?.run || null;
+ return !run || normalizeText(run.status).toLowerCase() === "running";
+}
+
+function stopEventStream() {
+ if (pageState.eventSource) {
+ pageState.eventSource.close();
+ pageState.eventSource = null;
+ }
+}
+
+function startEventStream() {
+ if (pageState.eventSource || !shouldListenForUpdates(pageState.snapshot)) {
+ return;
+ }
+
+ const eventSource = new EventSource(buildApiUrl("/api/user/workflows/activity/stream"));
+ eventSource.onmessage = event => {
+ if (!event?.data) {
+ return;
+ }
+
+ try {
+ const payload = JSON.parse(event.data);
+ applySnapshot(payload);
+ } catch (error) {
+ console.warn("Failed to parse workflow activity event", error);
+ }
+ };
+ eventSource.onerror = () => {
+ const runStatus = normalizeText(pageState.snapshot?.run?.status).toLowerCase();
+ if (runStatus && runStatus !== "running") {
+ stopEventStream();
+ }
+ };
+
+ pageState.eventSource = eventSource;
+}
+
+function toggleEventStream() {
+ if (shouldListenForUpdates(pageState.snapshot)) {
+ startEventStream();
+ } else {
+ stopEventStream();
+ }
+}
+
+async function initializePage() {
+ const hasAnyIdentifier = Boolean(getQueryParam("conversationId") || getQueryParam("workflowId") || getQueryParam("runId"));
+ if (!hasAnyIdentifier) {
+ applySnapshot({
+ workflow: null,
+ conversation: null,
+ run: null,
+ activities: [],
+ lane_count: 1,
+ live: false,
+ });
+ if (captionEl) {
+ captionEl.textContent = "Open this page from a workflow conversation or a workflow run history entry.";
+ }
+ return;
+ }
+
+ try {
+ await loadSnapshot();
+ } catch (error) {
+ console.error("Failed to load workflow activity", error);
+ applySnapshot({
+ workflow: null,
+ conversation: null,
+ run: null,
+ activities: [],
+ lane_count: 1,
+ live: false,
+ });
+ if (titleEl) {
+ titleEl.textContent = "Workflow activity unavailable";
+ }
+ if (statusEl) {
+ applyStatusBadge(statusEl, "failed");
+ }
+ if (captionEl) {
+ captionEl.textContent = error.message || "Unable to load workflow activity.";
+ }
+ }
+}
+
+if (refreshBtn) {
+ refreshBtn.addEventListener("click", () => {
+ initializePage().catch(error => {
+ console.warn("Workflow activity refresh failed", error);
+ });
+ });
+}
+
+window.addEventListener("beforeunload", () => {
+ stopEventStream();
+});
+
+window.addEventListener("DOMContentLoaded", () => {
+ initializePage().catch(error => {
+ console.warn("Workflow activity initialization failed", error);
+ });
+});
diff --git a/application/single_app/static/js/workspace/view-utils.js b/application/single_app/static/js/workspace/view-utils.js
index 216a8ffe0..6fb6003bf 100644
--- a/application/single_app/static/js/workspace/view-utils.js
+++ b/application/single_app/static/js/workspace/view-utils.js
@@ -52,6 +52,8 @@ export function getTypeIcon(type) {
if (!type) return "bi-lightning-charge";
const t = type.toLowerCase();
if (t.includes("sql")) return "bi-database";
+ if (t.includes("cosmos")) return "bi-database-fill-gear";
+ if (t.includes("simplechat")) return "bi-chat-square-dots";
if (t.includes("openapi")) return "bi-globe";
if (t.includes("log_analytics")) return "bi-graph-up";
if (t.includes("msgraph")) return "bi-microsoft";
diff --git a/application/single_app/static/js/workspace/workspace-init.js b/application/single_app/static/js/workspace/workspace-init.js
index da541dc3c..1059ed752 100644
--- a/application/single_app/static/js/workspace/workspace-init.js
+++ b/application/single_app/static/js/workspace/workspace-init.js
@@ -65,6 +65,13 @@ document.addEventListener('DOMContentLoaded', () => {
} else {
console.error("fetchUserPrompts function not found.");
}
+ } else if (targetId === '#workflows-tab') {
+ console.log("Loading workflows tab data...");
+ if (typeof window.fetchUserWorkflows === 'function') {
+ window.fetchUserWorkflows();
+ } else {
+ console.error("fetchUserWorkflows function not found.");
+ }
}
}
diff --git a/application/single_app/static/js/workspace/workspace_workflows.js b/application/single_app/static/js/workspace/workspace_workflows.js
new file mode 100644
index 000000000..520937f62
--- /dev/null
+++ b/application/single_app/static/js/workspace/workspace_workflows.js
@@ -0,0 +1,1105 @@
+// workspace_workflows.js
+
+import { showToast } from "../chat/chat-toast.js";
+import { escapeHtml, truncateDescription } from "./view-utils.js";
+
+const workflowsTableBody = document.getElementById("workflows-table-body");
+const workflowsSearchInput = document.getElementById("workflows-search");
+const workflowsSummary = document.getElementById("workflows-summary");
+const createWorkflowBtn = document.getElementById("create-workflow-btn");
+
+const workflowModalEl = document.getElementById("workflowModal");
+const workflowModal = workflowModalEl && window.bootstrap ? bootstrap.Modal.getOrCreateInstance(workflowModalEl) : null;
+const workflowForm = document.getElementById("workflow-form");
+const workflowModalLabel = document.getElementById("workflowModalLabel");
+const workflowSaveBtn = document.getElementById("workflow-save-btn");
+
+const workflowIdInput = document.getElementById("workflow-id");
+const workflowNameInput = document.getElementById("workflow-name");
+const workflowDescriptionInput = document.getElementById("workflow-description");
+const workflowTaskPromptInput = document.getElementById("workflow-task-prompt");
+const workflowRunnerTypeSelect = document.getElementById("workflow-runner-type");
+const workflowAgentFields = document.getElementById("workflow-agent-fields");
+const workflowAgentSelect = document.getElementById("workflow-agent-select");
+const workflowAgentHelp = document.getElementById("workflow-agent-help");
+const workflowModelFields = document.getElementById("workflow-model-fields");
+const workflowModelSourceSelect = document.getElementById("workflow-model-source");
+const workflowModelEndpointGroup = document.getElementById("workflow-model-endpoint-group");
+const workflowModelEndpointSelect = document.getElementById("workflow-model-endpoint-select");
+const workflowModelGroup = document.getElementById("workflow-model-group");
+const workflowModelSelect = document.getElementById("workflow-model-select");
+const workflowModelHelp = document.getElementById("workflow-model-help");
+const workflowTriggerTypeSelect = document.getElementById("workflow-trigger-type");
+const workflowScheduleValueGroup = document.getElementById("workflow-schedule-value-group");
+const workflowScheduleUnitGroup = document.getElementById("workflow-schedule-unit-group");
+const workflowScheduleValueInput = document.getElementById("workflow-schedule-value");
+const workflowScheduleUnitSelect = document.getElementById("workflow-schedule-unit");
+const workflowEnabledGroup = document.getElementById("workflow-enabled-group");
+const workflowEnabledToggle = document.getElementById("workflow-enabled");
+const workflowTriggerHelp = document.getElementById("workflow-trigger-help");
+const workflowAlertPrioritySelect = document.getElementById("workflow-alert-priority");
+
+const workflowHistoryModalEl = document.getElementById("workflowHistoryModal");
+const workflowHistoryModal = workflowHistoryModalEl && window.bootstrap ? bootstrap.Modal.getOrCreateInstance(workflowHistoryModalEl) : null;
+const workflowHistoryModalLabel = document.getElementById("workflowHistoryModalLabel");
+const workflowHistoryBody = document.getElementById("workflow-history-body");
+const workflowHistoryConversationId = document.getElementById("workflow-history-conversation-id");
+const workflowHistoryConversationLink = document.getElementById("workflow-history-open-conversation-link");
+
+const workflowDeleteModalEl = document.getElementById("workflowDeleteModal");
+const workflowDeleteModal = workflowDeleteModalEl && window.bootstrap ? bootstrap.Modal.getOrCreateInstance(workflowDeleteModalEl) : null;
+const workflowDeleteName = document.getElementById("workflow-delete-name");
+const workflowDeleteConfirmBtn = document.getElementById("workflow-delete-confirm-btn");
+
+let workflows = [];
+let agentOptions = [];
+let agentsLoaded = false;
+let workflowPendingDelete = null;
+let currentHistoryWorkflowId = "";
+let currentEditingWorkflow = null;
+
+function normalizeText(value) {
+ return String(value || "").trim();
+}
+
+function setElementVisibility(element, isVisible) {
+ if (!element) {
+ return;
+ }
+ element.classList.toggle("d-none", !isVisible);
+}
+
+function formatDateTime(value) {
+ if (!value) {
+ return "";
+ }
+
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ return value;
+ }
+
+ return new Intl.DateTimeFormat(undefined, {
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(date);
+}
+
+function buildWorkflowConversationUrl(conversationId) {
+ const normalizedConversationId = normalizeText(conversationId);
+ if (!normalizedConversationId) {
+ return "";
+ }
+
+ return `/chats?conversationId=${encodeURIComponent(normalizedConversationId)}`;
+}
+
+function buildWorkflowActivityUrl(conversationId, runId = "", workflowId = "") {
+ const normalizedConversationId = normalizeText(conversationId);
+ if (!normalizedConversationId) {
+ return "";
+ }
+
+ const url = new URL("/workflow-activity", window.location.origin);
+ url.searchParams.set("conversationId", normalizedConversationId);
+
+ const normalizedRunId = normalizeText(runId);
+ if (normalizedRunId) {
+ url.searchParams.set("runId", normalizedRunId);
+ }
+
+ const normalizedWorkflowId = normalizeText(workflowId);
+ if (normalizedWorkflowId) {
+ url.searchParams.set("workflowId", normalizedWorkflowId);
+ }
+
+ return url.toString();
+}
+
+function updateWorkflowConversationLink(element, conversationId) {
+ if (!element) {
+ return;
+ }
+
+ const conversationUrl = buildWorkflowConversationUrl(conversationId);
+ element.classList.toggle("d-none", !conversationUrl);
+ element.href = conversationUrl || "#";
+}
+
+function buildStatusBadge(status) {
+ const normalizedStatus = normalizeText(status).toLowerCase() || "idle";
+ const variant = normalizedStatus === "completed"
+ ? "success"
+ : normalizedStatus === "failed"
+ ? "danger"
+ : normalizedStatus === "running"
+ ? "primary"
+ : "secondary";
+ const label = normalizedStatus.charAt(0).toUpperCase() + normalizedStatus.slice(1);
+ return `
${escapeHtml(label)} `;
+}
+
+function getWorkflowRunnerLabel(workflow) {
+ if (!workflow || typeof workflow !== "object") {
+ return "";
+ }
+
+ if (workflow.runner_type === "agent") {
+ const selectedAgent = workflow.selected_agent && typeof workflow.selected_agent === "object"
+ ? workflow.selected_agent
+ : {};
+ const label = normalizeText(selectedAgent.display_name || selectedAgent.name) || "Selected agent";
+ return selectedAgent.is_global ? `${label} (Global Agent)` : `${label} (Personal Agent)`;
+ }
+
+ const modelBindingSummary = workflow.model_binding_summary && typeof workflow.model_binding_summary === "object"
+ ? workflow.model_binding_summary
+ : {};
+ return normalizeText(modelBindingSummary.label) || "Default app model";
+}
+
+function getWorkflowTriggerLabel(workflow) {
+ if (!workflow || typeof workflow !== "object") {
+ return "Manual";
+ }
+
+ if (workflow.trigger_type !== "interval") {
+ return "Manual";
+ }
+
+ const schedule = workflow.schedule && typeof workflow.schedule === "object" ? workflow.schedule : {};
+ const value = Number(schedule.value || 0);
+ const unit = normalizeText(schedule.unit) || "minutes";
+ return `Every ${value} ${unit}`;
+}
+
+function getWorkflowAlertLabel(workflow) {
+ const priority = normalizeText(workflow?.alert_priority).toLowerCase();
+ if (!priority || priority === "none") {
+ return "Off";
+ }
+
+ return `${priority.charAt(0).toUpperCase()}${priority.slice(1)} priority`;
+}
+
+function buildWorkflowSearchText(workflow) {
+ return [
+ workflow.name,
+ workflow.description,
+ workflow.task_prompt,
+ getWorkflowRunnerLabel(workflow),
+ getWorkflowTriggerLabel(workflow),
+ getWorkflowAlertLabel(workflow),
+ ].map((value) => normalizeText(value).toLowerCase()).join(" ");
+}
+
+function getCustomEndpointOptions() {
+ const endpointGroups = [
+ {
+ endpoints: Array.isArray(window.globalModelEndpoints) ? window.globalModelEndpoints : [],
+ scope: "global",
+ scopeLabel: "Global",
+ },
+ {
+ endpoints: Array.isArray(window.workspaceModelEndpoints) ? window.workspaceModelEndpoints : [],
+ scope: "user",
+ scopeLabel: "Workspace",
+ },
+ ];
+
+ const options = [];
+
+ endpointGroups.forEach((group) => {
+ group.endpoints.forEach((endpoint) => {
+ if (!endpoint || endpoint.enabled === false) {
+ return;
+ }
+
+ const enabledModels = Array.isArray(endpoint.models)
+ ? endpoint.models.filter((model) => model && model.enabled !== false)
+ : [];
+
+ if (!enabledModels.length) {
+ return;
+ }
+
+ options.push({
+ ...endpoint,
+ models: enabledModels,
+ scope: group.scope,
+ scopeLabel: group.scopeLabel,
+ });
+ });
+ });
+
+ return options;
+}
+
+function getEndpointDisplayName(endpoint) {
+ const endpointName = normalizeText(endpoint?.name) || "Unnamed Endpoint";
+ const scopeLabel = normalizeText(endpoint?.scopeLabel) || "Global";
+ return `${scopeLabel}: ${endpointName}`;
+}
+
+function getModelDisplayName(model) {
+ return normalizeText(model?.displayName || model?.deploymentName || model?.modelName || model?.name || model?.id) || "Unnamed Model";
+}
+
+function getAgentOptionKey(agent) {
+ const scope = agent?.is_global ? "global" : "personal";
+ return `${scope}:${normalizeText(agent?.id || agent?.name)}`;
+}
+
+function getSelectedAgentOption() {
+ const selectedKey = normalizeText(workflowAgentSelect?.value);
+ return agentOptions.find((agent) => getAgentOptionKey(agent) === selectedKey) || null;
+}
+
+function getSelectedEndpointOption() {
+ const endpointId = normalizeText(workflowModelEndpointSelect?.value);
+ return getCustomEndpointOptions().find((endpoint) => normalizeText(endpoint.id) === endpointId) || null;
+}
+
+function refreshWorkflowSummary(items) {
+ if (!workflowsSummary) {
+ return;
+ }
+
+ const totalCount = workflows.length;
+ const scheduledCount = workflows.filter((workflow) => workflow.trigger_type === "interval").length;
+ const activeCount = workflows.filter((workflow) => workflow.trigger_type === "interval" && workflow.is_enabled).length;
+ const visibleCount = items.length;
+
+ workflowsSummary.textContent = `${visibleCount} shown of ${totalCount} workflows. ${scheduledCount} scheduled, ${activeCount} active.`;
+}
+
+function renderWorkflowEmptyState(message) {
+ if (!workflowsTableBody) {
+ return;
+ }
+
+ workflowsTableBody.innerHTML = `
+
+ ${escapeHtml(message)}
+
+ `;
+}
+
+function renderWorkflowTable(items) {
+ if (!workflowsTableBody) {
+ return;
+ }
+
+ if (!items.length) {
+ renderWorkflowEmptyState(workflows.length ? "No workflows match the current search." : "No workflows created yet.");
+ refreshWorkflowSummary(items);
+ return;
+ }
+
+ workflowsTableBody.innerHTML = items.map((workflow) => {
+ const workflowName = escapeHtml(normalizeText(workflow.name) || "Untitled Workflow");
+ const description = escapeHtml(truncateDescription(normalizeText(workflow.description), 120));
+ const runnerLabel = escapeHtml(getWorkflowRunnerLabel(workflow));
+ const triggerLabel = escapeHtml(getWorkflowTriggerLabel(workflow));
+ const lastRunStatus = workflow.last_run_status ? buildStatusBadge(workflow.last_run_status) : '
Never run ';
+ const lastRunAt = workflow.last_run_at
+ ? `
${escapeHtml(formatDateTime(workflow.last_run_at))}
`
+ : "";
+ const lastRunPreview = normalizeText(workflow.last_run_response_preview)
+ ? `
${escapeHtml(truncateDescription(workflow.last_run_response_preview, 160))}
`
+ : normalizeText(workflow.last_run_error)
+ ? `
${escapeHtml(truncateDescription(workflow.last_run_error, 120))}
`
+ : "";
+ const nextRunMeta = workflow.trigger_type === "interval" && workflow.next_run_at
+ ? `
Next run: ${escapeHtml(formatDateTime(workflow.next_run_at))}
`
+ : "";
+ const alertMeta = `
Alert: ${escapeHtml(getWorkflowAlertLabel(workflow))}
`;
+ const conversationMeta = workflow.conversation_id
+ ? '
Conversation ready
'
+ : "";
+ const disabledMeta = workflow.trigger_type === "interval" && !workflow.is_enabled
+ ? '
Scheduled runs are paused.
'
+ : "";
+ const runnerMeta = workflow.runner_type === "agent"
+ ? '
Uses your selected agent configuration.
'
+ : '
Uses direct model execution.
';
+ const runDisabled = normalizeText(workflow.status).toLowerCase() === "running";
+
+ return `
+
+
+ ${workflowName}
+ ${description ? `${description}
` : ""}
+ ${conversationMeta}
+
+
+ ${runnerLabel}
+ ${runnerMeta}
+
+
+ ${escapeHtml(triggerLabel)}
+ ${alertMeta}
+ ${disabledMeta}
+ ${nextRunMeta}
+
+
+ ${lastRunStatus}
+ ${lastRunAt}
+ ${lastRunPreview}
+
+
+
+ ${runDisabled ? "Running..." : "Run"}
+ History
+ Edit
+ Delete
+
+
+
+ `;
+ }).join("");
+
+ refreshWorkflowSummary(items);
+}
+
+function filterWorkflows() {
+ const searchTerm = normalizeText(workflowsSearchInput?.value).toLowerCase();
+ if (!searchTerm) {
+ renderWorkflowTable(workflows);
+ return;
+ }
+
+ const filteredWorkflows = workflows.filter((workflow) => buildWorkflowSearchText(workflow).includes(searchTerm));
+ renderWorkflowTable(filteredWorkflows);
+}
+
+async function loadAgentOptions(forceRefresh = false) {
+ if (agentsLoaded && !forceRefresh) {
+ return agentOptions;
+ }
+
+ try {
+ const response = await fetch("/api/user/agents", {
+ credentials: "same-origin",
+ });
+ const data = await response.json().catch(() => null);
+
+ if (!response.ok) {
+ throw new Error((data && typeof data === "object" && !Array.isArray(data) ? data.error : "") || "Unable to load agents right now.");
+ }
+
+ agentOptions = Array.isArray(data)
+ ? data
+ : Array.isArray(data?.agents)
+ ? data.agents
+ : [];
+ agentsLoaded = true;
+ } catch (error) {
+ agentOptions = [];
+ agentsLoaded = false;
+ console.error("Failed to load workflow agents", error);
+ }
+
+ return agentOptions;
+}
+
+function populateAgentSelect(selectedAgent = null) {
+ if (!workflowAgentSelect) {
+ return;
+ }
+
+ const selectedAgentKey = selectedAgent ? getAgentOptionKey(selectedAgent) : "";
+ const options = [...agentOptions].sort((left, right) => {
+ const leftLabel = normalizeText(left.display_name || left.name).toLowerCase();
+ const rightLabel = normalizeText(right.display_name || right.name).toLowerCase();
+ return leftLabel.localeCompare(rightLabel);
+ });
+
+ workflowAgentSelect.innerHTML = "";
+
+ if (!options.length && !selectedAgent) {
+ workflowAgentSelect.innerHTML = '
No agents available ';
+ workflowAgentSelect.disabled = true;
+ if (workflowAgentHelp) {
+ workflowAgentHelp.textContent = "No agents are currently available for workflow selection.";
+ }
+ return;
+ }
+
+ options.forEach((agent) => {
+ const option = document.createElement("option");
+ option.value = getAgentOptionKey(agent);
+ option.textContent = `${normalizeText(agent.display_name || agent.name) || "Unnamed Agent"}${agent.is_global ? " (Global)" : ""}`;
+ if (option.value === selectedAgentKey) {
+ option.selected = true;
+ }
+ workflowAgentSelect.appendChild(option);
+ });
+
+ if (selectedAgent && !options.some((agent) => getAgentOptionKey(agent) === selectedAgentKey)) {
+ const fallbackOption = document.createElement("option");
+ fallbackOption.value = selectedAgentKey;
+ fallbackOption.textContent = `${normalizeText(selectedAgent.display_name || selectedAgent.name) || "Current Agent"} (Unavailable)`;
+ fallbackOption.selected = true;
+ workflowAgentSelect.appendChild(fallbackOption);
+ }
+
+ workflowAgentSelect.disabled = false;
+ if (workflowAgentHelp) {
+ workflowAgentHelp.textContent = options.length
+ ? "Choose a personal agent or a merged global agent."
+ : "This workflow references an agent that is no longer available.";
+ }
+}
+
+function refreshModelSourceOptions() {
+ if (!workflowModelSourceSelect) {
+ return;
+ }
+
+ const hasCustomEndpoints = getCustomEndpointOptions().length > 0;
+ const customOption = Array.from(workflowModelSourceSelect.options).find((option) => option.value === "custom");
+ if (customOption) {
+ customOption.disabled = !hasCustomEndpoints;
+ }
+
+ if (!hasCustomEndpoints && workflowModelSourceSelect.value === "custom") {
+ workflowModelSourceSelect.value = "default";
+ }
+}
+
+function populateEndpointSelect(selectedEndpointId = "") {
+ if (!workflowModelEndpointSelect) {
+ return;
+ }
+
+ const endpoints = getCustomEndpointOptions();
+ workflowModelEndpointSelect.innerHTML = "";
+
+ if (!endpoints.length) {
+ workflowModelEndpointSelect.innerHTML = '
No endpoints available ';
+ workflowModelEndpointSelect.disabled = true;
+ return;
+ }
+
+ endpoints.forEach((endpoint, index) => {
+ const option = document.createElement("option");
+ option.value = normalizeText(endpoint.id);
+ option.textContent = getEndpointDisplayName(endpoint);
+ if ((selectedEndpointId && option.value === selectedEndpointId) || (!selectedEndpointId && index === 0)) {
+ option.selected = true;
+ }
+ workflowModelEndpointSelect.appendChild(option);
+ });
+
+ workflowModelEndpointSelect.disabled = false;
+}
+
+function populateModelSelect(selectedEndpointId = "", selectedModelId = "") {
+ if (!workflowModelSelect) {
+ return;
+ }
+
+ const endpoint = getCustomEndpointOptions().find((item) => normalizeText(item.id) === selectedEndpointId) || getSelectedEndpointOption();
+ workflowModelSelect.innerHTML = "";
+
+ if (!endpoint) {
+ workflowModelSelect.innerHTML = '
No models available ';
+ workflowModelSelect.disabled = true;
+ return;
+ }
+
+ endpoint.models.forEach((model, index) => {
+ const modelId = normalizeText(model.id);
+ const option = document.createElement("option");
+ option.value = modelId;
+ option.textContent = getModelDisplayName(model);
+ if ((selectedModelId && modelId === selectedModelId) || (!selectedModelId && index === 0)) {
+ option.selected = true;
+ }
+ workflowModelSelect.appendChild(option);
+ });
+
+ workflowModelSelect.disabled = false;
+}
+
+function updateModelHelpText() {
+ if (!workflowModelHelp) {
+ return;
+ }
+
+ const source = normalizeText(workflowModelSourceSelect?.value) || "default";
+ if (source === "default") {
+ const currentLabel = normalizeText(currentEditingWorkflow?.model_binding_summary?.label);
+ workflowModelHelp.textContent = currentLabel || "The default app model follows your admin-configured default selection or legacy GPT settings.";
+ return;
+ }
+
+ const endpoint = getSelectedEndpointOption();
+ const modelId = normalizeText(workflowModelSelect?.value);
+ const model = endpoint?.models?.find((candidate) => normalizeText(candidate.id) === modelId);
+
+ if (!endpoint || !model) {
+ workflowModelHelp.textContent = "Choose an enabled endpoint and model for this workflow.";
+ return;
+ }
+
+ workflowModelHelp.textContent = `${getEndpointDisplayName(endpoint)} / ${getModelDisplayName(model)}`;
+}
+
+function updateRunnerFields() {
+ const runnerType = normalizeText(workflowRunnerTypeSelect?.value) || "model";
+ const useAgent = runnerType === "agent";
+ const useCustomModel = normalizeText(workflowModelSourceSelect?.value) === "custom";
+
+ setElementVisibility(workflowAgentFields, useAgent);
+ setElementVisibility(workflowModelFields, !useAgent);
+ setElementVisibility(workflowModelEndpointGroup, !useAgent && useCustomModel);
+ setElementVisibility(workflowModelGroup, !useAgent && useCustomModel);
+
+ if (useAgent) {
+ populateAgentSelect(currentEditingWorkflow?.selected_agent || null);
+ } else {
+ refreshModelSourceOptions();
+ populateEndpointSelect(normalizeText(currentEditingWorkflow?.model_endpoint_id));
+ populateModelSelect(normalizeText(workflowModelEndpointSelect?.value), normalizeText(currentEditingWorkflow?.model_id));
+ updateModelHelpText();
+ }
+}
+
+function updateScheduleConstraints() {
+ if (!workflowScheduleValueInput || !workflowScheduleUnitSelect) {
+ return;
+ }
+
+ const unit = normalizeText(workflowScheduleUnitSelect.value) || "seconds";
+ const maxValue = unit === "hours" ? 24 : 59;
+ workflowScheduleValueInput.max = String(maxValue);
+
+ const currentValue = Number(workflowScheduleValueInput.value || 0);
+ if (currentValue > maxValue) {
+ workflowScheduleValueInput.value = String(maxValue);
+ }
+}
+
+function updateTriggerFields() {
+ const triggerType = normalizeText(workflowTriggerTypeSelect?.value) || "manual";
+ const isInterval = triggerType === "interval";
+
+ setElementVisibility(workflowScheduleValueGroup, isInterval);
+ setElementVisibility(workflowScheduleUnitGroup, isInterval);
+ setElementVisibility(workflowEnabledGroup, isInterval);
+
+ if (workflowEnabledToggle && !isInterval) {
+ workflowEnabledToggle.checked = true;
+ }
+
+ if (workflowTriggerHelp) {
+ workflowTriggerHelp.textContent = isInterval
+ ? "Interval workflows are picked up by the scheduler when the next run time is due."
+ : "Manual workflows run only when you trigger them from the workspace.";
+ }
+
+ updateScheduleConstraints();
+}
+
+function resetWorkflowForm() {
+ currentEditingWorkflow = null;
+
+ if (workflowForm) {
+ workflowForm.reset();
+ }
+ if (workflowIdInput) {
+ workflowIdInput.value = "";
+ }
+ if (workflowNameInput) {
+ workflowNameInput.value = "";
+ }
+ if (workflowDescriptionInput) {
+ workflowDescriptionInput.value = "";
+ }
+ if (workflowTaskPromptInput) {
+ workflowTaskPromptInput.value = "";
+ }
+ if (workflowRunnerTypeSelect) {
+ workflowRunnerTypeSelect.value = "model";
+ }
+ if (workflowModelSourceSelect) {
+ workflowModelSourceSelect.value = "default";
+ }
+ if (workflowTriggerTypeSelect) {
+ workflowTriggerTypeSelect.value = "manual";
+ }
+ if (workflowScheduleValueInput) {
+ workflowScheduleValueInput.value = "10";
+ }
+ if (workflowScheduleUnitSelect) {
+ workflowScheduleUnitSelect.value = "seconds";
+ }
+ if (workflowEnabledToggle) {
+ workflowEnabledToggle.checked = true;
+ }
+ if (workflowAlertPrioritySelect) {
+ workflowAlertPrioritySelect.value = "none";
+ }
+ if (workflowSaveBtn) {
+ workflowSaveBtn.disabled = false;
+ workflowSaveBtn.textContent = "Save Workflow";
+ }
+ if (workflowModalLabel) {
+ workflowModalLabel.textContent = "Create Workflow";
+ }
+
+ populateAgentSelect(null);
+ refreshModelSourceOptions();
+ populateEndpointSelect("");
+ populateModelSelect(normalizeText(workflowModelEndpointSelect?.value), "");
+ updateRunnerFields();
+ updateTriggerFields();
+}
+
+async function openWorkflowModal(workflow = null) {
+ if (!workflowModal) {
+ return;
+ }
+
+ await loadAgentOptions(true);
+ resetWorkflowForm();
+ currentEditingWorkflow = workflow;
+
+ if (workflow) {
+ if (workflowIdInput) {
+ workflowIdInput.value = normalizeText(workflow.id);
+ }
+ if (workflowNameInput) {
+ workflowNameInput.value = normalizeText(workflow.name);
+ }
+ if (workflowDescriptionInput) {
+ workflowDescriptionInput.value = normalizeText(workflow.description);
+ }
+ if (workflowTaskPromptInput) {
+ workflowTaskPromptInput.value = normalizeText(workflow.task_prompt);
+ }
+ if (workflowRunnerTypeSelect) {
+ workflowRunnerTypeSelect.value = normalizeText(workflow.runner_type) || "model";
+ }
+ if (workflowTriggerTypeSelect) {
+ workflowTriggerTypeSelect.value = normalizeText(workflow.trigger_type) || "manual";
+ }
+ if (workflowEnabledToggle) {
+ workflowEnabledToggle.checked = workflow.is_enabled !== false;
+ }
+ if (workflowAlertPrioritySelect) {
+ workflowAlertPrioritySelect.value = normalizeText(workflow.alert_priority).toLowerCase() || "none";
+ }
+ if (workflowScheduleValueInput) {
+ workflowScheduleValueInput.value = String(workflow.schedule?.value || 10);
+ }
+ if (workflowScheduleUnitSelect) {
+ workflowScheduleUnitSelect.value = normalizeText(workflow.schedule?.unit) || "seconds";
+ }
+ if (workflowModalLabel) {
+ workflowModalLabel.textContent = "Edit Workflow";
+ }
+
+ if (workflow.runner_type === "agent") {
+ populateAgentSelect(workflow.selected_agent || null);
+ } else {
+ const useCustomModel = Boolean(normalizeText(workflow.model_endpoint_id) && normalizeText(workflow.model_id));
+ if (workflowModelSourceSelect) {
+ workflowModelSourceSelect.value = useCustomModel ? "custom" : "default";
+ }
+ refreshModelSourceOptions();
+ populateEndpointSelect(normalizeText(workflow.model_endpoint_id));
+ populateModelSelect(normalizeText(workflow.model_endpoint_id || workflowModelEndpointSelect?.value), normalizeText(workflow.model_id));
+ }
+ }
+
+ updateRunnerFields();
+ updateTriggerFields();
+ workflowModal.show();
+}
+
+function buildWorkflowPayload() {
+ const runnerType = normalizeText(workflowRunnerTypeSelect?.value) || "model";
+ const triggerType = normalizeText(workflowTriggerTypeSelect?.value) || "manual";
+ const payload = {
+ id: normalizeText(workflowIdInput?.value),
+ name: normalizeText(workflowNameInput?.value),
+ description: normalizeText(workflowDescriptionInput?.value),
+ task_prompt: normalizeText(workflowTaskPromptInput?.value),
+ runner_type: runnerType,
+ trigger_type: triggerType,
+ alert_priority: normalizeText(workflowAlertPrioritySelect?.value).toLowerCase() || "none",
+ is_enabled: triggerType === "interval" ? Boolean(workflowEnabledToggle?.checked) : true,
+ schedule: {},
+ selected_agent: {},
+ model_endpoint_id: "",
+ model_id: "",
+ };
+
+ if (!payload.name) {
+ throw new Error("Workflow name is required.");
+ }
+ if (!payload.task_prompt) {
+ throw new Error("Task prompt is required.");
+ }
+
+ if (runnerType === "agent") {
+ const selectedAgent = getSelectedAgentOption();
+ if (!selectedAgent) {
+ throw new Error("Select an agent for this workflow.");
+ }
+ payload.selected_agent = {
+ id: normalizeText(selectedAgent.id),
+ name: normalizeText(selectedAgent.name),
+ is_global: Boolean(selectedAgent.is_global),
+ };
+ } else if (normalizeText(workflowModelSourceSelect?.value) === "custom") {
+ const endpointId = normalizeText(workflowModelEndpointSelect?.value);
+ const modelId = normalizeText(workflowModelSelect?.value);
+ if (!endpointId || !modelId) {
+ throw new Error("Select both an endpoint and a model for this workflow.");
+ }
+ payload.model_endpoint_id = endpointId;
+ payload.model_id = modelId;
+ }
+
+ if (triggerType === "interval") {
+ const scheduleValue = Number(workflowScheduleValueInput?.value || 0);
+ const scheduleUnit = normalizeText(workflowScheduleUnitSelect?.value) || "seconds";
+ if (!Number.isInteger(scheduleValue) || scheduleValue < 1) {
+ throw new Error("Schedule value must be at least 1.");
+ }
+ payload.schedule = {
+ value: scheduleValue,
+ unit: scheduleUnit,
+ };
+ }
+
+ return payload;
+}
+
+async function saveWorkflow(event) {
+ event.preventDefault();
+
+ if (!workflowSaveBtn) {
+ return;
+ }
+
+ let payload;
+ try {
+ payload = buildWorkflowPayload();
+ } catch (error) {
+ showToast(escapeHtml(error.message || "Unable to save workflow."), "danger");
+ return;
+ }
+
+ workflowSaveBtn.disabled = true;
+ workflowSaveBtn.textContent = "Saving...";
+
+ try {
+ const response = await fetch("/api/user/workflows", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: "same-origin",
+ body: JSON.stringify(payload),
+ });
+
+ const data = await response.json().catch(() => ({}));
+ if (!response.ok) {
+ throw new Error(data.error || "Unable to save workflow right now.");
+ }
+
+ workflowModal?.hide();
+ showToast("Workflow saved.", "success");
+ await fetchUserWorkflows();
+ } catch (error) {
+ showToast(escapeHtml(error.message || "Unable to save workflow right now."), "danger");
+ } finally {
+ workflowSaveBtn.disabled = false;
+ workflowSaveBtn.textContent = "Save Workflow";
+ }
+}
+
+function renderHistoryLoading() {
+ if (!workflowHistoryBody) {
+ return;
+ }
+
+ workflowHistoryBody.innerHTML = `
+
+
+ Loading...
+ Loading run history...
+
+
+ `;
+}
+
+function renderRunHistory(runs) {
+ if (!workflowHistoryBody) {
+ return;
+ }
+
+ if (!Array.isArray(runs) || !runs.length) {
+ workflowHistoryBody.innerHTML = `
+
+ No workflow runs yet.
+
+ `;
+ return;
+ }
+
+ workflowHistoryBody.innerHTML = runs.map((run) => {
+ const conversationId = normalizeText(run.conversation_id);
+ const conversationUrl = buildWorkflowConversationUrl(conversationId);
+ const activityUrl = buildWorkflowActivityUrl(conversationId, normalizeText(run.id), currentHistoryWorkflowId);
+ const details = normalizeText(run.error)
+ ? `
${escapeHtml(run.error)}
`
+ : normalizeText(run.response_preview)
+ ? `
${escapeHtml(run.response_preview)}
`
+ : '
No preview available.
';
+ const conversationLink = conversationUrl
+ ? `
+
+
${escapeHtml(conversationId)}
+ `
+ : '
Not created yet.
';
+
+ return `
+
+ ${buildStatusBadge(run.status)}
+
+ ${escapeHtml(formatDateTime(run.started_at) || "-")}
+ ${run.completed_at ? `Completed ${escapeHtml(formatDateTime(run.completed_at))}
` : ""}
+
+ ${escapeHtml(normalizeText(run.trigger_source) || "manual")}
+ ${details}
+ ${conversationLink}
+
+ `;
+ }).join("");
+}
+
+async function openHistoryModalForWorkflow(workflow) {
+ if (!workflow || !workflowHistoryModal) {
+ return;
+ }
+
+ currentHistoryWorkflowId = normalizeText(workflow.id);
+ if (workflowHistoryModalLabel) {
+ workflowHistoryModalLabel.textContent = `${normalizeText(workflow.name) || "Workflow"} Run History`;
+ }
+ if (workflowHistoryConversationId) {
+ workflowHistoryConversationId.textContent = normalizeText(workflow.conversation_id) || "Not created yet.";
+ }
+ updateWorkflowConversationLink(workflowHistoryConversationLink, workflow.conversation_id);
+ renderHistoryLoading();
+ workflowHistoryModal.show();
+
+ try {
+ const response = await fetch(`/api/user/workflows/${encodeURIComponent(currentHistoryWorkflowId)}/runs`, {
+ credentials: "same-origin",
+ });
+ const data = await response.json().catch(() => ({}));
+ if (!response.ok) {
+ throw new Error(data.error || "Unable to load workflow history.");
+ }
+ renderRunHistory(data.runs || []);
+ } catch (error) {
+ workflowHistoryBody.innerHTML = `
+
+ ${escapeHtml(error.message || "Unable to load workflow history.")}
+
+ `;
+ }
+}
+
+async function runWorkflow(workflow, button) {
+ if (!workflow) {
+ return;
+ }
+
+ const originalText = button?.textContent || "Run";
+ if (button) {
+ button.disabled = true;
+ button.textContent = "Running...";
+ }
+
+ try {
+ const response = await fetch(`/api/user/workflows/${encodeURIComponent(workflow.id)}/run`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: "same-origin",
+ });
+ const data = await response.json().catch(() => ({}));
+
+ if (!response.ok || data.success === false) {
+ throw new Error(data.run?.error || data.error || "Workflow run failed.");
+ }
+
+ showToast("Workflow run completed.", "success");
+ window.dispatchEvent(new CustomEvent("workflow-alert-refresh-requested"));
+ await fetchUserWorkflows();
+
+ if (currentHistoryWorkflowId && currentHistoryWorkflowId === normalizeText(workflow.id)) {
+ const refreshedWorkflow = workflows.find((item) => normalizeText(item.id) === currentHistoryWorkflowId) || workflow;
+ await openHistoryModalForWorkflow(refreshedWorkflow);
+ }
+ } catch (error) {
+ showToast(escapeHtml(error.message || "Workflow run failed."), "danger");
+ await fetchUserWorkflows();
+ } finally {
+ if (button) {
+ button.disabled = false;
+ button.textContent = originalText;
+ }
+ }
+}
+
+function promptDeleteWorkflow(workflow) {
+ if (!workflow || !workflowDeleteModal) {
+ return;
+ }
+
+ workflowPendingDelete = workflow;
+ if (workflowDeleteName) {
+ workflowDeleteName.textContent = normalizeText(workflow.name) || "this workflow";
+ }
+ workflowDeleteModal.show();
+}
+
+async function deleteWorkflow() {
+ if (!workflowPendingDelete || !workflowDeleteConfirmBtn) {
+ return;
+ }
+
+ workflowDeleteConfirmBtn.disabled = true;
+ workflowDeleteConfirmBtn.textContent = "Deleting...";
+
+ try {
+ const response = await fetch(`/api/user/workflows/${encodeURIComponent(workflowPendingDelete.id)}`, {
+ method: "DELETE",
+ credentials: "same-origin",
+ });
+ const data = await response.json().catch(() => ({}));
+ if (!response.ok) {
+ throw new Error(data.error || "Unable to delete workflow right now.");
+ }
+
+ workflowDeleteModal?.hide();
+ showToast("Workflow deleted.", "success");
+ workflowPendingDelete = null;
+ await fetchUserWorkflows();
+ } catch (error) {
+ showToast(escapeHtml(error.message || "Unable to delete workflow right now."), "danger");
+ } finally {
+ workflowDeleteConfirmBtn.disabled = false;
+ workflowDeleteConfirmBtn.textContent = "Delete Workflow";
+ }
+}
+
+function findWorkflowById(workflowId) {
+ return workflows.find((workflow) => normalizeText(workflow.id) === normalizeText(workflowId)) || null;
+}
+
+function handleWorkflowTableClick(event) {
+ const button = event.target.closest("button[data-action]");
+ if (!button) {
+ return;
+ }
+
+ const workflow = findWorkflowById(button.getAttribute("data-workflow-id"));
+ if (!workflow) {
+ return;
+ }
+
+ const action = button.getAttribute("data-action");
+ if (action === "run") {
+ runWorkflow(workflow, button);
+ } else if (action === "history") {
+ openHistoryModalForWorkflow(workflow);
+ } else if (action === "edit") {
+ openWorkflowModal(workflow);
+ } else if (action === "delete") {
+ promptDeleteWorkflow(workflow);
+ }
+}
+
+async function fetchUserWorkflows() {
+ if (!workflowsTableBody) {
+ return [];
+ }
+
+ workflowsTableBody.innerHTML = `
+
+
+ Loading...
+ Loading workflows...
+
+
+ `;
+
+ try {
+ const response = await fetch("/api/user/workflows", {
+ credentials: "same-origin",
+ });
+ const data = await response.json().catch(() => ({}));
+
+ if (!response.ok) {
+ throw new Error(data.error || "Unable to load workflows right now.");
+ }
+
+ workflows = Array.isArray(data.workflows) ? data.workflows : [];
+ filterWorkflows();
+ return workflows;
+ } catch (error) {
+ workflows = [];
+ renderWorkflowEmptyState(error.message || "Unable to load workflows right now.");
+ refreshWorkflowSummary([]);
+ return [];
+ }
+}
+
+function initializeWorkflowEvents() {
+ if (!workflowsTableBody) {
+ return;
+ }
+
+ createWorkflowBtn?.addEventListener("click", () => {
+ openWorkflowModal();
+ });
+ workflowsSearchInput?.addEventListener("input", filterWorkflows);
+ workflowsTableBody.addEventListener("click", handleWorkflowTableClick);
+ workflowForm?.addEventListener("submit", saveWorkflow);
+ workflowDeleteConfirmBtn?.addEventListener("click", deleteWorkflow);
+ workflowRunnerTypeSelect?.addEventListener("change", updateRunnerFields);
+ workflowModelSourceSelect?.addEventListener("change", updateRunnerFields);
+ workflowModelEndpointSelect?.addEventListener("change", () => {
+ populateModelSelect(normalizeText(workflowModelEndpointSelect.value), "");
+ updateModelHelpText();
+ });
+ workflowModelSelect?.addEventListener("change", updateModelHelpText);
+ workflowTriggerTypeSelect?.addEventListener("change", updateTriggerFields);
+ workflowScheduleUnitSelect?.addEventListener("change", updateScheduleConstraints);
+ workflowModalEl?.addEventListener("hidden.bs.modal", resetWorkflowForm);
+ workflowDeleteModalEl?.addEventListener("hidden.bs.modal", () => {
+ workflowPendingDelete = null;
+ if (workflowDeleteConfirmBtn) {
+ workflowDeleteConfirmBtn.disabled = false;
+ workflowDeleteConfirmBtn.textContent = "Delete Workflow";
+ }
+ });
+}
+
+window.fetchUserWorkflows = fetchUserWorkflows;
+
+initializeWorkflowEvents();
\ No newline at end of file
diff --git a/application/single_app/static/json/schemas/cosmos_query.definition.json b/application/single_app/static/json/schemas/cosmos_query.definition.json
new file mode 100644
index 000000000..661a94401
--- /dev/null
+++ b/application/single_app/static/json/schemas/cosmos_query.definition.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "./plugin.definition.schema.json",
+ "allowedAuthTypes": [
+ "identity",
+ "key"
+ ]
+}
\ No newline at end of file
diff --git a/application/single_app/static/json/schemas/cosmos_query_plugin.additional_settings.schema.json b/application/single_app/static/json/schemas/cosmos_query_plugin.additional_settings.schema.json
new file mode 100644
index 000000000..7ad3685ab
--- /dev/null
+++ b/application/single_app/static/json/schemas/cosmos_query_plugin.additional_settings.schema.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Cosmos Query Plugin Additional Settings",
+ "type": "object",
+ "properties": {
+ "database_name": {
+ "type": "string",
+ "description": "Azure Cosmos DB database name."
+ },
+ "container_name": {
+ "type": "string",
+ "description": "Azure Cosmos DB container name."
+ },
+ "partition_key_path": {
+ "type": "string",
+ "description": "Partition key path for the configured container, for example /tenantId or /id."
+ },
+ "field_hints": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "default": [],
+ "description": "Optional field names or document properties the model should prefer when querying this container."
+ },
+ "max_items": {
+ "type": "integer",
+ "default": 100,
+ "minimum": 1,
+ "maximum": 1000,
+ "description": "Maximum number of items returned by a query."
+ },
+ "timeout": {
+ "type": "integer",
+ "default": 30,
+ "minimum": 1,
+ "maximum": 120,
+ "description": "Absolute timeout in seconds for Cosmos client requests."
+ }
+ },
+ "required": ["database_name", "container_name", "partition_key_path"],
+ "additionalProperties": false
+}
\ No newline at end of file
diff --git a/application/single_app/static/json/schemas/document_search.definition.json b/application/single_app/static/json/schemas/document_search.definition.json
new file mode 100644
index 000000000..14221a9bd
--- /dev/null
+++ b/application/single_app/static/json/schemas/document_search.definition.json
@@ -0,0 +1,5 @@
+{
+ "allowedAuthTypes": [
+ "NoAuth"
+ ]
+}
\ No newline at end of file
diff --git a/application/single_app/static/json/schemas/document_search_plugin.additional_settings.schema.json b/application/single_app/static/json/schemas/document_search_plugin.additional_settings.schema.json
new file mode 100644
index 000000000..d147ddd5f
--- /dev/null
+++ b/application/single_app/static/json/schemas/document_search_plugin.additional_settings.schema.json
@@ -0,0 +1,52 @@
+{
+ "title": "Document Search Settings",
+ "description": "Default search, windowing, and summarization settings for SimpleChat internal document search.",
+ "type": "object",
+ "properties": {
+ "default_doc_scope": {
+ "type": "string",
+ "enum": [
+ "all",
+ "personal",
+ "group",
+ "public"
+ ],
+ "default": "all"
+ },
+ "default_top_n": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 500,
+ "default": 12
+ },
+ "default_window_unit": {
+ "type": "string",
+ "enum": [
+ "pages",
+ "chunks"
+ ],
+ "default": "pages"
+ },
+ "default_window_size": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "default_window_percent": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "default_focus_instructions": {
+ "type": "string"
+ },
+ "default_window_target_length": {
+ "type": "string",
+ "default": "2 pages"
+ },
+ "default_final_target_length": {
+ "type": "string",
+ "default": "2 pages"
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/single_app/static/json/schemas/search.definition.json b/application/single_app/static/json/schemas/search.definition.json
new file mode 100644
index 000000000..14221a9bd
--- /dev/null
+++ b/application/single_app/static/json/schemas/search.definition.json
@@ -0,0 +1,5 @@
+{
+ "allowedAuthTypes": [
+ "NoAuth"
+ ]
+}
\ No newline at end of file
diff --git a/application/single_app/static/json/schemas/search_plugin.additional_settings.schema.json b/application/single_app/static/json/schemas/search_plugin.additional_settings.schema.json
new file mode 100644
index 000000000..d147ddd5f
--- /dev/null
+++ b/application/single_app/static/json/schemas/search_plugin.additional_settings.schema.json
@@ -0,0 +1,52 @@
+{
+ "title": "Document Search Settings",
+ "description": "Default search, windowing, and summarization settings for SimpleChat internal document search.",
+ "type": "object",
+ "properties": {
+ "default_doc_scope": {
+ "type": "string",
+ "enum": [
+ "all",
+ "personal",
+ "group",
+ "public"
+ ],
+ "default": "all"
+ },
+ "default_top_n": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 500,
+ "default": 12
+ },
+ "default_window_unit": {
+ "type": "string",
+ "enum": [
+ "pages",
+ "chunks"
+ ],
+ "default": "pages"
+ },
+ "default_window_size": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "default_window_percent": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "default_focus_instructions": {
+ "type": "string"
+ },
+ "default_window_target_length": {
+ "type": "string",
+ "default": "2 pages"
+ },
+ "default_final_target_length": {
+ "type": "string",
+ "default": "2 pages"
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/single_app/static/json/schemas/simplechat.definition.json b/application/single_app/static/json/schemas/simplechat.definition.json
new file mode 100644
index 000000000..8da91f3eb
--- /dev/null
+++ b/application/single_app/static/json/schemas/simplechat.definition.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "./plugin.definition.schema.json",
+ "allowedAuthTypes": [
+ "user"
+ ]
+}
\ No newline at end of file
diff --git a/application/single_app/templates/_agent_modal.html b/application/single_app/templates/_agent_modal.html
index ef7fefc5c..45303db11 100644
--- a/application/single_app/templates/_agent_modal.html
+++ b/application/single_app/templates/_agent_modal.html
@@ -371,6 +371,30 @@
Available Actions