${fileContent}`; 170 | } 171 | 172 | const modal = new bootstrap.Modal(modalContainer); 173 | modal.show(); 174 | } 175 | 176 | export function getUrlParameter(name) { 177 | name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); 178 | const regex = new RegExp("[\\?&]" + name + "=([^]*)"); 179 | const results = regex.exec(location.search); 180 | return results === null 181 | ? "" 182 | : decodeURIComponent(results[1].replace(/\+/g, " ")); 183 | } 184 | 185 | document.addEventListener("DOMContentLoaded", function () { 186 | const tooltipTriggerList = [].slice.call( 187 | document.querySelectorAll('[data-bs-toggle="tooltip"]') 188 | ); 189 | tooltipTriggerList.forEach(function (tooltipTriggerEl) { 190 | new bootstrap.Tooltip(tooltipTriggerEl); 191 | }); 192 | }); 193 | 194 | if (imageGenBtn) { 195 | imageGenBtn.addEventListener("click", function () { 196 | this.classList.toggle("active"); 197 | 198 | const isImageGenEnabled = this.classList.contains("active"); 199 | const docBtn = document.getElementById("search-documents-btn"); 200 | const webBtn = document.getElementById("search-web-btn"); 201 | const fileBtn = document.getElementById("choose-file-btn"); 202 | 203 | if (isImageGenEnabled) { 204 | if (docBtn) { 205 | docBtn.disabled = true; 206 | docBtn.classList.remove("active"); 207 | } 208 | if (webBtn) { 209 | webBtn.disabled = true; 210 | webBtn.classList.remove("active"); 211 | } 212 | if (fileBtn) { 213 | fileBtn.disabled = true; 214 | fileBtn.classList.remove("active"); 215 | } 216 | } else { 217 | if (docBtn) docBtn.disabled = false; 218 | if (webBtn) webBtn.disabled = false; 219 | if (fileBtn) fileBtn.disabled = false; 220 | } 221 | }); 222 | } 223 | 224 | if (webSearchBtn) { 225 | webSearchBtn.addEventListener("click", function () { 226 | this.classList.toggle("active"); 227 | }); 228 | } 229 | 230 | if (chooseFileBtn) { 231 | chooseFileBtn.addEventListener("click", function () { 232 | const fileInput = document.getElementById("file-input"); 233 | if (fileInput) fileInput.click(); 234 | }); 235 | } 236 | 237 | if (fileInputEl) { 238 | fileInputEl.addEventListener("change", function () { 239 | const file = fileInputEl.files[0]; 240 | const fileBtn = document.getElementById("choose-file-btn"); 241 | const uploadBtn = document.getElementById("upload-btn"); 242 | if (!fileBtn || !uploadBtn) return; 243 | 244 | if (file) { 245 | fileBtn.classList.add("active"); 246 | fileBtn.querySelector(".file-btn-text").textContent = file.name; 247 | uploadBtn.style.display = "block"; 248 | cancelFileSelection.style.display = "inline"; 249 | } else { 250 | resetFileButton(); 251 | } 252 | }); 253 | } 254 | 255 | if (cancelFileSelection) { 256 | // Prevent the click from also triggering the "choose file" flow. 257 | cancelFileSelection.addEventListener("click", (event) => { 258 | event.stopPropagation(); 259 | resetFileButton(); 260 | }); 261 | } 262 | 263 | if (uploadBtn) { 264 | uploadBtn.addEventListener("click", () => { 265 | const fileInput = document.getElementById("file-input"); 266 | if (!fileInput) return; 267 | 268 | const file = fileInput.files[0]; 269 | if (!file) { 270 | showToast("Please select a file to upload.", "danger"); 271 | return; 272 | } 273 | 274 | if (!currentConversationId) { 275 | createNewConversation(() => { 276 | uploadFileToConversation(file); 277 | }); 278 | } else { 279 | uploadFileToConversation(file); 280 | } 281 | }); 282 | } 283 | -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-layout.js: -------------------------------------------------------------------------------- 1 | // static/js/chat/chat-layout.js 2 | 3 | const leftPane = document.getElementById('left-pane'); 4 | const rightPane = document.getElementById('right-pane'); 5 | const dockToggleButton = document.getElementById('dock-toggle-btn'); 6 | const splitContainer = document.getElementById('split-container'); // Might not be needed directly if targeting body 7 | 8 | // --- API Function for User Settings --- 9 | async function saveUserSetting(settingsToUpdate) { 10 | 11 | 12 | try { 13 | const response = await fetch('/api/user/settings', { 14 | method: 'POST', 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | // Add CSRF token header if you use CSRF protection 18 | }, 19 | body: JSON.stringify({ settings: settingsToUpdate }), // Send nested structure 20 | }); 21 | if (!response.ok) { 22 | console.error('Failed to save user settings:', response.statusText); 23 | const errorData = await response.json().catch(() => ({})); // Try to get error details 24 | console.error('Error details:', errorData); 25 | // Fallback to localStorage on API failure? 26 | // localStorage.setItem(key, JSON.stringify(value)); 27 | } else { 28 | console.log('User settings saved successfully via API for:', Object.keys(settingsToUpdate)); 29 | } 30 | } catch (error) { 31 | console.error('Error calling save settings API:', error); 32 | // Fallback to localStorage on network error? 33 | // localStorage.setItem(key, JSON.stringify(value)); 34 | } 35 | } 36 | 37 | async function loadUserSettings() { 38 | let settings = {}; 39 | 40 | try { 41 | const response = await fetch('/api/user/settings'); 42 | if (response.ok) { 43 | const data = await response.json(); 44 | settings = data.settings || {}; // Expect settings under a 'settings' key 45 | console.log('User settings loaded via API:', settings); 46 | } else { 47 | console.warn('Failed to load user settings via API:', response.statusText); 48 | // Optionally fallback to localStorage here too 49 | } 50 | } catch (error) { 51 | console.error('Error fetching user settings:', error); 52 | // Optionally fallback to localStorage here too 53 | } 54 | 55 | // Apply loaded settings 56 | currentLayout = settings[USER_SETTINGS_KEY_LAYOUT] || 'split'; // Default to 'split' 57 | currentSplitSizes = settings[USER_SETTINGS_KEY_SPLIT] || [25, 75]; // Default sizes 58 | 59 | console.log(`Applying initial layout: ${currentLayout}, sizes: ${currentSplitSizes}`); 60 | applyLayout(currentLayout, false); // Apply layout without saving again 61 | } 62 | 63 | 64 | // --- Split.js Initialization --- 65 | function initializeSplit() { 66 | if (splitInstance) { 67 | splitInstance.destroy(); // Destroy existing instance if any 68 | splitInstance = null; 69 | console.log('Split.js instance destroyed.'); 70 | } 71 | 72 | console.log('Initializing Split.js with sizes:', currentSplitSizes); 73 | splitInstance = Split(['#left-pane', '#right-pane'], { 74 | sizes: currentSplitSizes, // Use potentially loaded sizes 75 | minSize: [200, 350], // Min width in pixels [left, right] - adjust as needed 76 | gutterSize: 8, // Width of the draggable gutter 77 | cursor: 'col-resize', 78 | direction: 'horizontal', 79 | onDragEnd: function(sizes) { 80 | console.log('Split drag ended. New sizes:', sizes); 81 | currentSplitSizes = sizes; // Update current sizes 82 | // Debounce saving split sizes to avoid rapid API calls 83 | debounceSaveSplitSizes(sizes); 84 | }, 85 | elementStyle: (dimension, size, gutterSize) => ({ 86 | // Use flex-basis for sizing to work well with flex container 87 | 'flex-basis': `calc(${size}% - ${gutterSize}px)`, 88 | }), 89 | gutterStyle: (dimension, gutterSize) => ({ 90 | 'flex-basis': `${gutterSize}px`, 91 | }), 92 | }); 93 | console.log('Split.js initialized.'); 94 | } 95 | 96 | // Debounce function 97 | let debounceTimer; 98 | function debounceSaveSplitSizes(sizes) { 99 | clearTimeout(debounceTimer); 100 | debounceTimer = setTimeout(() => { 101 | saveUserSetting({ [USER_SETTINGS_KEY_SPLIT]: sizes }); 102 | }, 500); // Save 500ms after dragging stops 103 | } 104 | 105 | // --- Layout Switching Logic --- 106 | function applyLayout(layout, shouldSave = true) { 107 | currentLayout = layout; // Update global state 108 | 109 | // Remove previous layout classes 110 | document.body.classList.remove('layout-split', 'layout-docked', 'left-pane-hidden'); 111 | 112 | if (layout === 'docked') { 113 | console.log('Applying docked layout.'); 114 | document.body.classList.add('layout-docked'); 115 | if (splitInstance) { 116 | currentSplitSizes = splitInstance.getSizes(); // Save sizes before destroying 117 | splitInstance.destroy(); 118 | splitInstance = null; 119 | console.log('Split.js instance destroyed for docking.'); 120 | } 121 | // Ensure panes have correct styles applied by CSS (.layout-docked) 122 | leftPane.style.width = ''; // Let CSS handle width 123 | leftPane.style.flexBasis = ''; // Remove split.js style 124 | rightPane.style.marginLeft = ''; // Let CSS handle margin 125 | rightPane.style.width = ''; // Let CSS handle width 126 | rightPane.style.flexBasis = ''; // Remove split.js style 127 | 128 | } else { // 'split' layout 129 | console.log('Applying split layout.'); 130 | document.body.classList.add('layout-split'); // Use a class for split state too? Optional. 131 | // Re-initialize Split.js if it's not already running 132 | if (!splitInstance) { 133 | initializeSplit(); 134 | } 135 | // Remove fixed positioning styles if they were somehow manually added 136 | leftPane.style.position = ''; 137 | leftPane.style.top = ''; 138 | leftPane.style.left = ''; 139 | leftPane.style.height = ''; 140 | leftPane.style.zIndex = ''; 141 | rightPane.style.marginLeft = ''; 142 | rightPane.style.width = ''; 143 | } 144 | 145 | // Update toggle button icon/title 146 | updateDockToggleButton(); 147 | 148 | // Save the new layout preference 149 | if (shouldSave) { 150 | saveUserSetting({ [USER_SETTINGS_KEY_LAYOUT]: layout }); 151 | } 152 | } 153 | 154 | function toggleDocking() { 155 | const nextLayout = currentLayout === 'split' ? 'docked' : 'split'; 156 | applyLayout(nextLayout, true); // Apply and save 157 | } 158 | 159 | // Maybe add a function to toggle the docked sidebar visibility 160 | function toggleDockedSidebarVisibility() { 161 | if (currentLayout === 'docked') { 162 | document.body.classList.toggle('left-pane-hidden'); 163 | const isHidden = document.body.classList.contains('left-pane-hidden'); 164 | saveUserSetting({ 'dockedSidebarHidden': isHidden }); // Save visibility state if needed 165 | updateDockToggleButton(); // Update icon maybe 166 | } 167 | } 168 | 169 | function updateDockToggleButton() { 170 | const icon = dockToggleButton.querySelector('i'); 171 | if (!icon) return; // Add safety check 172 | 173 | if (currentLayout === 'docked') { 174 | icon.classList.remove('bi-layout-sidebar-inset'); 175 | icon.classList.add('bi-layout-sidebar-inset-reverse'); 176 | dockToggleButton.title = "Undock Sidebar (Split View)"; 177 | // NO MORE onclick assignment here 178 | // If you need the hide/show functionality, you might need a separate button 179 | // or add logic within the main toggleDocking based on state, 180 | // but avoid direct onclick reassignment here. 181 | 182 | } else { // 'split' layout 183 | icon.classList.remove('bi-layout-sidebar-inset-reverse'); 184 | icon.classList.add('bi-layout-sidebar-inset'); 185 | dockToggleButton.title = "Dock Sidebar Left"; 186 | // NO MORE onclick assignment here either 187 | } 188 | } 189 | 190 | 191 | // --- Event Listeners --- 192 | if (dockToggleButton) { 193 | dockToggleButton.addEventListener('click', toggleDocking); 194 | } else { 195 | console.error('Dock toggle button not found.'); 196 | } 197 | 198 | // --- Initial Load --- 199 | document.addEventListener('DOMContentLoaded', () => { 200 | loadUserSettings(); // Load settings and apply initial layout 201 | }); -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-loading-indicator.js: -------------------------------------------------------------------------------- 1 | // chat-loading.js 2 | 3 | export function showLoadingIndicator() { 4 | let loadingSpinner = document.getElementById("loading-spinner"); 5 | if (!loadingSpinner) { 6 | loadingSpinner = document.createElement("div"); 7 | loadingSpinner.id = "loading-spinner"; 8 | loadingSpinner.innerHTML = ` 9 |
Last Updated: January 30, 2025
18 | 19 |20 | Welcome to our AI-powered chat service. By using this service, 21 | you agree to follow this Acceptable Use Policy (AUP) to ensure 22 | a safe and respectful experience for all users. 23 |
24 | 25 |You must not use this service to:
27 |We respect your privacy. Please do not share personal, confidential, or sensitive information while using this service.
39 | 40 |42 | The AI assistant provides information based on patterns in data and 43 | should not be considered a replacement for professional advice (legal, 44 | medical, financial, etc.). Use AI-generated content responsibly. 45 |
46 | 47 |49 | Violations of this policy may result in temporary or permanent 50 | suspension of access to the service. 51 |
52 | 53 |55 | We may update this AUP from time to time. Continued use of the service 56 | after any modifications constitutes your acceptance of the new terms. 57 |
58 | 59 |60 | By using this service, you agree to this Acceptable Use Policy. 61 |
62 | 63 |38 | You are logged in but do not have the required permissions to access this application. 39 | Please submit a ticket to request access. 40 |
41 | {% else %} 42 |47 | Please sign in to continue. 48 |
49 | {% endif %} 50 | {% endif %} 51 |Name: | 13 |{{ user.get('name', 'N/A') }} | 14 |
---|---|
Email: | 17 |{{ user.get('preferred_username', 'N/A') }} | 18 |
Object ID: | 21 |{{ user.get('oid', 'N/A') }} | 22 |
Version: {{ config['VERSION'] }}
26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /artifacts/app_service_manifest/manifest-ms_graph.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/artifacts/app_service_manifest/manifest-ms_graph.json -------------------------------------------------------------------------------- /artifacts/architecture.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/artifacts/architecture.vsdx -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-conversation-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "