From b5b36f8afad6195fe8c50cb947ebb231fef7bc85 Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:39:05 -0400 Subject: [PATCH 01/57] Create timeharbor.code-workspace --- timeharbor.code-workspace | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 timeharbor.code-workspace diff --git a/timeharbor.code-workspace b/timeharbor.code-workspace new file mode 100644 index 0000000..ef9f5d2 --- /dev/null +++ b/timeharbor.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": "." + } + ] +} \ No newline at end of file From 529d5c5259517cee0480ea7ff298df8f484f129d Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:28:47 -0400 Subject: [PATCH 02/57] Add Ozwell AI writing assistant integration Introduces OzwellButton and OzwellModal components for AI-powered writing assistance, with context-aware autofill in ticket creation and other inputs. Adds settings page for Ozwell configuration, server methods for session and prompt management, and updates layout and tickets page to support Ozwell features. --- client/components/layout/MainLayout.html | 20 +- client/components/ozwell/OzwellButton.html | 9 + client/components/ozwell/OzwellButton.js | 82 +++ client/components/ozwell/OzwellModal.html | 107 ++++ client/components/ozwell/OzwellModal.js | 580 +++++++++++++++++++ client/components/settings/SettingsPage.html | 113 ++++ client/components/settings/SettingsPage.js | 140 +++++ client/components/tickets/TicketsPage.html | 112 ++-- client/components/tickets/TicketsPage.js | 96 ++- client/main.js | 10 + collections.js | 6 + server/main.js | 105 +++- server/methods/auth.js | 20 +- server/methods/ozwell.js | 375 ++++++++++++ server/methods/ozwellPrompts.js | 183 ++++++ 15 files changed, 1864 insertions(+), 94 deletions(-) create mode 100644 client/components/ozwell/OzwellButton.html create mode 100644 client/components/ozwell/OzwellButton.js create mode 100644 client/components/ozwell/OzwellModal.html create mode 100644 client/components/ozwell/OzwellModal.js create mode 100644 client/components/settings/SettingsPage.html create mode 100644 client/components/settings/SettingsPage.js create mode 100644 server/methods/ozwell.js create mode 100644 server/methods/ozwellPrompts.js diff --git a/client/components/layout/MainLayout.html b/client/components/layout/MainLayout.html index 15ae2c3..7ef7fa5 100644 --- a/client/components/layout/MainLayout.html +++ b/client/components/layout/MainLayout.html @@ -7,29 +7,33 @@

TimeHarbor

Home Teams Projects + Settings
- + {{#if logoutMessage}} -
- {{logoutMessage}} -
+
+ {{logoutMessage}} +
{{/if}} - +
{{> Template.dynamic template=main}}
+ + + {{> ozwellModal}} \ No newline at end of file diff --git a/client/components/ozwell/OzwellButton.html b/client/components/ozwell/OzwellButton.html new file mode 100644 index 0000000..972e83d --- /dev/null +++ b/client/components/ozwell/OzwellButton.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/client/components/ozwell/OzwellButton.js b/client/components/ozwell/OzwellButton.js new file mode 100644 index 0000000..484e1d0 --- /dev/null +++ b/client/components/ozwell/OzwellButton.js @@ -0,0 +1,82 @@ +import { Template } from 'meteor/templating'; + +// Import the HTML template +import './OzwellButton.html'; + +Template.ozwellButton.events({ + 'click .ozwell-btn'(event, template) { + event.preventDefault(); + event.stopPropagation(); + + // Find the associated input element + const button = event.currentTarget; + const container = button.closest('.input-group, .form-control, form, .card'); + let inputElement = null; + + // Look for text inputs, textareas in the same container + if (container) { + inputElement = container.querySelector('input[type="text"], textarea, [contenteditable="true"]'); + } + + // If not found, look in parent container + if (!inputElement) { + const parentContainer = button.closest('.card, .form-group, .field'); + if (parentContainer) { + inputElement = parentContainer.querySelector('input[type="text"], textarea, [contenteditable="true"]'); + } + } + + if (!inputElement) { + alert('No text input found to assist with'); + return; + } + + // Get context from the current page - try multiple ways + let context = {}; + + // First try to get context from current template + if (template.getOzwellContext) { + context = template.getOzwellContext(); + } else { + // Try to find a parent template with the context method + let parentTemplate = template; + while (parentTemplate && parentTemplate.view && parentTemplate.view.parentView) { + parentTemplate = parentTemplate.view.parentView.templateInstance ? parentTemplate.view.parentView.templateInstance() : null; + if (parentTemplate && parentTemplate.getOzwellContext) { + context = parentTemplate.getOzwellContext(); + break; + } + } + + // If still no context, provide a basic fallback + if (!context.teamId) { + // Try to get team info from the URL or current state + const teamSelect = document.querySelector('#teamSelect'); + const teamId = teamSelect ? teamSelect.value : null; + const teamName = teamSelect ? teamSelect.options[teamSelect.selectedIndex]?.text : 'Current Project'; + + context = { + teamId: teamId, + teamName: teamName || 'Current Project', + user: { + username: Meteor.user()?.username || 'Unknown User' + }, + currentTicket: null, + recentActivity: [] + }; + } + } + + // Add current text from the input element + context.currentText = inputElement.value || inputElement.textContent || ''; + + console.log('Opening Ozwell with context:', context); + + // Open Ozwell modal + if (window.openOzwell) { + window.openOzwell(inputElement, context); + } else { + console.error('Ozwell not available'); + } + } +}); \ No newline at end of file diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html new file mode 100644 index 0000000..2c04e18 --- /dev/null +++ b/client/components/ozwell/OzwellModal.html @@ -0,0 +1,107 @@ + \ No newline at end of file diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js new file mode 100644 index 0000000..cd6d249 --- /dev/null +++ b/client/components/ozwell/OzwellModal.js @@ -0,0 +1,580 @@ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; + +// Import the HTML template +import './OzwellModal.html'; + +// Template helpers and events for OzwellModal +Template.ozwellModal.onCreated(function () { + // Add reference for methods that need access to template + const template = this; + + this.isOzwellOpen = new ReactiveVar(false); + this.selectedPrompt = new ReactiveVar(null); + this.ozwellSessionUrl = new ReactiveVar(null); + this.availablePrompts = new ReactiveVar([]); + this.canSave = new ReactiveVar(false); + this.currentContext = new ReactiveVar(null); + this.currentInputElement = new ReactiveVar(null); + this.currentTeamId = new ReactiveVar(null); + this.sessionReady = new ReactiveVar(false); + this.generatedContent = new ReactiveVar(null); // Store content from Ozwell + + // Store reference to this template instance globally for access from other components + window.ozwellModalInstance = this; + + // Set up global iframe loaded handler + window.ozwellIframeLoaded = function () { + console.log('Ozwell iframe loaded'); + if (window.ozwellModalInstance) { + // Enable save button after iframe loads + setTimeout(() => { + window.ozwellModalInstance.canSave.set(true); + console.log('Save button enabled after iframe load'); + }, 3000); + } + }; + + // Define loadPrompts method first + this.loadPrompts = function () { + Meteor.call('getOzwellPrompts', (err, prompts) => { + if (!err && prompts) { + this.availablePrompts.set(prompts); + } else { + console.error('Failed to load Ozwell prompts:', err); + // Fallback to basic prompts + this.availablePrompts.set([ + { + id: 'custom', + title: 'Help me write', + description: 'Get general writing assistance', + icon: 'pencil' + } + ]); + } + }); + }; + + // Load available prompts + this.loadPrompts(); + + // Open Ozwell modal with context + this.openOzwell = function (inputElement, context = {}) { + // Check if user has Ozwell configured + const user = Meteor.user(); + if (!user?.profile?.ozwellEnabled) { + alert('Please configure Ozwell in your settings first.'); + return; + } + + template.currentInputElement.set(inputElement); + template.currentContext.set(context); + template.currentTeamId.set(context.teamId); + template.isOzwellOpen.set(true); + template.canSave.set(false); + template.sessionReady.set(false); + + // Reset state + template.selectedPrompt.set(null); + template.ozwellSessionUrl.set(null); + }; + + // Set up postMessage listener for iframe communication + this.setupPostMessageListener = function (prompt, context) { + // Clean up any existing listener + if (template.messageHandler) { + window.removeEventListener('message', template.messageHandler); + } + + template.messageHandler = function (event) { + // Verify origin for security + if (event.origin !== 'https://ai.bluehive.com') { + return; + } + + const data = event.data; + console.log('Received postMessage from Ozwell:', data); + + // Handle different message formats from Ozwell + if (data.channel === 'IframeSync' && data.type === 'ready') { + template.sessionReady.set(true); + + // Enable save button after a short delay since Ozwell is ready + setTimeout(() => { + template.canSave.set(true); + }, 2000); + + // Send initial context to Ozwell + const iframe = document.querySelector('#ozwell-iframe'); + if (iframe && iframe.contentWindow) { + const selectedPrompt = template.selectedPrompt.get(); + const contextData = { + type: 'mcp-context', + context: { + teamName: context.teamName || 'Current Team', + currentText: context.currentText || '', + prompt: selectedPrompt?.template || selectedPrompt?.title || prompt.template || prompt.title, + systemMessage: selectedPrompt?.systemMessage || prompt.systemMessage || 'You are a helpful writing assistant.', + projectType: 'Time Tracking Application', + instructions: selectedPrompt?.description || 'Help the user improve their content.', + ...context + } + }; + + console.log('Sending context to Ozwell:', contextData); + iframe.contentWindow.postMessage(contextData, 'https://ai.bluehive.com'); + + // Also send the prompt as initial message if available + if (selectedPrompt?.template && context.currentText) { + setTimeout(() => { + const promptMessage = selectedPrompt.template.replace('{{currentText}}', context.currentText || '') + .replace('{{teamName}}', context.teamName || 'Current Team'); + + iframe.contentWindow.postMessage({ + type: 'ozwell-send-message', + message: promptMessage + }, 'https://ai.bluehive.com'); + }, 1000); + } + } + } else if (data.channel === 'iframe-basic' && data.message === 'sessionRendered') { + // Session is rendered and ready + template.sessionReady.set(true); + template.canSave.set(true); + } else if (data.type === 'ozwell-ready') { + template.sessionReady.set(true); + + // Enable save button after a short delay since Ozwell is ready + setTimeout(() => { + template.canSave.set(true); + }, 2000); + + // Send initial context to Ozwell + const iframe = document.querySelector('#ozwell-iframe'); + if (iframe && iframe.contentWindow) { + const selectedPrompt = template.selectedPrompt.get(); + const contextData = { + type: 'mcp-context', + context: { + teamName: context.teamName || 'Current Team', + currentText: context.currentText || '', + prompt: selectedPrompt?.template || selectedPrompt?.title || prompt.template || prompt.title, + systemMessage: selectedPrompt?.systemMessage || prompt.systemMessage || 'You are a helpful writing assistant.', + projectType: 'Time Tracking Application', + instructions: selectedPrompt?.description || 'Help the user improve their content.', + ...context + } + }; + + console.log('Sending context to Ozwell:', contextData); + iframe.contentWindow.postMessage(contextData, 'https://ai.bluehive.com'); + + // Also send the prompt as initial message if available + if (selectedPrompt?.template && context.currentText) { + setTimeout(() => { + const promptMessage = selectedPrompt.template.replace('{{currentText}}', context.currentText || '') + .replace('{{teamName}}', context.teamName || 'Current Team'); + + iframe.contentWindow.postMessage({ + type: 'ozwell-send-message', + message: promptMessage + }, 'https://ai.bluehive.com'); + }, 1000); + } + } + } else if (data.type === 'ozwell-content-ready' || data.type === 'iframe-basic' || data.type === 'sessionRendered') { + // Content is ready to be saved + template.canSave.set(true); + if (data.content) { + template.generatedContent.set(data.content); + } + } else if (data.type === 'ozwell-content-changed' || data.type === 'messageAdded' || data.type === 'messagesUpdated') { + // Content has been modified in Ozwell + template.canSave.set(true); + if (data.content) { + template.generatedContent.set(data.content); + } + } else if (data.type === 'ozwell-get-content') { + // Request current content from Ozwell - send a message to get it + const iframe = document.querySelector('#ozwell-iframe'); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage({ + type: 'get-current-content' + }, 'https://ai.bluehive.com'); + } + } else if (data.type === 'ozwell-current-content' || data.type === 'export-content' || data.type === 'content-export') { + // Received the current content from Ozwell + if (data.content) { + template.generatedContent.set(data.content); + template.canSave.set(true); + console.log('Received content from Ozwell:', data.content.substring(0, 100) + '...'); + } + } else if (data.channel === 'iframe-basic' && data.content) { + // Content received via iframe-basic channel + template.generatedContent.set(data.content); + template.canSave.set(true); + console.log('Received content via iframe-basic:', data.content.substring(0, 100) + '...'); + } else if (data.channel === 'IframeSync' && data.content) { + // Content received via IframeSync channel + template.generatedContent.set(data.content); + template.canSave.set(true); + console.log('Received content via IframeSync:', data.content.substring(0, 100) + '...'); + } else if (data.type === 'message' && data.text) { + // Message content received + template.generatedContent.set(data.text); + template.canSave.set(true); + console.log('Received message content:', data.text.substring(0, 100) + '...'); + } else if (data.message && typeof data.message === 'string' && data.message.length > 10) { + // Generic message content + template.generatedContent.set(data.message); + template.canSave.set(true); + console.log('Received generic message:', data.message.substring(0, 100) + '...'); + } + + // For any message from Ozwell iframe, enable save button (fallback) + if (!template.canSave.get() && template.sessionReady.get()) { + setTimeout(() => { + template.canSave.set(true); + }, 3000); + } + }; + + window.addEventListener('message', template.messageHandler); + }; + + // Initialize Ozwell session + this.initializeOzwellSession = function (prompt) { + const teamId = template.currentTeamId.get(); + const context = template.currentContext.get(); + + if (!teamId) { + console.error('No team ID available for Ozwell session'); + return; + } + + // Show loading state + template.ozwellSessionUrl.set(null); + + // Create Ozwell session + Meteor.call('createOzwellSession', teamId, false, (err, sessionData) => { + if (err) { + console.error('Failed to create Ozwell session:', err); + alert('Failed to start Ozwell session. Please check your configuration.'); + template.closeOzwell(false); + return; + } + + template.ozwellSessionUrl.set(sessionData.loginUrl); + + // Set up postMessage listener for iframe communication + template.setupPostMessageListener(prompt, context); + }); + }; + + // Close Ozwell modal + this.closeOzwell = function (save = false) { + if (save) { + // Try multiple ways to get content from Ozwell + const iframe = document.querySelector('#ozwell-iframe'); + if (iframe && iframe.contentWindow && template.sessionReady.get()) { + + // First, try to send specific postMessage requests to get content + console.log('Requesting content from Ozwell iframe...'); + + // Try different postMessage requests that Ozwell might respond to + const contentRequests = [ + { type: 'get-current-content' }, + { type: 'export-content' }, + { channel: 'iframe-basic', message: 'getContent' }, + { channel: 'IframeSync', type: 'getContent' }, + { type: 'get-last-message' }, + { type: 'export-conversation' } + ]; + + contentRequests.forEach((request, index) => { + setTimeout(() => { + iframe.contentWindow.postMessage(request, 'https://ai.bluehive.com'); + }, index * 200); + }); + + // Wait longer for responses and then try to extract content + setTimeout(() => { + // If we still don't have content, try to access iframe DOM directly + let extractedContent = template.generatedContent.get(); + + if (!extractedContent || extractedContent.trim().length === 0) { + try { + // Try to access iframe content directly + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + + // Try multiple selectors to find content + const selectors = [ + '.message-content', + '.chat-message', + '.response-text', + '.message-text', + '.content', + '[data-message]', + '.message:last-child', + '.chat-content .message:last-child', + 'p:last-child', + 'div[role="textbox"]', + '.ql-editor', + '.text-content' + ]; + + for (const selector of selectors) { + const elements = iframeDoc.querySelectorAll(selector); + if (elements.length > 0) { + const lastElement = elements[elements.length - 1]; + const content = lastElement.textContent || lastElement.innerText; + if (content && content.trim().length > 10 && !content.includes('Type a message')) { + extractedContent = content.trim(); + console.log(`Content extracted using selector ${selector}:`, extractedContent.substring(0, 100) + '...'); + break; + } + } + } + + // If still no good content, try to get all text from the iframe + if (!extractedContent || extractedContent.trim().length === 0) { + const allText = iframeDoc.body.textContent || iframeDoc.body.innerText; + // Look for meaningful content (not UI text) + const lines = allText.split('\n').map(line => line.trim()).filter(line => + line.length > 20 && + !line.includes('Type a message') && + !line.includes('Ozwell AI') && + !line.includes('Send') && + !line.includes('Cancel') && + !line.toLowerCase().includes('loading') && + !line.toLowerCase().includes('connecting') + ); + + if (lines.length > 0) { + extractedContent = lines[lines.length - 1]; // Get the last meaningful line + console.log('Content extracted from body text:', extractedContent.substring(0, 100) + '...'); + } + } + + } catch (e) { + console.log('Cannot access iframe content directly (CORS):', e); + // This is expected due to CORS restrictions + } + } + + if (extractedContent && extractedContent.trim().length > 0) { + template.generatedContent.set(extractedContent); + } + + template.performAutofill(); + }, 1500); + + // Don't close immediately if we're saving - wait for autofill + return; + } else { + // If no iframe or not ready, try to use any stored content + template.performAutofill(); + } + } + + template.closeModal(); + }; + + // Perform the actual autofill + this.performAutofill = function () { + let content = template.generatedContent.get(); + const inputElement = template.currentInputElement.get(); + + // If no content was captured, try to get some content one more time + if (!content || content.trim().length === 0 || content === 'Content generated with Ozwell AI assistant') { + // Try to get content from the iframe one more time + const iframe = document.querySelector('#ozwell-iframe'); + if (iframe && iframe.contentWindow) { + try { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + + // Last attempt to get actual content + const textElements = iframeDoc.querySelectorAll('p, div, span'); + let foundContent = ''; + + for (const element of textElements) { + const text = element.textContent || element.innerText; + if (text && text.trim().length > 20 && + !text.includes('Type a message') && + !text.includes('AI assistant') && + !text.includes('Ozwell') && + !text.includes('Send') && + !text.includes('Cancel')) { + foundContent = text.trim(); + break; + } + } + + if (foundContent) { + content = foundContent; + console.log('Found content in final attempt:', content.substring(0, 100) + '...'); + } + } catch (e) { + // CORS restriction - expected + } + } + + // If still no content, use a more descriptive placeholder + if (!content || content.trim().length === 0) { + content = '[Ozwell AI content - please check the generated text above]'; + } + } + + if (content && inputElement) { + console.log('Autofilling content:', content.substring(0, 200) + (content.length > 200 ? '...' : '')); + + // Handle different types of input elements + if (inputElement.tagName === 'TEXTAREA' || inputElement.tagName === 'INPUT') { + inputElement.value = content; + + // Trigger input event to notify other parts of the app + const event = new Event('input', { bubbles: true }); + inputElement.dispatchEvent(event); + + // Also trigger change event + const changeEvent = new Event('change', { bubbles: true }); + inputElement.dispatchEvent(changeEvent); + } else if (inputElement.contentEditable === 'true') { + inputElement.textContent = content; + + // Trigger input event for contentEditable + const event = new Event('input', { bubbles: true }); + inputElement.dispatchEvent(event); + } + + // Focus the input element + inputElement.focus(); + + console.log('Ozwell content autofilled successfully'); + } else { + console.log('No content to autofill or no input element'); + } + + template.closeModal(); + }; + + // Close modal and clean up + this.closeModal = function () { + template.isOzwellOpen.set(false); + + // Clean up postMessage listener + if (template.messageHandler) { + window.removeEventListener('message', template.messageHandler); + template.messageHandler = null; + } + + // Reset state + template.selectedPrompt.set(null); + template.ozwellSessionUrl.set(null); + template.currentInputElement.set(null); + template.currentContext.set(null); + template.currentTeamId.set(null); + template.sessionReady.set(false); + template.canSave.set(false); + template.generatedContent.set(null); + + console.log('Ozwell modal closed'); + }; +}); + +Template.ozwellModal.onDestroyed(function () { + // Clean up global reference + if (window.ozwellModalInstance === this) { + window.ozwellModalInstance = null; + } +}); + +Template.ozwellModal.helpers({ + isOzwellOpen() { + return Template.instance().isOzwellOpen.get(); + }, + + selectedPrompt() { + return Template.instance().selectedPrompt.get(); + }, + + ozwellSessionUrl() { + return Template.instance().ozwellSessionUrl.get(); + }, + + availablePrompts() { + const allPrompts = Template.instance().availablePrompts.get(); + // Filter prompts based on current context if needed + return allPrompts; + }, + + canSave() { + return Template.instance().canSave.get(); + }, + + saveButtonClass() { + return Template.instance().canSave.get() ? '' : 'btn-disabled'; + }, + + eq(a, b) { + return a === b; + } +}); + +Template.ozwellModal.events({ + 'click #ozwell-close'(event, template) { + template.closeModal(); + }, + + 'click #ozwell-backdrop'(event, template) { + if (event.target.id === 'ozwell-backdrop') { + template.closeModal(); + } + }, + + 'click #ozwell-cancel'(event, template) { + template.closeModal(); + }, + + 'click #ozwell-save'(event, template) { + template.closeOzwell(true); + }, + + 'click .prompt-btn'(event, template) { + const promptId = event.currentTarget.getAttribute('data-prompt-id'); + const prompts = template.availablePrompts.get(); + const selectedPrompt = prompts.find(p => p.id === promptId); + + if (selectedPrompt) { + template.selectedPrompt.set(selectedPrompt); + template.initializeOzwellSession(selectedPrompt); + } + }, + + 'click #use-custom-prompt'(event, template) { + // Create a custom prompt object + const customPrompt = { + id: 'custom', + title: 'Custom Prompt', + template: 'Current text: "{{currentText}}"\n\nProject: {{teamName}}\n\nPlease help me improve this content.', + systemMessage: 'You are a helpful writing assistant. Help the user improve their content while maintaining their intended meaning.' + }; + + template.selectedPrompt.set(customPrompt); + template.initializeOzwellSession(customPrompt); + } +}); + +// Template methods +Template.ozwellModal.helpers({ + // Additional methods accessible from template +}); + +// Global helper function to open Ozwell from any component +window.openOzwell = function (inputElement, context = {}) { + if (window.ozwellModalInstance) { + window.ozwellModalInstance.openOzwell(inputElement, context); + } else { + console.error('Ozwell modal not available'); + } +}; \ No newline at end of file diff --git a/client/components/settings/SettingsPage.html b/client/components/settings/SettingsPage.html new file mode 100644 index 0000000..c0db8a2 --- /dev/null +++ b/client/components/settings/SettingsPage.html @@ -0,0 +1,113 @@ + \ No newline at end of file diff --git a/client/components/settings/SettingsPage.js b/client/components/settings/SettingsPage.js new file mode 100644 index 0000000..e5cbb45 --- /dev/null +++ b/client/components/settings/SettingsPage.js @@ -0,0 +1,140 @@ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; + +// Import the HTML template +import './SettingsPage.html'; + +Template.settings.onCreated(function () { + this.showOzwellConfig = new ReactiveVar(false); + this.isConfiguring = new ReactiveVar(false); + this.configMessage = new ReactiveVar(''); + this.configMessageType = new ReactiveVar('alert-info'); +}); + +Template.settings.helpers({ + ozwellConfigured() { + const user = Meteor.user(); + return user?.profile?.ozwellEnabled && user?.profile?.ozwellApiKey; + }, + + showOzwellConfig() { + return Template.instance().showOzwellConfig.get(); + }, + + isConfiguring() { + return Template.instance().isConfiguring.get(); + }, + + configMessage() { + return Template.instance().configMessage.get(); + }, + + configMessageType() { + return Template.instance().configMessageType.get(); + }, + + apiKeyInputClass() { + return Template.instance().isConfiguring.get() ? 'input-disabled' : ''; + }, + + saveButtonClass() { + return Template.instance().isConfiguring.get() ? 'btn-disabled' : ''; + }, + + cancelButtonClass() { + const isConfiguring = Template.instance().isConfiguring.get(); + return isConfiguring ? 'btn btn-outline disabled' : 'btn btn-outline'; + } +}); + +Template.settings.events({ + 'click #configureOzwell'(event, template) { + template.showOzwellConfig.set(true); + template.configMessage.set(''); + }, + + 'click #cancelOzwellConfig'(event, template) { + template.showOzwellConfig.set(false); + template.configMessage.set(''); + }, + + 'click #reconfigureOzwell'(event, template) { + template.showOzwellConfig.set(true); + template.configMessage.set(''); + }, + + 'submit #ozwellConfigForm'(event, template) { + event.preventDefault(); + + const apiKey = event.target.apiKey.value.trim(); + if (!apiKey) { + template.configMessage.set('Please enter an API key'); + template.configMessageType.set('alert-error'); + return; + } + + template.isConfiguring.set(true); + template.configMessage.set('Testing API key...'); + template.configMessageType.set('alert-info'); + + Meteor.call('saveOzwellApiKey', apiKey, (err, result) => { + template.isConfiguring.set(false); + + if (err) { + console.error('Failed to save Ozwell API key:', err); + template.configMessage.set(err.reason || 'Failed to save API key'); + template.configMessageType.set('alert-error'); + } else { + template.configMessage.set('Ozwell configured successfully! You can now use AI writing assistance.'); + template.configMessageType.set('alert-success'); + template.showOzwellConfig.set(false); + + // Clear the form + event.target.reset(); + } + }); + }, + + 'click #testOzwellConnection'(event, template) { + template.configMessage.set('Testing connection...'); + template.configMessageType.set('alert-info'); + + const user = Meteor.user(); + const apiKey = user?.profile?.ozwellApiKey; + + if (!apiKey) { + template.configMessage.set('No API key configured'); + template.configMessageType.set('alert-error'); + return; + } + + Meteor.call('testOzwellCredentials', apiKey, (err, result) => { + if (err) { + console.error('Ozwell connection test failed:', err); + template.configMessage.set(err.reason || 'Connection test failed'); + template.configMessageType.set('alert-error'); + } else { + template.configMessage.set('Connection test successful!'); + template.configMessageType.set('alert-success'); + } + }); + }, + + 'click #disableOzwell'(event, template) { + if (confirm('Are you sure you want to disable Ozwell? Your API key will be removed.')) { + Meteor.call('updateUserProfile', { + 'profile.ozwellApiKey': null, + 'profile.ozwellEnabled': false + }, (err) => { + if (err) { + console.error('Failed to disable Ozwell:', err); + template.configMessage.set('Failed to disable Ozwell'); + template.configMessageType.set('alert-error'); + } else { + template.configMessage.set('Ozwell has been disabled'); + template.configMessageType.set('alert-info'); + } + }); + } + } +}); \ No newline at end of file diff --git a/client/components/tickets/TicketsPage.html b/client/components/tickets/TicketsPage.html index d98eebf..3b71ed7 100644 --- a/client/components/tickets/TicketsPage.html +++ b/client/components/tickets/TicketsPage.html @@ -1,60 +1,70 @@ \ No newline at end of file diff --git a/client/components/tickets/TicketsPage.js b/client/components/tickets/TicketsPage.js index 313ccd6..5442baa 100644 --- a/client/components/tickets/TicketsPage.js +++ b/client/components/tickets/TicketsPage.js @@ -44,9 +44,9 @@ const ticketManager = { try { templateInstance.activeTicketId.set(ticketId); const now = utils.now(); - + await utils.meteorCall('updateTicketStart', ticketId, now); - + if (clockEvent) { await utils.meteorCall('clockEventAddTicket', clockEvent._id, ticketId, now); } @@ -61,7 +61,7 @@ const ticketManager = { try { const now = utils.now(); await utils.meteorCall('updateTicketStop', ticketId, now); - + if (clockEvent) { await utils.meteorCall('clockEventStopTicket', clockEvent._id, ticketId, now); } @@ -75,12 +75,12 @@ const ticketManager = { // Switch from one ticket to another switchTicket: async (newTicketId, templateInstance, clockEvent) => { const currentActiveId = templateInstance.activeTicketId.get(); - + if (currentActiveId) { const success = await ticketManager.stopTicket(currentActiveId, clockEvent); if (!success) return false; } - + await ticketManager.startTicket(newTicketId, templateInstance, clockEvent); return true; } @@ -118,17 +118,51 @@ Template.tickets.onCreated(function () { this.activeTicketId = new ReactiveVar(null); this.clockedIn = new ReactiveVar(false); + // Add getOzwellContext method to this template instance + this.getOzwellContext = () => { + const teamId = this.selectedTeamId.get(); + const team = Teams.findOne(teamId); + const activeTicketId = this.activeTicketId.get(); + const activeTicket = activeTicketId ? Tickets.findOne(activeTicketId) : null; + + // Get recent tickets for context + const recentTickets = Tickets.find( + { teamId }, + { sort: { updatedAt: -1 }, limit: 5 } + ).fetch(); + + return { + teamId, + teamName: team?.name || 'Unknown Project', + user: { + username: Meteor.user()?.username || 'Unknown User' + }, + currentTicket: activeTicket ? { + title: activeTicket.title, + description: activeTicket.github || '', + status: 'active', + totalTime: activeTicket.totalTime || 0 + } : null, + recentActivity: recentTickets.map(ticket => ({ + title: ticket.title, + description: ticket.github || '', + totalTime: ticket.totalTime || 0, + lastUpdated: ticket.updatedAt || ticket.createdAt + })) + }; + }; + this.autorun(() => { const teamIds = Teams.find({}).map(t => t._id); let teamId = this.selectedTeamId.get(); - + if (!teamId && teamIds.length > 0) { this.selectedTeamId.set(teamIds[0]); teamId = this.selectedTeamId.get(); } - + this.subscribe('teamTickets', teamIds); - + if (teamId) { const activeSession = ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null }); if (activeSession) { @@ -152,14 +186,14 @@ Template.tickets.helpers({ tickets() { const teamId = Template.instance().selectedTeamId.get(); if (!teamId) return []; - + const activeTicketId = Template.instance().activeTicketId.get(); const now = currentTime.get(); - + return Tickets.find({ teamId }).fetch().map(ticket => { const isActive = ticket._id === activeTicketId && ticket.startTimestamp; const elapsed = isActive ? Math.max(0, Math.floor((now - ticket.startTimestamp) / 1000)) : 0; - + return { ...ticket, displayTime: (ticket.accumulatedTime || 0) + elapsed @@ -191,7 +225,7 @@ Template.tickets.helpers({ currentActiveTicketInfo() { const activeTicketId = Template.instance().activeTicketId.get(); if (!activeTicketId) return null; - + const ticket = Tickets.findOne(activeTicketId); return ticket ? { id: ticket._id, @@ -204,7 +238,7 @@ Template.tickets.helpers({ const teamId = Template.instance().selectedTeamId.get(); const hasActiveSession = teamId ? !!ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null }) : false; const hasOtherActiveTicket = Template.instance().activeTicketId.get() && Template.instance().activeTicketId.get() !== ticketId; - + if (isActive) return 'btn btn-outline btn-neutral'; if (hasActiveSession && !hasOtherActiveTicket) return 'btn btn-outline btn-neutral'; if (hasActiveSession && hasOtherActiveTicket) return 'btn btn-disabled'; @@ -215,7 +249,7 @@ Template.tickets.helpers({ const teamId = Template.instance().selectedTeamId.get(); const hasActiveSession = teamId ? !!ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null }) : false; const hasOtherActiveTicket = Template.instance().activeTicketId.get() && Template.instance().activeTicketId.get() !== ticketId; - + if (isActive) return 'Click to stop this activity'; if (hasActiveSession && !hasOtherActiveTicket) return 'Click to start this activity'; if (hasActiveSession && hasOtherActiveTicket) return 'Stop the current activity first'; @@ -239,10 +273,10 @@ Template.tickets.events({ 'paste [name="title"]'(e) { setTimeout(() => extractUrlTitle(e.target.value, e.target), 0); }, - + async 'submit #createTicketForm'(e, t) { e.preventDefault(); - + const formData = { teamId: t.selectedTeamId.get(), title: e.target.title.value.trim(), @@ -251,23 +285,23 @@ Template.tickets.events({ minutes: parseInt(e.target.minutes.value) || 0, seconds: parseInt(e.target.seconds.value) || 0 }; - + if (!formData.title) { alert('Ticket title is required.'); return; } - + try { const accumulatedTime = utils.calculateAccumulatedTime(formData.hours, formData.minutes, formData.seconds); - const ticketId = await utils.meteorCall('createTicket', { - teamId: formData.teamId, - title: formData.title, - github: formData.github, - accumulatedTime + const ticketId = await utils.meteorCall('createTicket', { + teamId: formData.teamId, + title: formData.title, + github: formData.github, + accumulatedTime }); - + t.showCreateTicketForm.set(false); - + if (accumulatedTime > 0) { const clockEvent = ClockEvents.findOne({ userId: Meteor.userId(), teamId: formData.teamId, endTime: null }); await ticketManager.startTicket(ticketId, t, clockEvent); @@ -276,35 +310,35 @@ Template.tickets.events({ utils.handleError(error, 'Error creating ticket'); } }, - + async 'click .activate-ticket'(e, t) { const ticketId = e.currentTarget.dataset.id; const isActive = t.activeTicketId.get() === ticketId; const teamId = t.selectedTeamId.get(); const clockEvent = ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null }); - + if (!isActive) { if (!clockEvent) { alert('Please start a session before starting an activity.'); return; } - + await ticketManager.switchTicket(ticketId, t, clockEvent); } else { await ticketManager.stopTicket(ticketId, clockEvent); t.activeTicketId.set(null); } }, - + 'click #clockInBtn'(e, t) { const teamId = t.selectedTeamId.get(); sessionManager.startSession(teamId); }, - + async 'click #clockOutBtn'(e, t) { const teamId = t.selectedTeamId.get(); const activeTicketId = t.activeTicketId.get(); - + const success = await sessionManager.stopSession(teamId, activeTicketId); if (success) { t.activeTicketId.set(null); diff --git a/client/main.js b/client/main.js index b6a498c..58de517 100644 --- a/client/main.js +++ b/client/main.js @@ -11,6 +11,11 @@ import './components/layout/MainLayout.html'; import './components/teams/TeamsPage.html'; import './components/tickets/TicketsPage.html'; import './components/home/HomePage.html'; +import './components/settings/SettingsPage.html'; + +// Import Ozwell components +import './components/ozwell/OzwellModal.html'; +import './components/ozwell/OzwellButton.html'; // Import component JS files import './components/auth/AuthPage.js'; @@ -18,6 +23,11 @@ import './components/layout/MainLayout.js'; import './components/teams/TeamsPage.js'; import './components/tickets/TicketsPage.js'; import './components/home/HomePage.js'; +import './components/settings/SettingsPage.js'; + +// Import Ozwell component JS files +import './components/ozwell/OzwellModal.js'; +import './components/ozwell/OzwellButton.js'; // Import currentTime from MainLayout diff --git a/collections.js b/collections.js index 853e1f9..5af1846 100644 --- a/collections.js +++ b/collections.js @@ -5,3 +5,9 @@ export const Teams = new Mongo.Collection('teams'); export const Sessions = new Mongo.Collection('sessions'); export const ClockEvents = new Mongo.Collection('clockevents'); + +// Ozwell Integration Collections +export const OzwellWorkspaces = new Mongo.Collection('ozwellworkspaces'); +export const OzwellUsers = new Mongo.Collection('ozwellusers'); +export const OzwellConversations = new Mongo.Collection('ozwellconversations'); +export const OzwellPrompts = new Mongo.Collection('ozwellprompts'); diff --git a/server/main.js b/server/main.js index bc4871d..56c6269 100644 --- a/server/main.js +++ b/server/main.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Tickets, Teams, Sessions, ClockEvents } from '../collections.js'; +import { Tickets, Teams, Sessions, ClockEvents, OzwellPrompts } from '../collections.js'; // Import authentication methods import { authMethods } from './methods/auth.js'; // Import team methods @@ -8,6 +8,9 @@ import { teamMethods } from './methods/teams.js'; // Import ticket and clock event methods import { ticketMethods } from './methods/tickets.js'; import { clockEventMethods } from './methods/clockEvents.js'; +// Import Ozwell methods +import { ozwellMethods } from './methods/ozwell.js'; +import { ozwellPromptMethods } from './methods/ozwellPrompts.js'; Meteor.startup(async () => { // Code to run on server startup if (await Tickets.find().countAsync() === 0) { @@ -27,6 +30,104 @@ Meteor.startup(async () => { const code = Math.random().toString(36).substr(2, 8).toUpperCase(); await Teams.updateAsync(team._id, { $set: { code } }); } + + // Initialize Ozwell default prompts directly (not as a method call since we're in startup) + const promptCount = await OzwellPrompts.find().countAsync(); + if (promptCount === 0) { + const defaultPrompts = [ + { + id: 'draft-time-entry', + title: 'Help me draft a time entry', + description: 'Get assistance writing a detailed time entry for your work', + template: `Help me write a detailed time entry for my work today. Here's what I'm working on: + +Project: {{teamName}} +{{#if currentTicket}} +Current Activity: {{currentTicket.title}} +{{#if currentTicket.description}} +Notes/Reference: {{currentTicket.description}} +{{/if}} +{{/if}} + +{{#if recentActivity}} +Recent activities in this project: +{{#each recentActivity}} +- {{title}}{{#if description}} ({{description}}){{/if}} +{{/each}} +{{/if}} + +Please help me write a professional time entry that describes what I accomplished, any challenges I faced, and next steps. Make it detailed enough for project tracking but concise for time logging.`, + category: 'time-tracking', + contexts: ['ticket-form', 'time-entry'], + icon: 'clock', + systemMessage: 'You are a professional time tracking assistant. Help users write clear, detailed time entries that capture their work accomplishments, challenges, and progress. Focus on being specific and actionable.', + createdAt: new Date() + }, + { + id: 'summarize-daily-activity', + title: 'Summarize my activity today', + description: 'Create a summary of your work activities for the day', + template: `Please create a summary of my work activities for today. + +Project: {{teamName}} +User: {{user.username}} + +{{#if recentActivity}} +Activities worked on: +{{#each recentActivity}} +- {{title}}{{#if description}} - {{description}}{{/if}} + Time spent: {{totalTime}} seconds + Last updated: {{lastUpdated}} +{{/each}} +{{/if}} + +Please provide: +1. A brief overview of what I accomplished today +2. Key highlights or milestones reached +3. Any blockers or challenges encountered +4. Suggested priorities for tomorrow + +Make it suitable for sharing with team members or for personal reflection.`, + category: 'reporting', + contexts: ['end-of-day', 'summary'], + icon: 'chart-bar', + systemMessage: 'You are a professional work summary assistant. Help users create clear, organized summaries of their daily work that highlight accomplishments, identify challenges, and suggest next steps.', + createdAt: new Date() + }, + { + id: 'improve-activity-description', + title: 'Improve my activity description', + description: 'Get help making your activity description more clear and detailed', + template: `Please help me improve this activity description: + +Current text: "{{currentText}}" + +{{#if currentTicket}} +Activity: {{currentTicket.title}} +{{/if}} +Project: {{teamName}} + +Please help me: +1. Make the description more clear and specific +2. Add relevant technical details if appropriate +3. Ensure it's useful for future reference +4. Make it professional and well-structured + +Keep the core meaning but enhance clarity, detail, and usefulness for project tracking.`, + category: 'writing', + contexts: ['ticket-form', 'note-taking'], + icon: 'pencil', + systemMessage: 'You are a professional writing assistant specializing in technical documentation. Help users write clear, detailed, and well-structured activity descriptions that are useful for project tracking and future reference.', + createdAt: new Date() + } + ]; + + for (const prompt of defaultPrompts) { + await OzwellPrompts.insertAsync(prompt); + } + + console.log('Initialized', defaultPrompts.length, 'default Ozwell prompts'); + } }); Meteor.publish('userTeams', function () { @@ -81,6 +182,8 @@ Meteor.methods({ ...teamMethods, ...ticketMethods, ...clockEventMethods, + ...ozwellMethods, + ...ozwellPromptMethods, 'participants.create'(name) { check(name, String); diff --git a/server/methods/auth.js b/server/methods/auth.js index c63c723..11581f3 100644 --- a/server/methods/auth.js +++ b/server/methods/auth.js @@ -1,14 +1,15 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; +import { check } from 'meteor/check'; export const authMethods = { - createUserAccount({ username, password }) { + async createUserAccount({ username, password }) { if (!username || !password) { throw new Meteor.Error('invalid-data', 'Username and password are required'); } - + try { - const userId = Accounts.createUser({ username, password }); + const userId = await Accounts.createUserAsync({ username, password }); console.log('User created:', { userId, username }); // Log user creation details return userId; } catch (error) { @@ -16,4 +17,17 @@ export const authMethods = { throw new Meteor.Error('server-error', 'Failed to create user'); } }, + + async updateUserProfile(updates) { + check(updates, Object); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + try { + await Meteor.users.updateAsync(this.userId, { $set: updates }); + return { success: true }; + } catch (error) { + console.error('Error updating user profile:', error); + throw new Meteor.Error('server-error', 'Failed to update profile'); + } + } }; \ No newline at end of file diff --git a/server/methods/ozwell.js b/server/methods/ozwell.js new file mode 100644 index 0000000..dc46582 --- /dev/null +++ b/server/methods/ozwell.js @@ -0,0 +1,375 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { OzwellWorkspaces, OzwellUsers, OzwellConversations, OzwellPrompts, Teams, Tickets, ClockEvents } from '../../collections.js'; +import axios from 'axios'; + +const OZWELL_API_BASE = 'https://ai.bluehive.com/api/v1'; + +export const ozwellMethods = { + // Test Ozwell API credentials + async testOzwellCredentials(apiKey) { + check(apiKey, String); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + try { + const response = await axios.post(`${OZWELL_API_BASE}/test-credentials`, {}, { + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }); + + return { success: true, message: response.data.message }; + } catch (error) { + console.error('Ozwell credentials test failed:', error.response?.data); + throw new Meteor.Error('ozwell-error', 'Invalid API credentials'); + } + }, + + // Save user's Ozwell API key + async saveOzwellApiKey(apiKey) { + check(apiKey, String); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + // First test the credentials + await ozwellMethods.testOzwellCredentials.call(this, apiKey); + + // Save to user profile + await Meteor.users.updateAsync(this.userId, { + $set: { + 'profile.ozwellApiKey': apiKey, + 'profile.ozwellEnabled': true + } + }); + + return { success: true }; + }, + + // Create or get Ozwell workspace for a team + async getOrCreateOzwellWorkspace(teamId) { + check(teamId, String); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + // Check if user is member of team + const team = await Teams.findOneAsync({ _id: teamId, members: this.userId }); + if (!team) throw new Meteor.Error('not-authorized', 'Not a team member'); + + // Get user's API key + const user = await Meteor.users.findOneAsync(this.userId); + const apiKey = user?.profile?.ozwellApiKey; + if (!apiKey) throw new Meteor.Error('ozwell-error', 'Ozwell API key not configured'); + + // Check if workspace already exists for this team + let workspace = await OzwellWorkspaces.findOneAsync({ teamId }); + + if (!workspace) { + // Create new workspace + try { + const response = await axios.post(`${OZWELL_API_BASE}/workspaces/create`, { + name: `TimeHarbor - ${team.name}`, + metaData: { + externalId: teamId, + teamName: team.name + } + }, { + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }); + + workspace = { + teamId, + workspaceId: response.data.workspaceId, + name: team.name, + createdAt: new Date(), + createdBy: this.userId + }; + + workspace._id = await OzwellWorkspaces.insertAsync(workspace); + } catch (error) { + console.error('Failed to create Ozwell workspace:', error.response?.data); + throw new Meteor.Error('ozwell-error', 'Failed to create workspace'); + } + } + + return workspace; + }, + + // Create or get Ozwell user for current user in workspace + async getOrCreateOzwellUser(workspaceId) { + check(workspaceId, String); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + // Get user's API key + const user = await Meteor.users.findOneAsync(this.userId); + const apiKey = user?.profile?.ozwellApiKey; + if (!apiKey) throw new Meteor.Error('ozwell-error', 'Ozwell API key not configured'); + + // Check if user already exists in this workspace + let ozwellUser = await OzwellUsers.findOneAsync({ + userId: this.userId, + workspaceId + }); + + if (!ozwellUser) { + // Create new user in Ozwell workspace + try { + const response = await axios.post(`${OZWELL_API_BASE}/workspaces/${workspaceId}/create-user`, {}, { + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }); + + ozwellUser = { + userId: this.userId, + workspaceId, + ozwellUserId: response.data.userId, + username: user.username, + createdAt: new Date() + }; + + ozwellUser._id = await OzwellUsers.insertAsync(ozwellUser); + } catch (error) { + console.error('Failed to create Ozwell user:', error.response?.data); + throw new Meteor.Error('ozwell-error', 'Failed to create user'); + } + } + + return ozwellUser; + }, + + // Create Ozwell session for user + async createOzwellSession(teamId, forceNewSession = false) { + check(teamId, String); + check(forceNewSession, Boolean); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + // Get user's API key + const user = await Meteor.users.findOneAsync(this.userId); + const apiKey = user?.profile?.ozwellApiKey; + if (!apiKey) throw new Meteor.Error('ozwell-error', 'Ozwell API key not configured'); + + // Get or create workspace + const workspace = await ozwellMethods.getOrCreateOzwellWorkspace.call(this, teamId); + + // Get or create user + const ozwellUser = await ozwellMethods.getOrCreateOzwellUser.call(this, workspace.workspaceId); + + // Create user session + try { + const response = await axios.post(`${OZWELL_API_BASE}/workspaces/${workspace.workspaceId}/create-user-session`, { + userId: ozwellUser.ozwellUserId, + metaData: { + createListenSession: true, + forceNewSession, + embedType: 'iframe-basic' + } + }, { + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }); + + return { + loginUrl: response.data.loginUrl, + loginToken: response.data.loginToken, + workspaceId: workspace.workspaceId, + userId: ozwellUser.ozwellUserId + }; + } catch (error) { + console.error('Failed to create Ozwell session:', error.response?.data); + throw new Meteor.Error('ozwell-error', 'Failed to create session'); + } + }, + + // Get page context for MCP + async getPageContext(teamId, ticketId = null) { + check(teamId, String); + check(ticketId, Match.Maybe(String)); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + // Get team info + const team = await Teams.findOneAsync({ _id: teamId, members: this.userId }); + if (!team) throw new Meteor.Error('not-authorized', 'Not a team member'); + + const context = { + team: { + name: team.name, + id: team._id, + memberCount: team.members?.length || 0 + }, + user: { + username: (await Meteor.users.findOneAsync(this.userId))?.username + } + }; + + // Add ticket context if provided + if (ticketId) { + const ticket = await Tickets.findOneAsync({ _id: ticketId, teamId }); + if (ticket) { + context.currentTicket = { + title: ticket.title, + description: ticket.github || '', + status: ticket.status || 'active', + totalTime: ticket.totalTime || 0 + }; + } + } + + // Add recent team activity + const recentTickets = await Tickets.find( + { teamId }, + { sort: { updatedAt: -1 }, limit: 5 } + ).fetchAsync(); + + context.recentActivity = recentTickets.map(ticket => ({ + title: ticket.title, + description: ticket.github || '', + totalTime: ticket.totalTime || 0, + lastUpdated: ticket.updatedAt + })); + + return context; + }, + + // Search user's history + async searchUserHistory(query, teamId = null) { + check(query, String); + check(teamId, Match.Maybe(String)); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + const searchRegex = new RegExp(query, 'i'); + const filter = { userId: this.userId }; + + if (teamId) { + filter.teamId = teamId; + } + + // Search in tickets + const tickets = await Tickets.find({ + ...filter, + $or: [ + { title: searchRegex }, + { github: searchRegex } + ] + }, { + sort: { updatedAt: -1 }, + limit: 10 + }).fetchAsync(); + + // Search in clock events + const clockEvents = await ClockEvents.find({ + ...filter, + $or: [ + { 'tickets.title': searchRegex }, + { 'tickets.github': searchRegex } + ] + }, { + sort: { startTime: -1 }, + limit: 10 + }).fetchAsync(); + + return { + tickets: tickets.map(t => ({ + title: t.title, + description: t.github || '', + totalTime: t.totalTime || 0, + date: t.updatedAt || t.createdAt + })), + sessions: clockEvents.map(ce => ({ + startTime: ce.startTime, + endTime: ce.endTime, + totalTime: ce.totalTime || 0, + ticketCount: ce.tickets?.length || 0 + })) + }; + }, + + // Search project history + async searchProjectHistory(query, teamId) { + check(query, String); + check(teamId, String); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + // Check team membership + const team = await Teams.findOneAsync({ _id: teamId, members: this.userId }); + if (!team) throw new Meteor.Error('not-authorized', 'Not a team member'); + + const searchRegex = new RegExp(query, 'i'); + + // Search all team tickets + const tickets = await Tickets.find({ + teamId, + $or: [ + { title: searchRegex }, + { github: searchRegex } + ] + }, { + sort: { updatedAt: -1 }, + limit: 15 + }).fetchAsync(); + + // Get usernames for context + const userIds = [...new Set(tickets.map(t => t.userId))]; + const users = await Meteor.users.find( + { _id: { $in: userIds } }, + { fields: { username: 1 } } + ).fetchAsync(); + + const userMap = {}; + users.forEach(u => userMap[u._id] = u.username); + + return { + teamName: team.name, + results: tickets.map(t => ({ + title: t.title, + description: t.github || '', + author: userMap[t.userId] || 'Unknown', + totalTime: t.totalTime || 0, + date: t.updatedAt || t.createdAt + })) + }; + }, + + // Save conversation + async saveOzwellConversation(teamId, messages, metadata = {}) { + check(teamId, String); + check(messages, Array); + check(metadata, Object); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + // Check team membership + const team = await Teams.findOneAsync({ _id: teamId, members: this.userId }); + if (!team) throw new Meteor.Error('not-authorized', 'Not a team member'); + + const conversation = { + teamId, + userId: this.userId, + messages, + metadata, + createdAt: new Date(), + updatedAt: new Date() + }; + + return await OzwellConversations.insertAsync(conversation); + }, + + // Get conversation history + async getOzwellConversations(teamId, limit = 10) { + check(teamId, String); + check(limit, Number); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + // Check team membership + const team = await Teams.findOneAsync({ _id: teamId, members: this.userId }); + if (!team) throw new Meteor.Error('not-authorized', 'Not a team member'); + + return await OzwellConversations.find( + { teamId, userId: this.userId }, + { sort: { updatedAt: -1 }, limit } + ).fetchAsync(); + } +}; \ No newline at end of file diff --git a/server/methods/ozwellPrompts.js b/server/methods/ozwellPrompts.js new file mode 100644 index 0000000..911c849 --- /dev/null +++ b/server/methods/ozwellPrompts.js @@ -0,0 +1,183 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { OzwellPrompts } from '../../collections.js'; + +export const ozwellPromptMethods = { + // Get all available prompts + async getOzwellPrompts() { + if (!this.userId) throw new Meteor.Error('not-authorized'); + + return await OzwellPrompts.find({}).fetchAsync(); + }, + + // Initialize default prompts (called on startup) + async initializeDefaultPrompts() { + const promptCount = await OzwellPrompts.find().countAsync(); + + if (promptCount === 0) { + const defaultPrompts = [ + { + id: 'draft-time-entry', + title: 'Help me draft a time entry', + description: 'Get assistance writing a detailed time entry for your work', + template: `Help me write a detailed time entry for my work today. Here's what I'm working on: + +Project: {{teamName}} +{{#if currentTicket}} +Current Activity: {{currentTicket.title}} +{{#if currentTicket.description}} +Notes/Reference: {{currentTicket.description}} +{{/if}} +{{/if}} + +{{#if recentActivity}} +Recent activities in this project: +{{#each recentActivity}} +- {{title}}{{#if description}} ({{description}}){{/if}} +{{/each}} +{{/if}} + +Please help me write a professional time entry that describes what I accomplished, any challenges I faced, and next steps. Make it detailed enough for project tracking but concise for time logging.`, + category: 'time-tracking', + contexts: ['ticket-form', 'time-entry'], + icon: 'clock', + systemMessage: 'You are a professional time tracking assistant. Help users write clear, detailed time entries that capture their work accomplishments, challenges, and progress. Focus on being specific and actionable.', + createdAt: new Date() + }, + { + id: 'summarize-daily-activity', + title: 'Summarize my activity today', + description: 'Create a summary of your work activities for the day', + template: `Please create a summary of my work activities for today. + +Project: {{teamName}} +User: {{user.username}} + +{{#if recentActivity}} +Activities worked on: +{{#each recentActivity}} +- {{title}}{{#if description}} - {{description}}{{/if}} + Time spent: {{totalTime}} seconds + Last updated: {{lastUpdated}} +{{/each}} +{{/if}} + +Please provide: +1. A brief overview of what I accomplished today +2. Key highlights or milestones reached +3. Any blockers or challenges encountered +4. Suggested priorities for tomorrow + +Make it suitable for sharing with team members or for personal reflection.`, + category: 'reporting', + contexts: ['end-of-day', 'summary'], + icon: 'chart-bar', + systemMessage: 'You are a professional work summary assistant. Help users create clear, organized summaries of their daily work that highlight accomplishments, identify challenges, and suggest next steps.', + createdAt: new Date() + }, + { + id: 'cross-link-related-work', + title: 'Find related work and cross-links', + description: 'Analyze current work and suggest connections to related activities', + template: `I'm working on: "{{currentText}}" + +Project context: +{{#if currentTicket}} +Current Activity: {{currentTicket.title}} +{{#if currentTicket.description}} +Description: {{currentTicket.description}} +{{/if}} +{{/if}} + +Project: {{teamName}} + +{{#if recentActivity}} +Other recent activities in this project: +{{#each recentActivity}} +- {{title}}{{#if description}} ({{description}}){{/if}} +{{/each}} +{{/if}} + +Please help me: +1. Identify connections between my current work and other activities +2. Suggest relevant cross-references or links to include +3. Recommend related work that might be helpful to reference +4. Improve the description to better connect with existing project work + +Focus on making my work more discoverable and connected to the broader project context.`, + category: 'organization', + contexts: ['ticket-form', 'note-taking'], + icon: 'link', + systemMessage: 'You are a project organization assistant. Help users connect their current work to related activities, suggest meaningful cross-references, and improve project coherence through better linking and categorization.', + createdAt: new Date() + }, + { + id: 'improve-activity-description', + title: 'Improve my activity description', + description: 'Get help making your activity description more clear and detailed', + template: `Please help me improve this activity description: + +Current text: "{{currentText}}" + +{{#if currentTicket}} +Activity: {{currentTicket.title}} +{{/if}} +Project: {{teamName}} + +Please help me: +1. Make the description more clear and specific +2. Add relevant technical details if appropriate +3. Ensure it's useful for future reference +4. Make it professional and well-structured + +Keep the core meaning but enhance clarity, detail, and usefulness for project tracking.`, + category: 'writing', + contexts: ['ticket-form', 'note-taking'], + icon: 'pencil', + systemMessage: 'You are a professional writing assistant specializing in technical documentation. Help users write clear, detailed, and well-structured activity descriptions that are useful for project tracking and future reference.', + createdAt: new Date() + }, + { + id: 'plan-work-session', + title: 'Help me plan my work session', + description: 'Get assistance planning and organizing your upcoming work', + template: `Help me plan my work session for {{teamName}}. + +{{#if currentTicket}} +I'm planning to work on: {{currentTicket.title}} +{{#if currentTicket.description}} +Current notes: {{currentTicket.description}} +{{/if}} +{{/if}} + +{{#if recentActivity}} +Recent project activities: +{{#each recentActivity}} +- {{title}}{{#if description}} ({{description}}){{/if}} +{{/each}} +{{/if}} + +Please help me: +1. Break down the work into manageable tasks +2. Suggest a logical order for completing tasks +3. Identify potential challenges or dependencies +4. Recommend time estimates for different parts +5. Suggest any preparatory work or research needed + +Focus on creating a clear, actionable plan that will help me work efficiently and track progress.`, + category: 'planning', + contexts: ['session-start', 'ticket-form'], + icon: 'clipboard-list', + systemMessage: 'You are a work planning assistant. Help users break down their work into manageable tasks, organize them logically, and create actionable plans that improve productivity and progress tracking.', + createdAt: new Date() + } + ]; + + for (const prompt of defaultPrompts) { + await OzwellPrompts.insertAsync(prompt); + } + + console.log('Initialized', defaultPrompts.length, 'default Ozwell prompts'); + } + } +}; \ No newline at end of file From 312a5ae38123e8076a1f6a09725ee2115026c0ef Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:37:19 -0400 Subject: [PATCH 03/57] Add 'Paste and Save' feature to OzwellModal Introduces a new 'Paste and Save' button in the Ozwell modal footer when a session URL is present. This feature allows users to copy AI-generated content from the chat, paste it from the clipboard, and save it, with validation to prevent saving console logs or irrelevant content. Includes user instructions and fallback for restricted clipboard access. --- client/components/ozwell/OzwellModal.html | 41 +++++++++++++------ client/components/ozwell/OzwellModal.js | 49 +++++++++++++++++++++++ 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html index 2c04e18..c9299b1 100644 --- a/client/components/ozwell/OzwellModal.html +++ b/client/components/ozwell/OzwellModal.html @@ -86,19 +86,36 @@

Choose how I can help you:

-
-
- {{#if selectedPrompt}} - Using prompt: {{selectedPrompt.title}} - {{else}} - Select a prompt to get started - {{/if}} +
+ {{#if ozwellSessionUrl}} + +
+ 💡 To save content:
+ 1. Select the AI response text in the chat above
+ 2. Copy it (Ctrl+C or Cmd+C)
+ 3. Click "Paste and Save"
-
- - + {{/if}} + +
+
+ {{#if selectedPrompt}} + Using prompt: {{selectedPrompt.title}} + {{else}} + Select a prompt to get started + {{/if}} +
+
+ + {{#if ozwellSessionUrl}} + + {{/if}} + +
diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js index cd6d249..6874532 100644 --- a/client/components/ozwell/OzwellModal.js +++ b/client/components/ozwell/OzwellModal.js @@ -271,6 +271,51 @@ Template.ozwellModal.onCreated(function () { }); }; + // Paste from clipboard and save + this.pasteAndSave = async function () { + try { + // Try to read from clipboard + const clipboardText = await navigator.clipboard.readText(); + + if (clipboardText && clipboardText.trim().length > 0) { + // Validate clipboard content - check if it looks like console logs or browser stuff + const lowerClipboard = clipboardText.toLowerCase(); + const isConsoleLog = lowerClipboard.includes('console') || + lowerClipboard.includes('hmr:') || + lowerClipboard.includes('.js?hash=') || + lowerClipboard.includes('ozwellbutton.js') || + lowerClipboard.includes('ozwellmodal.js') || + lowerClipboard.includes('meteor_js_resource') || + clipboardText.includes('@') && clipboardText.includes(':'); + + if (isConsoleLog) { + alert('❌ It looks like you copied console logs instead of Ozwell content.\n\n✅ Please:\n1. Click inside the Ozwell chat area above\n2. Select the AI-generated text (not the console)\n3. Copy it (Ctrl+C or Cmd+C)\n4. Click "Paste and Save" again'); + return; + } + + // Store the clipboard content + template.generatedContent.set(clipboardText.trim()); + console.log('Content from clipboard:', clipboardText.substring(0, 100) + '...'); + + // Perform autofill with clipboard content + template.performAutofill(); + } else { + // If no clipboard content, prompt user to manually copy + alert('📋 No content found in clipboard.\n\n✅ Please:\n1. Click inside the Ozwell chat area above\n2. Select the AI-generated text\n3. Copy it (Ctrl+C or Cmd+C)\n4. Click "Paste and Save" again'); + } + } catch (err) { + console.log('Clipboard access not available:', err); + // Fallback: prompt user with manual input + const manualContent = prompt('Clipboard access is restricted.\n\nPlease copy the content from Ozwell above and paste it here:'); + if (manualContent && manualContent.trim().length > 0) { + template.generatedContent.set(manualContent.trim()); + template.performAutofill(); + } else { + alert('No content provided. Please copy the text from Ozwell and try again.'); + } + } + }; + // Close Ozwell modal this.closeOzwell = function (save = false) { if (save) { @@ -540,6 +585,10 @@ Template.ozwellModal.events({ template.closeOzwell(true); }, + 'click #ozwell-paste-save'(event, template) { + template.pasteAndSave(); + }, + 'click .prompt-btn'(event, template) { const promptId = event.currentTarget.getAttribute('data-prompt-id'); const prompts = template.availablePrompts.get(); From 1449443c5964cb9fafd3f02db15f1707f616840e Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:05:42 -0400 Subject: [PATCH 04/57] Enhance Ozwell modal with rich context and autofill Improves the Ozwell modal by providing time tracking-specific prompts, richer context data for AI interactions, and enhanced content monitoring for autofill. TicketsPage now supplies detailed project statistics and activity summaries to support more informative AI-generated content. --- client/components/ozwell/OzwellModal.html | 11 +- client/components/ozwell/OzwellModal.js | 167 ++++++++++++++++++---- client/components/tickets/TicketsPage.js | 32 ++++- 3 files changed, 175 insertions(+), 35 deletions(-) diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html index c9299b1..2180dfe 100644 --- a/client/components/ozwell/OzwellModal.html +++ b/client/components/ozwell/OzwellModal.html @@ -90,29 +90,30 @@

Choose how I can help you:

{{#if ozwellSessionUrl}}
- 💡 To save content:
+ To save content:
1. Select the AI response text in the chat above
2. Copy it (Ctrl+C or Cmd+C)
3. Click "Paste and Save" + 4. Autofill feature coming soon
{{/if}}
{{#if selectedPrompt}} - Using prompt: {{selectedPrompt.title}} + Using: {{selectedPrompt.title}} for {{../teamName}} {{else}} - Select a prompt to get started + Select a time tracking prompt to get started {{/if}}
{{#if ozwellSessionUrl}} - {{/if}} -
diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js index 6874532..5b83e7a 100644 --- a/client/components/ozwell/OzwellModal.js +++ b/client/components/ozwell/OzwellModal.js @@ -42,13 +42,39 @@ Template.ozwellModal.onCreated(function () { this.availablePrompts.set(prompts); } else { console.error('Failed to load Ozwell prompts:', err); - // Fallback to basic prompts + // Fallback to time tracking specific prompts this.availablePrompts.set([ { - id: 'custom', - title: 'Help me write', - description: 'Get general writing assistance', - icon: 'pencil' + id: 'ticket-title', + title: 'Write ticket title', + description: 'Create a clear, concise ticket title', + icon: 'pencil', + template: 'Based on this work: "{{currentText}}" in project "{{teamName}}", create a professional ticket title that clearly describes what was accomplished. Make it concise and specific.', + systemMessage: 'You are a project management assistant. Create clear, professional ticket titles for time tracking entries.' + }, + { + id: 'ticket-description', + title: 'Write ticket description', + description: 'Generate detailed work description', + icon: 'chart-bar', + template: 'Project: {{teamName}}\nCurrent work notes: "{{currentText}}"\nRecent activity: {{recentActivitySummary}}\n\nPlease write a detailed ticket description that explains what was accomplished, including technical details and business value. Format it professionally.', + systemMessage: 'You are a technical documentation assistant. Write clear, detailed descriptions of development work for time tracking and project management.' + }, + { + id: 'daily-summary', + title: 'Daily work summary', + description: 'Summarize daily progress', + icon: 'clock', + template: 'Project: {{teamName}}\nToday\'s work: "{{currentText}}"\nTime spent: {{totalTimeToday}}\nRecent tickets: {{recentActivitySummary}}\n\nCreate a professional daily summary of work accomplished, highlighting key achievements and progress made.', + systemMessage: 'You are a productivity assistant. Create concise daily work summaries for time tracking and reporting.' + }, + { + id: 'status-update', + title: 'Project status update', + description: 'Generate project status report', + icon: 'link', + template: 'Project: {{teamName}}\nCurrent progress: "{{currentText}}"\nRecent work: {{recentActivitySummary}}\nTotal time invested: {{totalProjectTime}}\n\nGenerate a professional status update for stakeholders, highlighting progress, current state, and next steps.', + systemMessage: 'You are a project communication specialist. Create clear status updates for stakeholders and team members.' } ]); } @@ -108,31 +134,72 @@ Template.ozwellModal.onCreated(function () { const iframe = document.querySelector('#ozwell-iframe'); if (iframe && iframe.contentWindow) { const selectedPrompt = template.selectedPrompt.get(); + + // Process template variables + let promptText = selectedPrompt?.template || selectedPrompt?.title || prompt.template || prompt.title; + if (promptText && context) { + promptText = promptText + .replace(/\{\{teamName\}\}/g, context.teamName || 'Current Team') + .replace(/\{\{currentText\}\}/g, context.currentText || '') + .replace(/\{\{recentActivitySummary\}\}/g, context.recentActivitySummary || 'No recent activity') + .replace(/\{\{totalTimeToday\}\}/g, context.projectStats?.formattedTimeToday || '0m') + .replace(/\{\{totalProjectTime\}\}/g, context.projectStats?.formattedProjectTime || '0m'); + } + + // Rich MCP context with project data const contextData = { type: 'mcp-context', + protocol: 'model-context-protocol', + version: '1.0', context: { - teamName: context.teamName || 'Current Team', - currentText: context.currentText || '', - prompt: selectedPrompt?.template || selectedPrompt?.title || prompt.template || prompt.title, - systemMessage: selectedPrompt?.systemMessage || prompt.systemMessage || 'You are a helpful writing assistant.', - projectType: 'Time Tracking Application', - instructions: selectedPrompt?.description || 'Help the user improve their content.', - ...context + // Core project info + project: { + name: context.teamName || 'Current Team', + type: 'Time Tracking Application', + id: context.teamId + }, + + // User context + user: context.user || {}, + + // Current work context + currentWork: { + text: context.currentText || '', + ticket: context.currentTicket, + inputType: 'ticket_description' + }, + + // Project statistics and history + projectStats: context.projectStats || {}, + recentActivity: context.recentActivity || [], + + // AI prompt configuration + prompt: { + id: selectedPrompt?.id, + title: selectedPrompt?.title, + template: promptText, + systemMessage: selectedPrompt?.systemMessage || 'You are a helpful assistant for time tracking and project management.', + instructions: selectedPrompt?.description + }, + + // Application context + application: { + name: 'TimeHarbor', + domain: 'time-tracking', + capabilities: ['ticket-management', 'time-tracking', 'project-reporting'] + } } }; - console.log('Sending context to Ozwell:', contextData); + console.log('Sending rich MCP context to Ozwell:', contextData); iframe.contentWindow.postMessage(contextData, 'https://ai.bluehive.com'); - // Also send the prompt as initial message if available - if (selectedPrompt?.template && context.currentText) { + // Send the processed prompt as initial message + if (promptText && promptText.length > 0) { setTimeout(() => { - const promptMessage = selectedPrompt.template.replace('{{currentText}}', context.currentText || '') - .replace('{{teamName}}', context.teamName || 'Current Team'); - iframe.contentWindow.postMessage({ type: 'ozwell-send-message', - message: promptMessage + message: promptText }, 'https://ai.bluehive.com'); }, 1000); } @@ -231,6 +298,43 @@ Template.ozwellModal.onCreated(function () { console.log('Received generic message:', data.message.substring(0, 100) + '...'); } + // Enhanced content monitoring for real autofill + // Monitor for any message that might contain AI-generated content + if (data.type === 'messageAdded' || data.type === 'messagesUpdated' || data.type === 'conversationUpdated') { + // Try to extract content from various data structures + let extractedContent = null; + + if (data.messages && Array.isArray(data.messages) && data.messages.length > 0) { + // Get the last message from the conversation + const lastMessage = data.messages[data.messages.length - 1]; + if (lastMessage.role === 'assistant' && lastMessage.content) { + extractedContent = lastMessage.content; + } + } else if (data.message && data.message.content && data.message.role === 'assistant') { + extractedContent = data.message.content; + } else if (data.content && typeof data.content === 'string') { + extractedContent = data.content; + } else if (data.text && typeof data.text === 'string') { + extractedContent = data.text; + } + + if (extractedContent && extractedContent.length > 20) { + template.generatedContent.set(extractedContent); + template.canSave.set(true); + console.log('Captured AI content automatically:', extractedContent.substring(0, 100) + '...'); + } + } + + // Also monitor for clipboard events if Ozwell sends them + if (data.type === 'clipboardUpdate' || data.type === 'textCopied') { + if (data.content || data.text) { + const content = data.content || data.text; + template.generatedContent.set(content); + template.canSave.set(true); + console.log('Content captured from clipboard event:', content.substring(0, 100) + '...'); + } + } + // For any message from Ozwell iframe, enable save button (fallback) if (!template.canSave.get() && template.sessionReady.get()) { setTimeout(() => { @@ -319,30 +423,39 @@ Template.ozwellModal.onCreated(function () { // Close Ozwell modal this.closeOzwell = function (save = false) { if (save) { - // Try multiple ways to get content from Ozwell + // First check if we already have content from postMessage monitoring + let existingContent = template.generatedContent.get(); + if (existingContent && existingContent.trim().length > 0) { + console.log('Using already captured content:', existingContent.substring(0, 100) + '...'); + template.performAutofill(); + return; + } + + // Try to get content from Ozwell iframe const iframe = document.querySelector('#ozwell-iframe'); if (iframe && iframe.contentWindow && template.sessionReady.get()) { - // First, try to send specific postMessage requests to get content - console.log('Requesting content from Ozwell iframe...'); + console.log('Attempting intelligent content extraction from Ozwell...'); // Try different postMessage requests that Ozwell might respond to const contentRequests = [ + { type: 'get-conversation' }, { type: 'get-current-content' }, { type: 'export-content' }, - { channel: 'iframe-basic', message: 'getContent' }, - { channel: 'IframeSync', type: 'getContent' }, { type: 'get-last-message' }, - { type: 'export-conversation' } + { channel: 'iframe-basic', message: 'getMessages' }, + { channel: 'IframeSync', type: 'getContent' }, + { type: 'export-conversation' }, + { type: 'get-chat-history' } ]; contentRequests.forEach((request, index) => { setTimeout(() => { iframe.contentWindow.postMessage(request, 'https://ai.bluehive.com'); - }, index * 200); + }, index * 150); }); - // Wait longer for responses and then try to extract content + // Give time for responses and then attempt content extraction setTimeout(() => { // If we still don't have content, try to access iframe DOM directly let extractedContent = template.generatedContent.get(); diff --git a/client/components/tickets/TicketsPage.js b/client/components/tickets/TicketsPage.js index 5442baa..f7a2197 100644 --- a/client/components/tickets/TicketsPage.js +++ b/client/components/tickets/TicketsPage.js @@ -131,23 +131,49 @@ Template.tickets.onCreated(function () { { sort: { updatedAt: -1 }, limit: 5 } ).fetch(); + // Calculate time summaries + const totalProjectTime = recentTickets.reduce((sum, ticket) => sum + (ticket.totalTime || 0), 0); + const totalTimeToday = recentTickets + .filter(ticket => { + const today = new Date(); + const ticketDate = new Date(ticket.updatedAt || ticket.createdAt); + return ticketDate.toDateString() === today.toDateString(); + }) + .reduce((sum, ticket) => sum + (ticket.totalTime || 0), 0); + + // Create rich activity summary + const recentActivitySummary = recentTickets.length > 0 + ? recentTickets.map(ticket => `• ${ticket.title} (${Math.round((ticket.totalTime || 0) / 60)}min)`).join('\n') + : 'No recent activity'; + return { teamId, teamName: team?.name || 'Unknown Project', user: { - username: Meteor.user()?.username || 'Unknown User' + username: Meteor.user()?.username || 'Unknown User', + email: Meteor.user()?.emails?.[0]?.address || '' }, currentTicket: activeTicket ? { title: activeTicket.title, description: activeTicket.github || '', status: 'active', - totalTime: activeTicket.totalTime || 0 + totalTime: activeTicket.totalTime || 0, + formattedTime: `${Math.floor((activeTicket.totalTime || 0) / 3600)}h ${Math.floor(((activeTicket.totalTime || 0) % 3600) / 60)}m` } : null, + projectStats: { + totalTickets: recentTickets.length, + totalProjectTime: Math.round(totalProjectTime / 60), // in minutes + totalTimeToday: Math.round(totalTimeToday / 60), // in minutes + formattedProjectTime: `${Math.floor(totalProjectTime / 3600)}h ${Math.floor((totalProjectTime % 3600) / 60)}m`, + formattedTimeToday: `${Math.floor(totalTimeToday / 3600)}h ${Math.floor((totalTimeToday % 3600) / 60)}m` + }, + recentActivitySummary, recentActivity: recentTickets.map(ticket => ({ title: ticket.title, description: ticket.github || '', totalTime: ticket.totalTime || 0, - lastUpdated: ticket.updatedAt || ticket.createdAt + lastUpdated: ticket.updatedAt || ticket.createdAt, + formattedTime: `${Math.round((ticket.totalTime || 0) / 60)}min` })) }; }; From ea8fb1e3fa9dbba1d443997ac493b4f3ca9aaa12 Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:45:54 -0400 Subject: [PATCH 05/57] Change content extraction in OzwellModal Trying to get message content extraction working from Ozwell. Changed content capture from iframe and postMessage events. Added a catch-all extraction method, more comprehensive DOM selectors, and prioritize captured content for autofill. Refactored closeOzwell logic to use new extraction strategies and fallback messaging. --- client/components/ozwell/OzwellModal.js | 317 ++++++++++++++++-------- 1 file changed, 212 insertions(+), 105 deletions(-) diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js index 5b83e7a..4a8e63b 100644 --- a/client/components/ozwell/OzwellModal.js +++ b/client/components/ozwell/OzwellModal.js @@ -19,6 +19,7 @@ Template.ozwellModal.onCreated(function () { this.currentTeamId = new ReactiveVar(null); this.sessionReady = new ReactiveVar(false); this.generatedContent = new ReactiveVar(null); // Store content from Ozwell + this.capturedContent = new ReactiveVar(null); // Store captured AI content from postMessage // Store reference to this template instance globally for access from other components window.ozwellModalInstance = this; @@ -208,6 +209,24 @@ Template.ozwellModal.onCreated(function () { // Session is rendered and ready template.sessionReady.set(true); template.canSave.set(true); + } else if (data.type === 'ai-response' || data.type === 'message' || data.message) { + // Capture AI-generated content from various message formats + let content = null; + + if (data.content) { + content = data.content; + } else if (data.text) { + content = data.text; + } else if (data.message && typeof data.message === 'string') { + content = data.message; + } else if (data.response) { + content = data.response; + } + + if (content && typeof content === 'string' && content.trim().length > 10) { + console.log('Captured AI response:', content); + template.capturedContent.set(content); + } } else if (data.type === 'ozwell-ready') { template.sessionReady.set(true); @@ -296,6 +315,23 @@ Template.ozwellModal.onCreated(function () { template.generatedContent.set(data.message); template.canSave.set(true); console.log('Received generic message:', data.message.substring(0, 100) + '...'); + } else if (data.channel === 'iframe-basic' && data.message === 'sessionRendered') { + // Session is ready, try to extract content after a delay + template.canSave.set(true); + setTimeout(() => { + template.extractContentFromIframe(); + }, 3000); + } else if (data.type && (data.type.includes('conversation') || data.type.includes('chat'))) { + // Handle conversation data + if (data.messages && Array.isArray(data.messages) && data.messages.length > 0) { + const lastMessage = data.messages[data.messages.length - 1]; + if (lastMessage.content || lastMessage.text || lastMessage.message) { + const content = lastMessage.content || lastMessage.text || lastMessage.message; + template.generatedContent.set(content); + template.canSave.set(true); + console.log('Received conversation content:', content.substring(0, 100) + '...'); + } + } } // Enhanced content monitoring for real autofill @@ -341,11 +377,167 @@ Template.ozwellModal.onCreated(function () { template.canSave.set(true); }, 3000); } + + // FINAL CATCH-ALL: Try to extract content from any unhandled message + if (!template.capturedContent.get() && data) { + // Look for any text content in the entire data object + const searchForContent = (obj, depth = 0) => { + if (depth > 3) return null; // Prevent deep recursion + + if (typeof obj === 'string' && obj.length > 30 && + !obj.includes('sessionRendered') && + !obj.includes('ready') && + !obj.includes('http') && + !obj.includes('iframe')) { + return obj; + } + + if (obj && typeof obj === 'object') { + for (const key in obj) { + if (key === 'content' || key === 'text' || key === 'message' || key === 'response') { + const value = obj[key]; + if (typeof value === 'string' && value.length > 30) { + return value; + } + } + + const found = searchForContent(obj[key], depth + 1); + if (found) return found; + } + } + + return null; + }; + + const foundContent = searchForContent(data); + if (foundContent) { + console.log('Catch-all found content:', foundContent.substring(0, 100) + '...'); + template.capturedContent.set(foundContent); + } + } }; window.addEventListener('message', template.messageHandler); }; + // Extract content from iframe when postMessage doesn't work + this.extractContentFromIframe = function () { + const iframe = document.querySelector('#ozwell-iframe'); + if (!iframe || !iframe.contentWindow) return; + + console.log('Attempting to extract content from iframe...'); + + // Try multiple postMessage approaches first + const contentRequests = [ + { type: 'get-conversation' }, + { type: 'get-current-content' }, + { type: 'export-content' }, + { type: 'get-last-message' }, + { type: 'get-chat-history' }, + { channel: 'iframe-basic', message: 'getMessages' }, + { channel: 'IframeSync', type: 'getContent' }, + { action: 'getChatHistory' }, + { action: 'getLastResponse' }, + { command: 'export' } + ]; + + contentRequests.forEach((request, index) => { + setTimeout(() => { + iframe.contentWindow.postMessage(request, 'https://ai.bluehive.com'); + }, index * 100); + }); + + // After trying postMessage, attempt DOM extraction + setTimeout(() => { + try { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + if (!iframeDoc) { + console.log('Cannot access iframe document (CORS)'); + return; + } + + // Look for chat messages with more comprehensive selectors + const messageSelectors = [ + '[data-testid*="message"]', + '[class*="message"]', + '[class*="chat"]', + '[class*="response"]', + '[class*="content"]', + '.message-content', + '.chat-message', + '.response-text', + '.message-text', + '.ai-response', + '.assistant-message', + '[role="log"] > div:last-child', + '[role="log"] [class*="message"]:last-child', + '.conversation [class*="message"]:last-child', + 'div[data-message-id]', + '.prose', + 'article', + 'main [class*="text"]' + ]; + + let extractedContent = ''; + + for (const selector of messageSelectors) { + try { + const elements = iframeDoc.querySelectorAll(selector); + if (elements.length > 0) { + const lastElement = elements[elements.length - 1]; + const text = lastElement.textContent || lastElement.innerText; + + if (text && text.trim().length > 20 && + !text.toLowerCase().includes('type a message') && + !text.toLowerCase().includes('send') && + !text.toLowerCase().includes('loading') && + !text.toLowerCase().includes('connecting')) { + + extractedContent = text.trim(); + console.log(`Content extracted using selector "${selector}":`, extractedContent.substring(0, 150) + '...'); + break; + } + } + } catch (e) { + // Skip selector if it fails + } + } + + // If still no content, try to find any meaningful text + if (!extractedContent) { + const allText = iframeDoc.body ? (iframeDoc.body.textContent || iframeDoc.body.innerText) : ''; + const lines = allText.split('\n') + .map(line => line.trim()) + .filter(line => + line.length > 30 && + !line.toLowerCase().includes('type a message') && + !line.toLowerCase().includes('ozwell') && + !line.toLowerCase().includes('send') && + !line.toLowerCase().includes('cancel') && + !line.toLowerCase().includes('loading') && + !line.toLowerCase().includes('connecting') && + !line.toLowerCase().includes('powered by') + ); + + if (lines.length > 0) { + extractedContent = lines[lines.length - 1]; + console.log('Content extracted from body text:', extractedContent.substring(0, 150) + '...'); + } + } + + if (extractedContent) { + template.generatedContent.set(extractedContent); + console.log('Successfully extracted content from iframe'); + } else { + console.log('No meaningful content found in iframe'); + } + + } catch (e) { + console.log('Cannot access iframe content due to CORS:', e.message); + } + }, 1000); + }; + // Initialize Ozwell session this.initializeOzwellSession = function (prompt) { const teamId = template.currentTeamId.get(); @@ -423,116 +615,31 @@ Template.ozwellModal.onCreated(function () { // Close Ozwell modal this.closeOzwell = function (save = false) { if (save) { - // First check if we already have content from postMessage monitoring - let existingContent = template.generatedContent.get(); - if (existingContent && existingContent.trim().length > 0) { - console.log('Using already captured content:', existingContent.substring(0, 100) + '...'); - template.performAutofill(); - return; - } - - // Try to get content from Ozwell iframe - const iframe = document.querySelector('#ozwell-iframe'); - if (iframe && iframe.contentWindow && template.sessionReady.get()) { - - console.log('Attempting intelligent content extraction from Ozwell...'); - - // Try different postMessage requests that Ozwell might respond to - const contentRequests = [ - { type: 'get-conversation' }, - { type: 'get-current-content' }, - { type: 'export-content' }, - { type: 'get-last-message' }, - { channel: 'iframe-basic', message: 'getMessages' }, - { channel: 'IframeSync', type: 'getContent' }, - { type: 'export-conversation' }, - { type: 'get-chat-history' } - ]; + // First try to use captured content from postMessage + let capturedContent = template.capturedContent.get(); - contentRequests.forEach((request, index) => { - setTimeout(() => { - iframe.contentWindow.postMessage(request, 'https://ai.bluehive.com'); - }, index * 150); - }); + if (capturedContent && capturedContent.trim().length > 10) { + console.log('Using captured content from postMessage:', capturedContent.substring(0, 100) + '...'); + template.generatedContent.set(capturedContent); + template.performAutofill(); + } else { + // Fallback: try iframe extraction + template.extractContentFromIframe(); - // Give time for responses and then attempt content extraction + // Wait a moment for extraction then proceed with autofill setTimeout(() => { - // If we still don't have content, try to access iframe DOM directly - let extractedContent = template.generatedContent.get(); - - if (!extractedContent || extractedContent.trim().length === 0) { - try { - // Try to access iframe content directly - const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; - - // Try multiple selectors to find content - const selectors = [ - '.message-content', - '.chat-message', - '.response-text', - '.message-text', - '.content', - '[data-message]', - '.message:last-child', - '.chat-content .message:last-child', - 'p:last-child', - 'div[role="textbox"]', - '.ql-editor', - '.text-content' - ]; - - for (const selector of selectors) { - const elements = iframeDoc.querySelectorAll(selector); - if (elements.length > 0) { - const lastElement = elements[elements.length - 1]; - const content = lastElement.textContent || lastElement.innerText; - if (content && content.trim().length > 10 && !content.includes('Type a message')) { - extractedContent = content.trim(); - console.log(`Content extracted using selector ${selector}:`, extractedContent.substring(0, 100) + '...'); - break; - } - } - } - - // If still no good content, try to get all text from the iframe - if (!extractedContent || extractedContent.trim().length === 0) { - const allText = iframeDoc.body.textContent || iframeDoc.body.innerText; - // Look for meaningful content (not UI text) - const lines = allText.split('\n').map(line => line.trim()).filter(line => - line.length > 20 && - !line.includes('Type a message') && - !line.includes('Ozwell AI') && - !line.includes('Send') && - !line.includes('Cancel') && - !line.toLowerCase().includes('loading') && - !line.toLowerCase().includes('connecting') - ); - - if (lines.length > 0) { - extractedContent = lines[lines.length - 1]; // Get the last meaningful line - console.log('Content extracted from body text:', extractedContent.substring(0, 100) + '...'); - } - } - - } catch (e) { - console.log('Cannot access iframe content directly (CORS):', e); - // This is expected due to CORS restrictions - } - } - - if (extractedContent && extractedContent.trim().length > 0) { - template.generatedContent.set(extractedContent); + let existingContent = template.generatedContent.get(); + if (existingContent && existingContent.trim().length > 0) { + console.log('Using extracted content:', existingContent.substring(0, 100) + '...'); + template.performAutofill(); + } else { + console.log('No content captured, using fallback message'); + template.generatedContent.set('[No AI content found - please try copying manually]'); + template.performAutofill(); } - - template.performAutofill(); - }, 1500); - - // Don't close immediately if we're saving - wait for autofill - return; - } else { - // If no iframe or not ready, try to use any stored content - template.performAutofill(); + }, 1000); } + return; } template.closeModal(); From ae1140e54e4fc912b8a6401640db193f36fb2c4a Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Fri, 19 Sep 2025 19:29:58 -0400 Subject: [PATCH 06/57] Simplified OzwellModal save workflow and UI Merged the copy and save instructions into a single step and removed the Auto Save button from the modal. Updated the save logic to use a direct clipboard check when saving, streamlining the user experience and code. --- client/components/ozwell/OzwellModal.html | 9 ++----- client/components/ozwell/OzwellModal.js | 31 +++-------------------- 2 files changed, 6 insertions(+), 34 deletions(-) diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html index 2180dfe..f21715a 100644 --- a/client/components/ozwell/OzwellModal.html +++ b/client/components/ozwell/OzwellModal.html @@ -92,9 +92,7 @@

Choose how I can help you:

To save content:
1. Select the AI response text in the chat above
- 2. Copy it (Ctrl+C or Cmd+C)
- 3. Click "Paste and Save" - 4. Autofill feature coming soon + 2. Copy it (Ctrl+C or Cmd+C) and click "Paste and Save"
{{/if}} @@ -109,13 +107,10 @@

Choose how I can help you:

{{#if ozwellSessionUrl}} - {{/if}} -
diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js index 4a8e63b..a76a091 100644 --- a/client/components/ozwell/OzwellModal.js +++ b/client/components/ozwell/OzwellModal.js @@ -615,37 +615,14 @@ Template.ozwellModal.onCreated(function () { // Close Ozwell modal this.closeOzwell = function (save = false) { if (save) { - // First try to use captured content from postMessage - let capturedContent = template.capturedContent.get(); - - if (capturedContent && capturedContent.trim().length > 10) { - console.log('Using captured content from postMessage:', capturedContent.substring(0, 100) + '...'); - template.generatedContent.set(capturedContent); - template.performAutofill(); - } else { - // Fallback: try iframe extraction - template.extractContentFromIframe(); - - // Wait a moment for extraction then proceed with autofill - setTimeout(() => { - let existingContent = template.generatedContent.get(); - if (existingContent && existingContent.trim().length > 0) { - console.log('Using extracted content:', existingContent.substring(0, 100) + '...'); - template.performAutofill(); - } else { - console.log('No content captured, using fallback message'); - template.generatedContent.set('[No AI content found - please try copying manually]'); - template.performAutofill(); - } - }, 1000); - } + // Simple approach: just immediately check clipboard for content + console.log('Auto Save clicked - checking clipboard immediately...'); + template.pasteAndSave(); return; } template.closeModal(); - }; - - // Perform the actual autofill + }; // Perform the actual autofill this.performAutofill = function () { let content = template.generatedContent.get(); const inputElement = template.currentInputElement.get(); From 20d280454f83ad8f607b255a759c55daf642d7d7 Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Sat, 20 Sep 2025 15:04:23 -0400 Subject: [PATCH 07/57] Update OzwellModal.js --- client/components/ozwell/OzwellModal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js index a76a091..29e10ea 100644 --- a/client/components/ozwell/OzwellModal.js +++ b/client/components/ozwell/OzwellModal.js @@ -585,7 +585,7 @@ Template.ozwellModal.onCreated(function () { clipboardText.includes('@') && clipboardText.includes(':'); if (isConsoleLog) { - alert('❌ It looks like you copied console logs instead of Ozwell content.\n\n✅ Please:\n1. Click inside the Ozwell chat area above\n2. Select the AI-generated text (not the console)\n3. Copy it (Ctrl+C or Cmd+C)\n4. Click "Paste and Save" again'); + alert('It looks like you copied console logs instead of Ozwell content.\n\nPlease:\n1. Click inside the Ozwell chat area above\n2. Select the AI-generated text (not the console)\n3. Copy it (Ctrl+C or Cmd+C)\n4. Click "Paste and Save" again'); return; } @@ -597,7 +597,7 @@ Template.ozwellModal.onCreated(function () { template.performAutofill(); } else { // If no clipboard content, prompt user to manually copy - alert('📋 No content found in clipboard.\n\n✅ Please:\n1. Click inside the Ozwell chat area above\n2. Select the AI-generated text\n3. Copy it (Ctrl+C or Cmd+C)\n4. Click "Paste and Save" again'); + alert('No content found in clipboard.\n\nPlease:\n1. Click inside the Ozwell chat area above\n2. Select the AI-generated text\n3. Copy it (Ctrl+C or Cmd+C)\n4. Click "Paste and Save" again'); } } catch (err) { console.log('Clipboard access not available:', err); From af1d0b7ef1fdf58b28c7b5f6f962fe528251eab1 Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:45:36 -0400 Subject: [PATCH 08/57] Refactor Ozwell modal to use reference server API Replaces iframe-based Ozwell assistant with direct calls to a reference server API for chat and autofill functionality. Updates modal UI, conversation flow, and settings page to support custom server URL and model selection. Adds new server method for reference assistant and improves prompt/context handling throughout. --- client/components/ozwell/OzwellModal.html | 164 +-- client/components/ozwell/OzwellModal.js | 1018 ++++++------------ client/components/settings/SettingsPage.html | 22 +- client/components/settings/SettingsPage.js | 31 +- server/main.js | 2 + server/methods/ozwell.js | 45 +- server/methods/referenceAssistant.js | 60 ++ 7 files changed, 536 insertions(+), 806 deletions(-) create mode 100644 server/methods/referenceAssistant.js diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html index f21715a..8167004 100644 --- a/client/components/ozwell/OzwellModal.html +++ b/client/components/ozwell/OzwellModal.html @@ -1,41 +1,89 @@ \ No newline at end of file + diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js index 29e10ea..10755d6 100644 --- a/client/components/ozwell/OzwellModal.js +++ b/client/components/ozwell/OzwellModal.js @@ -1,763 +1,360 @@ import { Template } from 'meteor/templating'; import { ReactiveVar } from 'meteor/reactive-var'; +import { Meteor } from 'meteor/meteor'; -// Import the HTML template import './OzwellModal.html'; -// Template helpers and events for OzwellModal +const DEFAULT_SYSTEM_MESSAGE = 'You are a helpful assistant for time tracking and project management.'; + +const FALLBACK_PROMPTS = [ + { + id: 'ticket-title', + title: 'Write ticket title', + description: 'Create a clear, concise ticket title', + icon: 'pencil', + template: 'Based on this work: "{{currentText}}" in project "{{teamName}}", create a professional ticket title that clearly describes what was accomplished. Make it concise and specific.', + systemMessage: 'You are a project management assistant. Create clear, professional ticket titles for time tracking entries.' + }, + { + id: 'ticket-description', + title: 'Write ticket description', + description: 'Generate detailed work description', + icon: 'chart-bar', + template: 'Project: {{teamName}}\nCurrent work notes: "{{currentText}}"\nRecent activity: {{recentActivitySummary}}\n\nPlease write a detailed ticket description that explains what was accomplished, including technical details and business value. Format it professionally.', + systemMessage: 'You are a technical documentation assistant. Write clear, detailed descriptions of development work for time tracking and project management.' + }, + { + id: 'daily-summary', + title: 'Daily work summary', + description: 'Summarize daily progress', + icon: 'clock', + template: 'Project: {{teamName}}\nToday\'s work: "{{currentText}}"\nTime spent: {{totalTimeToday}}\nRecent tickets: {{recentActivitySummary}}\n\nCreate a professional daily summary of work accomplished, highlighting key achievements and progress made.', + systemMessage: 'You are a productivity assistant. Create concise daily work summaries for time tracking and reporting.' + }, + { + id: 'status-update', + title: 'Project status update', + description: 'Generate project status report', + icon: 'link', + template: 'Project: {{teamName}}\nCurrent progress: "{{currentText}}"\nRecent work: {{recentActivitySummary}}\nTotal time invested: {{totalProjectTime}}\n\nGenerate a professional status update for stakeholders, highlighting progress, current state, and next steps.', + systemMessage: 'You are a project communication specialist. Create clear status updates for stakeholders and team members.' + } +]; + Template.ozwellModal.onCreated(function () { - // Add reference for methods that need access to template const template = this; - this.isOzwellOpen = new ReactiveVar(false); - this.selectedPrompt = new ReactiveVar(null); - this.ozwellSessionUrl = new ReactiveVar(null); - this.availablePrompts = new ReactiveVar([]); - this.canSave = new ReactiveVar(false); - this.currentContext = new ReactiveVar(null); - this.currentInputElement = new ReactiveVar(null); - this.currentTeamId = new ReactiveVar(null); - this.sessionReady = new ReactiveVar(false); - this.generatedContent = new ReactiveVar(null); // Store content from Ozwell - this.capturedContent = new ReactiveVar(null); // Store captured AI content from postMessage - - // Store reference to this template instance globally for access from other components - window.ozwellModalInstance = this; - - // Set up global iframe loaded handler - window.ozwellIframeLoaded = function () { - console.log('Ozwell iframe loaded'); - if (window.ozwellModalInstance) { - // Enable save button after iframe loads - setTimeout(() => { - window.ozwellModalInstance.canSave.set(true); - console.log('Save button enabled after iframe load'); - }, 3000); - } + template.isOzwellOpen = new ReactiveVar(false); + template.selectedPrompt = new ReactiveVar(null); + template.availablePrompts = new ReactiveVar([]); + template.currentContext = new ReactiveVar(null); + template.currentInputElement = new ReactiveVar(null); + template.currentTeamId = new ReactiveVar(null); + template.messages = new ReactiveVar([]); + template.composerText = new ReactiveVar(''); + template.contextSummary = new ReactiveVar(''); + template.systemMessage = new ReactiveVar(DEFAULT_SYSTEM_MESSAGE); + template.generatedContent = new ReactiveVar(null); + template.canSave = new ReactiveVar(false); + template.isGenerating = new ReactiveVar(false); + template.errorMessage = new ReactiveVar(null); + template.headerSubtitle = new ReactiveVar('Ready to help with your work.'); + + // Expose template instance for global access + window.ozwellModalInstance = template; + + const replaceTemplateVariables = (text = '', context = {}) => { + if (!text) return ''; + + return text + .replace(/\{\{teamName\}\}/g, context.teamName || 'Current Project') + .replace(/\{\{currentText\}\}/g, context.currentText || '') + .replace(/\{\{recentActivitySummary\}\}/g, context.recentActivitySummary || 'No recent activity') + .replace(/\{\{totalTimeToday\}\}/g, context.projectStats?.formattedTimeToday || '0m') + .replace(/\{\{totalProjectTime\}\}/g, context.projectStats?.formattedProjectTime || '0m'); }; - // Define loadPrompts method first - this.loadPrompts = function () { - Meteor.call('getOzwellPrompts', (err, prompts) => { - if (!err && prompts) { - this.availablePrompts.set(prompts); - } else { - console.error('Failed to load Ozwell prompts:', err); - // Fallback to time tracking specific prompts - this.availablePrompts.set([ - { - id: 'ticket-title', - title: 'Write ticket title', - description: 'Create a clear, concise ticket title', - icon: 'pencil', - template: 'Based on this work: "{{currentText}}" in project "{{teamName}}", create a professional ticket title that clearly describes what was accomplished. Make it concise and specific.', - systemMessage: 'You are a project management assistant. Create clear, professional ticket titles for time tracking entries.' - }, - { - id: 'ticket-description', - title: 'Write ticket description', - description: 'Generate detailed work description', - icon: 'chart-bar', - template: 'Project: {{teamName}}\nCurrent work notes: "{{currentText}}"\nRecent activity: {{recentActivitySummary}}\n\nPlease write a detailed ticket description that explains what was accomplished, including technical details and business value. Format it professionally.', - systemMessage: 'You are a technical documentation assistant. Write clear, detailed descriptions of development work for time tracking and project management.' - }, - { - id: 'daily-summary', - title: 'Daily work summary', - description: 'Summarize daily progress', - icon: 'clock', - template: 'Project: {{teamName}}\nToday\'s work: "{{currentText}}"\nTime spent: {{totalTimeToday}}\nRecent tickets: {{recentActivitySummary}}\n\nCreate a professional daily summary of work accomplished, highlighting key achievements and progress made.', - systemMessage: 'You are a productivity assistant. Create concise daily work summaries for time tracking and reporting.' - }, - { - id: 'status-update', - title: 'Project status update', - description: 'Generate project status report', - icon: 'link', - template: 'Project: {{teamName}}\nCurrent progress: "{{currentText}}"\nRecent work: {{recentActivitySummary}}\nTotal time invested: {{totalProjectTime}}\n\nGenerate a professional status update for stakeholders, highlighting progress, current state, and next steps.', - systemMessage: 'You are a project communication specialist. Create clear status updates for stakeholders and team members.' - } - ]); - } - }); - }; - - // Load available prompts - this.loadPrompts(); + const buildContextSummary = (context = {}) => { + const summary = []; - // Open Ozwell modal with context - this.openOzwell = function (inputElement, context = {}) { - // Check if user has Ozwell configured - const user = Meteor.user(); - if (!user?.profile?.ozwellEnabled) { - alert('Please configure Ozwell in your settings first.'); - return; + if (context.teamName) { + summary.push(`Project: ${context.teamName}`); } - template.currentInputElement.set(inputElement); - template.currentContext.set(context); - template.currentTeamId.set(context.teamId); - template.isOzwellOpen.set(true); - template.canSave.set(false); - template.sessionReady.set(false); - - // Reset state - template.selectedPrompt.set(null); - template.ozwellSessionUrl.set(null); - }; - - // Set up postMessage listener for iframe communication - this.setupPostMessageListener = function (prompt, context) { - // Clean up any existing listener - if (template.messageHandler) { - window.removeEventListener('message', template.messageHandler); + if (context.user?.username) { + summary.push(`User: ${context.user.username}`); } - template.messageHandler = function (event) { - // Verify origin for security - if (event.origin !== 'https://ai.bluehive.com') { - return; + if (context.currentTicket?.title) { + summary.push(`Current Activity: ${context.currentTicket.title}`); + if (context.currentTicket.description) { + summary.push(`Details: ${context.currentTicket.description}`); } + } - const data = event.data; - console.log('Received postMessage from Ozwell:', data); - - // Handle different message formats from Ozwell - if (data.channel === 'IframeSync' && data.type === 'ready') { - template.sessionReady.set(true); - - // Enable save button after a short delay since Ozwell is ready - setTimeout(() => { - template.canSave.set(true); - }, 2000); - - // Send initial context to Ozwell - const iframe = document.querySelector('#ozwell-iframe'); - if (iframe && iframe.contentWindow) { - const selectedPrompt = template.selectedPrompt.get(); - - // Process template variables - let promptText = selectedPrompt?.template || selectedPrompt?.title || prompt.template || prompt.title; - if (promptText && context) { - promptText = promptText - .replace(/\{\{teamName\}\}/g, context.teamName || 'Current Team') - .replace(/\{\{currentText\}\}/g, context.currentText || '') - .replace(/\{\{recentActivitySummary\}\}/g, context.recentActivitySummary || 'No recent activity') - .replace(/\{\{totalTimeToday\}\}/g, context.projectStats?.formattedTimeToday || '0m') - .replace(/\{\{totalProjectTime\}\}/g, context.projectStats?.formattedProjectTime || '0m'); - } - - // Rich MCP context with project data - const contextData = { - type: 'mcp-context', - protocol: 'model-context-protocol', - version: '1.0', - context: { - // Core project info - project: { - name: context.teamName || 'Current Team', - type: 'Time Tracking Application', - id: context.teamId - }, - - // User context - user: context.user || {}, - - // Current work context - currentWork: { - text: context.currentText || '', - ticket: context.currentTicket, - inputType: 'ticket_description' - }, - - // Project statistics and history - projectStats: context.projectStats || {}, - recentActivity: context.recentActivity || [], - - // AI prompt configuration - prompt: { - id: selectedPrompt?.id, - title: selectedPrompt?.title, - template: promptText, - systemMessage: selectedPrompt?.systemMessage || 'You are a helpful assistant for time tracking and project management.', - instructions: selectedPrompt?.description - }, - - // Application context - application: { - name: 'TimeHarbor', - domain: 'time-tracking', - capabilities: ['ticket-management', 'time-tracking', 'project-reporting'] - } - } - }; - - console.log('Sending rich MCP context to Ozwell:', contextData); - iframe.contentWindow.postMessage(contextData, 'https://ai.bluehive.com'); - - // Send the processed prompt as initial message - if (promptText && promptText.length > 0) { - setTimeout(() => { - iframe.contentWindow.postMessage({ - type: 'ozwell-send-message', - message: promptText - }, 'https://ai.bluehive.com'); - }, 1000); - } - } - } else if (data.channel === 'iframe-basic' && data.message === 'sessionRendered') { - // Session is rendered and ready - template.sessionReady.set(true); - template.canSave.set(true); - } else if (data.type === 'ai-response' || data.type === 'message' || data.message) { - // Capture AI-generated content from various message formats - let content = null; - - if (data.content) { - content = data.content; - } else if (data.text) { - content = data.text; - } else if (data.message && typeof data.message === 'string') { - content = data.message; - } else if (data.response) { - content = data.response; - } - - if (content && typeof content === 'string' && content.trim().length > 10) { - console.log('Captured AI response:', content); - template.capturedContent.set(content); - } - } else if (data.type === 'ozwell-ready') { - template.sessionReady.set(true); - - // Enable save button after a short delay since Ozwell is ready - setTimeout(() => { - template.canSave.set(true); - }, 2000); - - // Send initial context to Ozwell - const iframe = document.querySelector('#ozwell-iframe'); - if (iframe && iframe.contentWindow) { - const selectedPrompt = template.selectedPrompt.get(); - const contextData = { - type: 'mcp-context', - context: { - teamName: context.teamName || 'Current Team', - currentText: context.currentText || '', - prompt: selectedPrompt?.template || selectedPrompt?.title || prompt.template || prompt.title, - systemMessage: selectedPrompt?.systemMessage || prompt.systemMessage || 'You are a helpful writing assistant.', - projectType: 'Time Tracking Application', - instructions: selectedPrompt?.description || 'Help the user improve their content.', - ...context - } - }; - - console.log('Sending context to Ozwell:', contextData); - iframe.contentWindow.postMessage(contextData, 'https://ai.bluehive.com'); - - // Also send the prompt as initial message if available - if (selectedPrompt?.template && context.currentText) { - setTimeout(() => { - const promptMessage = selectedPrompt.template.replace('{{currentText}}', context.currentText || '') - .replace('{{teamName}}', context.teamName || 'Current Team'); - - iframe.contentWindow.postMessage({ - type: 'ozwell-send-message', - message: promptMessage - }, 'https://ai.bluehive.com'); - }, 1000); - } - } - } else if (data.type === 'ozwell-content-ready' || data.type === 'iframe-basic' || data.type === 'sessionRendered') { - // Content is ready to be saved - template.canSave.set(true); - if (data.content) { - template.generatedContent.set(data.content); - } - } else if (data.type === 'ozwell-content-changed' || data.type === 'messageAdded' || data.type === 'messagesUpdated') { - // Content has been modified in Ozwell - template.canSave.set(true); - if (data.content) { - template.generatedContent.set(data.content); - } - } else if (data.type === 'ozwell-get-content') { - // Request current content from Ozwell - send a message to get it - const iframe = document.querySelector('#ozwell-iframe'); - if (iframe && iframe.contentWindow) { - iframe.contentWindow.postMessage({ - type: 'get-current-content' - }, 'https://ai.bluehive.com'); - } - } else if (data.type === 'ozwell-current-content' || data.type === 'export-content' || data.type === 'content-export') { - // Received the current content from Ozwell - if (data.content) { - template.generatedContent.set(data.content); - template.canSave.set(true); - console.log('Received content from Ozwell:', data.content.substring(0, 100) + '...'); - } - } else if (data.channel === 'iframe-basic' && data.content) { - // Content received via iframe-basic channel - template.generatedContent.set(data.content); - template.canSave.set(true); - console.log('Received content via iframe-basic:', data.content.substring(0, 100) + '...'); - } else if (data.channel === 'IframeSync' && data.content) { - // Content received via IframeSync channel - template.generatedContent.set(data.content); - template.canSave.set(true); - console.log('Received content via IframeSync:', data.content.substring(0, 100) + '...'); - } else if (data.type === 'message' && data.text) { - // Message content received - template.generatedContent.set(data.text); - template.canSave.set(true); - console.log('Received message content:', data.text.substring(0, 100) + '...'); - } else if (data.message && typeof data.message === 'string' && data.message.length > 10) { - // Generic message content - template.generatedContent.set(data.message); - template.canSave.set(true); - console.log('Received generic message:', data.message.substring(0, 100) + '...'); - } else if (data.channel === 'iframe-basic' && data.message === 'sessionRendered') { - // Session is ready, try to extract content after a delay - template.canSave.set(true); - setTimeout(() => { - template.extractContentFromIframe(); - }, 3000); - } else if (data.type && (data.type.includes('conversation') || data.type.includes('chat'))) { - // Handle conversation data - if (data.messages && Array.isArray(data.messages) && data.messages.length > 0) { - const lastMessage = data.messages[data.messages.length - 1]; - if (lastMessage.content || lastMessage.text || lastMessage.message) { - const content = lastMessage.content || lastMessage.text || lastMessage.message; - template.generatedContent.set(content); - template.canSave.set(true); - console.log('Received conversation content:', content.substring(0, 100) + '...'); - } - } - } + if (context.projectStats?.formattedProjectTime) { + summary.push(`Total time on project: ${context.projectStats.formattedProjectTime}`); + } - // Enhanced content monitoring for real autofill - // Monitor for any message that might contain AI-generated content - if (data.type === 'messageAdded' || data.type === 'messagesUpdated' || data.type === 'conversationUpdated') { - // Try to extract content from various data structures - let extractedContent = null; - - if (data.messages && Array.isArray(data.messages) && data.messages.length > 0) { - // Get the last message from the conversation - const lastMessage = data.messages[data.messages.length - 1]; - if (lastMessage.role === 'assistant' && lastMessage.content) { - extractedContent = lastMessage.content; - } - } else if (data.message && data.message.content && data.message.role === 'assistant') { - extractedContent = data.message.content; - } else if (data.content && typeof data.content === 'string') { - extractedContent = data.content; - } else if (data.text && typeof data.text === 'string') { - extractedContent = data.text; - } + if (context.projectStats?.formattedTimeToday) { + summary.push(`Time spent today: ${context.projectStats.formattedTimeToday}`); + } - if (extractedContent && extractedContent.length > 20) { - template.generatedContent.set(extractedContent); - template.canSave.set(true); - console.log('Captured AI content automatically:', extractedContent.substring(0, 100) + '...'); + if (context.recentActivity && Array.isArray(context.recentActivity) && context.recentActivity.length > 0) { + summary.push('Recent Activity:'); + context.recentActivity.forEach((item) => { + const pieces = [`- ${item.title}`]; + if (item.formattedTime) { + pieces.push(` (${item.formattedTime})`); } - } - - // Also monitor for clipboard events if Ozwell sends them - if (data.type === 'clipboardUpdate' || data.type === 'textCopied') { - if (data.content || data.text) { - const content = data.content || data.text; - template.generatedContent.set(content); - template.canSave.set(true); - console.log('Content captured from clipboard event:', content.substring(0, 100) + '...'); + if (item.description) { + pieces.push(` – ${item.description}`); } - } + summary.push(pieces.join('')); + }); + } - // For any message from Ozwell iframe, enable save button (fallback) - if (!template.canSave.get() && template.sessionReady.get()) { - setTimeout(() => { - template.canSave.set(true); - }, 3000); - } + if (context.currentText) { + summary.push(`Current input: ${context.currentText}`); + } - // FINAL CATCH-ALL: Try to extract content from any unhandled message - if (!template.capturedContent.get() && data) { - // Look for any text content in the entire data object - const searchForContent = (obj, depth = 0) => { - if (depth > 3) return null; // Prevent deep recursion - - if (typeof obj === 'string' && obj.length > 30 && - !obj.includes('sessionRendered') && - !obj.includes('ready') && - !obj.includes('http') && - !obj.includes('iframe')) { - return obj; - } - - if (obj && typeof obj === 'object') { - for (const key in obj) { - if (key === 'content' || key === 'text' || key === 'message' || key === 'response') { - const value = obj[key]; - if (typeof value === 'string' && value.length > 30) { - return value; - } - } - - const found = searchForContent(obj[key], depth + 1); - if (found) return found; - } - } - - return null; - }; - - const foundContent = searchForContent(data); - if (foundContent) { - console.log('Catch-all found content:', foundContent.substring(0, 100) + '...'); - template.capturedContent.set(foundContent); - } - } - }; + return summary.length > 0 ? summary.join('\n') : 'No additional project context provided.'; + }; - window.addEventListener('message', template.messageHandler); + const addMessage = (message) => { + const history = template.messages.get(); + template.messages.set([...history, { ...message, createdAt: new Date() }]); }; - // Extract content from iframe when postMessage doesn't work - this.extractContentFromIframe = function () { - const iframe = document.querySelector('#ozwell-iframe'); - if (!iframe || !iframe.contentWindow) return; - - console.log('Attempting to extract content from iframe...'); - - // Try multiple postMessage approaches first - const contentRequests = [ - { type: 'get-conversation' }, - { type: 'get-current-content' }, - { type: 'export-content' }, - { type: 'get-last-message' }, - { type: 'get-chat-history' }, - { channel: 'iframe-basic', message: 'getMessages' }, - { channel: 'IframeSync', type: 'getContent' }, - { action: 'getChatHistory' }, - { action: 'getLastResponse' }, - { command: 'export' } - ]; - - contentRequests.forEach((request, index) => { - setTimeout(() => { - iframe.contentWindow.postMessage(request, 'https://ai.bluehive.com'); - }, index * 100); - }); + const buildServerMessages = () => { + const messages = []; + const system = template.systemMessage.get(); + const summary = template.contextSummary.get(); - // After trying postMessage, attempt DOM extraction - setTimeout(() => { - try { - const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; - if (!iframeDoc) { - console.log('Cannot access iframe document (CORS)'); - return; - } + if (system) { + messages.push({ role: 'system', content: system }); + } - // Look for chat messages with more comprehensive selectors - const messageSelectors = [ - '[data-testid*="message"]', - '[class*="message"]', - '[class*="chat"]', - '[class*="response"]', - '[class*="content"]', - '.message-content', - '.chat-message', - '.response-text', - '.message-text', - '.ai-response', - '.assistant-message', - '[role="log"] > div:last-child', - '[role="log"] [class*="message"]:last-child', - '.conversation [class*="message"]:last-child', - 'div[data-message-id]', - '.prose', - 'article', - 'main [class*="text"]' - ]; - - let extractedContent = ''; - - for (const selector of messageSelectors) { - try { - const elements = iframeDoc.querySelectorAll(selector); - if (elements.length > 0) { - const lastElement = elements[elements.length - 1]; - const text = lastElement.textContent || lastElement.innerText; - - if (text && text.trim().length > 20 && - !text.toLowerCase().includes('type a message') && - !text.toLowerCase().includes('send') && - !text.toLowerCase().includes('loading') && - !text.toLowerCase().includes('connecting')) { - - extractedContent = text.trim(); - console.log(`Content extracted using selector "${selector}":`, extractedContent.substring(0, 150) + '...'); - break; - } - } - } catch (e) { - // Skip selector if it fails - } - } + if (summary) { + messages.push({ role: 'system', content: `Project context:\n${summary}` }); + } - // If still no content, try to find any meaningful text - if (!extractedContent) { - const allText = iframeDoc.body ? (iframeDoc.body.textContent || iframeDoc.body.innerText) : ''; - const lines = allText.split('\n') - .map(line => line.trim()) - .filter(line => - line.length > 30 && - !line.toLowerCase().includes('type a message') && - !line.toLowerCase().includes('ozwell') && - !line.toLowerCase().includes('send') && - !line.toLowerCase().includes('cancel') && - !line.toLowerCase().includes('loading') && - !line.toLowerCase().includes('connecting') && - !line.toLowerCase().includes('powered by') - ); - - if (lines.length > 0) { - extractedContent = lines[lines.length - 1]; - console.log('Content extracted from body text:', extractedContent.substring(0, 150) + '...'); - } - } + template.messages.get().forEach((msg) => { + if (msg.role === 'user' || msg.role === 'assistant') { + messages.push({ role: msg.role, content: msg.content }); + } + }); - if (extractedContent) { - template.generatedContent.set(extractedContent); - console.log('Successfully extracted content from iframe'); - } else { - console.log('No meaningful content found in iframe'); - } + return messages; + }; - } catch (e) { - console.log('Cannot access iframe content due to CORS:', e.message); + const callReferenceAssistant = (payload) => new Promise((resolve, reject) => { + Meteor.call('callReferenceAssistant', payload, (error, result) => { + if (error) { + reject(error); + } else { + resolve(result); } - }, 1000); + }); + }); + + template.resetConversation = function () { + template.messages.set([]); + template.composerText.set(''); + template.contextSummary.set(''); + template.systemMessage.set(DEFAULT_SYSTEM_MESSAGE); + template.generatedContent.set(null); + template.canSave.set(false); + template.isGenerating.set(false); + template.errorMessage.set(null); }; - // Initialize Ozwell session - this.initializeOzwellSession = function (prompt) { - const teamId = template.currentTeamId.get(); - const context = template.currentContext.get(); + template.performAutofill = function ({ closeModal = true } = {}) { + const content = template.generatedContent.get(); + const inputElement = template.currentInputElement.get(); - if (!teamId) { - console.error('No team ID available for Ozwell session'); + if (!content) { + alert('No assistant content is available yet. Generate a suggestion first.'); return; } - // Show loading state - template.ozwellSessionUrl.set(null); + if (!inputElement) { + alert('Unable to find the original input field to update.'); + return; + } - // Create Ozwell session - Meteor.call('createOzwellSession', teamId, false, (err, sessionData) => { - if (err) { - console.error('Failed to create Ozwell session:', err); - alert('Failed to start Ozwell session. Please check your configuration.'); - template.closeOzwell(false); - return; - } + if (inputElement.tagName === 'TEXTAREA' || inputElement.tagName === 'INPUT') { + inputElement.value = content; + } else if (inputElement.contentEditable === 'true') { + inputElement.textContent = content; + } - template.ozwellSessionUrl.set(sessionData.loginUrl); + const inputEvent = new Event('input', { bubbles: true }); + inputElement.dispatchEvent(inputEvent); + const changeEvent = new Event('change', { bubbles: true }); + inputElement.dispatchEvent(changeEvent); + inputElement.focus(); - // Set up postMessage listener for iframe communication - template.setupPostMessageListener(prompt, context); - }); + if (closeModal) { + template.closeModal(); + } }; - // Paste from clipboard and save - this.pasteAndSave = async function () { - try { - // Try to read from clipboard - const clipboardText = await navigator.clipboard.readText(); - - if (clipboardText && clipboardText.trim().length > 0) { - // Validate clipboard content - check if it looks like console logs or browser stuff - const lowerClipboard = clipboardText.toLowerCase(); - const isConsoleLog = lowerClipboard.includes('console') || - lowerClipboard.includes('hmr:') || - lowerClipboard.includes('.js?hash=') || - lowerClipboard.includes('ozwellbutton.js') || - lowerClipboard.includes('ozwellmodal.js') || - lowerClipboard.includes('meteor_js_resource') || - clipboardText.includes('@') && clipboardText.includes(':'); - - if (isConsoleLog) { - alert('It looks like you copied console logs instead of Ozwell content.\n\nPlease:\n1. Click inside the Ozwell chat area above\n2. Select the AI-generated text (not the console)\n3. Copy it (Ctrl+C or Cmd+C)\n4. Click "Paste and Save" again'); - return; - } - - // Store the clipboard content - template.generatedContent.set(clipboardText.trim()); - console.log('Content from clipboard:', clipboardText.substring(0, 100) + '...'); + template.closeModal = function () { + template.isOzwellOpen.set(false); + template.selectedPrompt.set(null); + template.resetConversation(); + template.currentInputElement.set(null); + template.currentContext.set(null); + template.currentTeamId.set(null); + template.headerSubtitle.set('Ready to help with your work.'); + }; - // Perform autofill with clipboard content - template.performAutofill(); - } else { - // If no clipboard content, prompt user to manually copy - alert('No content found in clipboard.\n\nPlease:\n1. Click inside the Ozwell chat area above\n2. Select the AI-generated text\n3. Copy it (Ctrl+C or Cmd+C)\n4. Click "Paste and Save" again'); - } - } catch (err) { - console.log('Clipboard access not available:', err); - // Fallback: prompt user with manual input - const manualContent = prompt('Clipboard access is restricted.\n\nPlease copy the content from Ozwell above and paste it here:'); - if (manualContent && manualContent.trim().length > 0) { - template.generatedContent.set(manualContent.trim()); - template.performAutofill(); + template.loadPrompts = function () { + Meteor.call('getOzwellPrompts', (err, prompts) => { + if (!err && prompts) { + template.availablePrompts.set(prompts); } else { - alert('No content provided. Please copy the text from Ozwell and try again.'); + template.availablePrompts.set(FALLBACK_PROMPTS); } - } + }); }; - // Close Ozwell modal - this.closeOzwell = function (save = false) { - if (save) { - // Simple approach: just immediately check clipboard for content - console.log('Auto Save clicked - checking clipboard immediately...'); - template.pasteAndSave(); + template.openOzwell = function (inputElement, context = {}) { + const user = Meteor.user(); + if (!user?.profile?.ozwellEnabled) { + alert('Please configure Ozwell in your settings first.'); return; } - template.closeModal(); - }; // Perform the actual autofill - this.performAutofill = function () { - let content = template.generatedContent.get(); - const inputElement = template.currentInputElement.get(); - - // If no content was captured, try to get some content one more time - if (!content || content.trim().length === 0 || content === 'Content generated with Ozwell AI assistant') { - // Try to get content from the iframe one more time - const iframe = document.querySelector('#ozwell-iframe'); - if (iframe && iframe.contentWindow) { - try { - const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; - - // Last attempt to get actual content - const textElements = iframeDoc.querySelectorAll('p, div, span'); - let foundContent = ''; - - for (const element of textElements) { - const text = element.textContent || element.innerText; - if (text && text.trim().length > 20 && - !text.includes('Type a message') && - !text.includes('AI assistant') && - !text.includes('Ozwell') && - !text.includes('Send') && - !text.includes('Cancel')) { - foundContent = text.trim(); - break; - } - } - - if (foundContent) { - content = foundContent; - console.log('Found content in final attempt:', content.substring(0, 100) + '...'); - } - } catch (e) { - // CORS restriction - expected - } - } - - // If still no content, use a more descriptive placeholder - if (!content || content.trim().length === 0) { - content = '[Ozwell AI content - please check the generated text above]'; - } - } - - if (content && inputElement) { - console.log('Autofilling content:', content.substring(0, 200) + (content.length > 200 ? '...' : '')); - - // Handle different types of input elements - if (inputElement.tagName === 'TEXTAREA' || inputElement.tagName === 'INPUT') { - inputElement.value = content; - - // Trigger input event to notify other parts of the app - const event = new Event('input', { bubbles: true }); - inputElement.dispatchEvent(event); - - // Also trigger change event - const changeEvent = new Event('change', { bubbles: true }); - inputElement.dispatchEvent(changeEvent); - } else if (inputElement.contentEditable === 'true') { - inputElement.textContent = content; + template.selectedPrompt.set(null); + template.resetConversation(); + template.currentInputElement.set(inputElement); + template.currentContext.set(context); + template.currentTeamId.set(context.teamId || null); + template.headerSubtitle.set(context.teamName ? `Project: ${context.teamName}` : 'Ready to help with your work.'); + template.isOzwellOpen.set(true); + }; - // Trigger input event for contentEditable - const event = new Event('input', { bubbles: true }); - inputElement.dispatchEvent(event); - } + template.initializeConversation = async function (prompt) { + template.selectedPrompt.set(prompt); + const context = template.currentContext.get() || {}; + const processedPrompt = replaceTemplateVariables(prompt?.template || prompt?.title || '', context); + const fallbackPrompt = context.currentText || 'Help me refine this note.'; + const userMessage = processedPrompt && processedPrompt.trim().length > 0 ? processedPrompt : fallbackPrompt; - // Focus the input element - inputElement.focus(); + template.systemMessage.set(prompt?.systemMessage || DEFAULT_SYSTEM_MESSAGE); + template.contextSummary.set(buildContextSummary(context)); + template.messages.set([]); + template.generatedContent.set(null); + template.canSave.set(false); + template.errorMessage.set(null); - console.log('Ozwell content autofilled successfully'); - } else { - console.log('No content to autofill or no input element'); + if (prompt?.title) { + template.headerSubtitle.set(prompt.title); } - template.closeModal(); + await template.sendChatMessage(userMessage); }; - // Close modal and clean up - this.closeModal = function () { - template.isOzwellOpen.set(false); - - // Clean up postMessage listener - if (template.messageHandler) { - window.removeEventListener('message', template.messageHandler); - template.messageHandler = null; - } + template.sendChatMessage = async function (content) { + const trimmed = (content || '').trim(); + if (!trimmed) return; + if (template.isGenerating.get()) return; - // Reset state - template.selectedPrompt.set(null); - template.ozwellSessionUrl.set(null); - template.currentInputElement.set(null); - template.currentContext.set(null); - template.currentTeamId.set(null); - template.sessionReady.set(false); + addMessage({ role: 'user', content: trimmed }); + template.composerText.set(''); + template.isGenerating.set(true); template.canSave.set(false); - template.generatedContent.set(null); + template.errorMessage.set(null); - console.log('Ozwell modal closed'); + const metadata = { + teamId: template.currentTeamId.get(), + promptId: template.selectedPrompt.get()?.id, + }; + + try { + const messages = buildServerMessages(); + const result = await callReferenceAssistant({ + messages, + metadata, + }); + + const assistantContent = result?.content; + if (assistantContent) { + addMessage({ role: 'assistant', content: assistantContent }); + template.generatedContent.set(assistantContent.trim()); + template.canSave.set(true); + } else { + template.errorMessage.set('The assistant returned no content. Please try again.'); + } + } catch (error) { + console.error('Failed to generate content from reference server:', error); + template.errorMessage.set(error?.reason || 'Failed to generate content. Please try again.'); + } finally { + template.isGenerating.set(false); + } }; -}); -Template.ozwellModal.onDestroyed(function () { - // Clean up global reference - if (window.ozwellModalInstance === this) { - window.ozwellModalInstance = null; - } + template.loadPrompts(); }); Template.ozwellModal.helpers({ isOzwellOpen() { return Template.instance().isOzwellOpen.get(); }, - selectedPrompt() { return Template.instance().selectedPrompt.get(); }, - - ozwellSessionUrl() { - return Template.instance().ozwellSessionUrl.get(); - }, - availablePrompts() { - const allPrompts = Template.instance().availablePrompts.get(); - // Filter prompts based on current context if needed - return allPrompts; + return Template.instance().availablePrompts.get(); }, - - canSave() { - return Template.instance().canSave.get(); + messages() { + return Template.instance().messages.get(); }, - - saveButtonClass() { - return Template.instance().canSave.get() ? '' : 'btn-disabled'; + composerText() { + return Template.instance().composerText.get(); + }, + composerDisabled() { + const instance = Template.instance(); + return instance.isGenerating.get() ? 'disabled' : null; + }, + sendDisabled() { + const instance = Template.instance(); + return instance.isGenerating.get() ? 'disabled' : null; + }, + contextSummary() { + return Template.instance().contextSummary.get(); + }, + headerSubtitle() { + return Template.instance().headerSubtitle.get(); + }, + isGenerating() { + return Template.instance().isGenerating.get(); + }, + showEmptyState() { + const instance = Template.instance(); + return instance.messages.get().length === 0 && instance.isGenerating.get(); + }, + messageWrapperClass(role) { + return role === 'user' ? 'justify-end' : 'justify-start'; + }, + messageBubbleClass(role) { + return role === 'user' + ? 'bg-primary text-white' + : 'bg-base-200 text-base-content'; + }, + insertDisabled() { + const instance = Template.instance(); + return instance.canSave.get() && !instance.isGenerating.get() ? '' : 'disabled'; + }, + saveDisabled() { + const instance = Template.instance(); + return instance.canSave.get() && !instance.isGenerating.get() ? '' : 'disabled'; + }, + errorMessage() { + return Template.instance().errorMessage.get(); }, - eq(a, b) { return a === b; } @@ -767,38 +364,27 @@ Template.ozwellModal.events({ 'click #ozwell-close'(event, template) { template.closeModal(); }, - 'click #ozwell-backdrop'(event, template) { if (event.target.id === 'ozwell-backdrop') { template.closeModal(); } }, - 'click #ozwell-cancel'(event, template) { template.closeModal(); }, - - 'click #ozwell-save'(event, template) { - template.closeOzwell(true); - }, - - 'click #ozwell-paste-save'(event, template) { - template.pasteAndSave(); - }, - 'click .prompt-btn'(event, template) { + event.preventDefault(); const promptId = event.currentTarget.getAttribute('data-prompt-id'); const prompts = template.availablePrompts.get(); - const selectedPrompt = prompts.find(p => p.id === promptId); + const selectedPrompt = prompts.find((prompt) => prompt.id === promptId); if (selectedPrompt) { template.selectedPrompt.set(selectedPrompt); - template.initializeOzwellSession(selectedPrompt); + template.initializeConversation(selectedPrompt); } }, - 'click #use-custom-prompt'(event, template) { - // Create a custom prompt object + event.preventDefault(); const customPrompt = { id: 'custom', title: 'Custom Prompt', @@ -807,20 +393,42 @@ Template.ozwellModal.events({ }; template.selectedPrompt.set(customPrompt); - template.initializeOzwellSession(customPrompt); + template.initializeConversation(customPrompt); + }, + 'submit #ozwell-composer'(event, template) { + event.preventDefault(); + if (template.isGenerating.get()) return; + + const text = template.composerText.get(); + template.sendChatMessage(text); + }, + 'input #ozwell-message-input'(event, template) { + template.composerText.set(event.target.value); + }, + 'click #ozwell-insert'(event, template) { + event.preventDefault(); + if (template.canSave.get()) { + template.performAutofill({ closeModal: false }); + } + }, + 'click #ozwell-save-close'(event, template) { + event.preventDefault(); + if (template.canSave.get()) { + template.performAutofill({ closeModal: true }); + } } }); -// Template methods -Template.ozwellModal.helpers({ - // Additional methods accessible from template +Template.ozwellModal.onDestroyed(function () { + if (window.ozwellModalInstance === this) { + window.ozwellModalInstance = null; + } }); -// Global helper function to open Ozwell from any component window.openOzwell = function (inputElement, context = {}) { if (window.ozwellModalInstance) { window.ozwellModalInstance.openOzwell(inputElement, context); } else { console.error('Ozwell modal not available'); } -}; \ No newline at end of file +}; diff --git a/client/components/settings/SettingsPage.html b/client/components/settings/SettingsPage.html index c0db8a2..e28bea1 100644 --- a/client/components/settings/SettingsPage.html +++ b/client/components/settings/SettingsPage.html @@ -53,7 +53,7 @@

Ozwell AI Writing Assistant

+ class="input input-bordered {{apiKeyInputClass}}" value="{{ozwellSettings.apiKey}}" required />
+
+ + +
+ +
+ + +
+
- \ No newline at end of file + diff --git a/client/components/settings/SettingsPage.js b/client/components/settings/SettingsPage.js index e5cbb45..17bf39d 100644 --- a/client/components/settings/SettingsPage.js +++ b/client/components/settings/SettingsPage.js @@ -9,6 +9,23 @@ Template.settings.onCreated(function () { this.isConfiguring = new ReactiveVar(false); this.configMessage = new ReactiveVar(''); this.configMessageType = new ReactiveVar('alert-info'); + this.ozwellSettings = new ReactiveVar({ + apiKey: '', + baseUrl: '', + model: '', + }); + + this.autorun(() => { + const user = Meteor.user(); + if (user) { + const profile = user.profile || {}; + this.ozwellSettings.set({ + apiKey: profile.ozwellApiKey || '', + baseUrl: profile.ozwellBaseUrl || '', + model: profile.ozwellModel || '', + }); + } + }); }); Template.settings.helpers({ @@ -44,6 +61,10 @@ Template.settings.helpers({ cancelButtonClass() { const isConfiguring = Template.instance().isConfiguring.get(); return isConfiguring ? 'btn btn-outline disabled' : 'btn btn-outline'; + }, + + ozwellSettings() { + return Template.instance().ozwellSettings.get(); } }); @@ -67,6 +88,8 @@ Template.settings.events({ event.preventDefault(); const apiKey = event.target.apiKey.value.trim(); + const baseUrl = event.target.baseUrl.value.trim() || 'http://localhost:3000/v1'; + const model = event.target.model.value.trim() || 'llama3'; if (!apiKey) { template.configMessage.set('Please enter an API key'); template.configMessageType.set('alert-error'); @@ -77,7 +100,7 @@ Template.settings.events({ template.configMessage.set('Testing API key...'); template.configMessageType.set('alert-info'); - Meteor.call('saveOzwellApiKey', apiKey, (err, result) => { + Meteor.call('saveOzwellConfiguration', { apiKey, baseUrl, model }, (err) => { template.isConfiguring.set(false); if (err) { @@ -101,6 +124,8 @@ Template.settings.events({ const user = Meteor.user(); const apiKey = user?.profile?.ozwellApiKey; + const baseUrl = user?.profile?.ozwellBaseUrl || 'http://localhost:3000/v1'; + const model = user?.profile?.ozwellModel || 'llama3'; if (!apiKey) { template.configMessage.set('No API key configured'); @@ -108,7 +133,7 @@ Template.settings.events({ return; } - Meteor.call('testOzwellCredentials', apiKey, (err, result) => { + Meteor.call('testOzwellCredentials', { apiKey, baseUrl, model }, (err) => { if (err) { console.error('Ozwell connection test failed:', err); template.configMessage.set(err.reason || 'Connection test failed'); @@ -137,4 +162,4 @@ Template.settings.events({ }); } } -}); \ No newline at end of file +}); diff --git a/server/main.js b/server/main.js index 56c6269..8dfba9e 100644 --- a/server/main.js +++ b/server/main.js @@ -11,6 +11,7 @@ import { clockEventMethods } from './methods/clockEvents.js'; // Import Ozwell methods import { ozwellMethods } from './methods/ozwell.js'; import { ozwellPromptMethods } from './methods/ozwellPrompts.js'; +import { referenceAssistantMethods } from './methods/referenceAssistant.js'; Meteor.startup(async () => { // Code to run on server startup if (await Tickets.find().countAsync() === 0) { @@ -184,6 +185,7 @@ Meteor.methods({ ...clockEventMethods, ...ozwellMethods, ...ozwellPromptMethods, + ...referenceAssistantMethods, 'participants.create'(name) { check(name, String); diff --git a/server/methods/ozwell.js b/server/methods/ozwell.js index dc46582..49f50f2 100644 --- a/server/methods/ozwell.js +++ b/server/methods/ozwell.js @@ -3,42 +3,55 @@ import { check } from 'meteor/check'; import { OzwellWorkspaces, OzwellUsers, OzwellConversations, OzwellPrompts, Teams, Tickets, ClockEvents } from '../../collections.js'; import axios from 'axios'; -const OZWELL_API_BASE = 'https://ai.bluehive.com/api/v1'; +const DEFAULT_REFERENCE_BASE_URL = 'http://localhost:3000/v1'; +const DEFAULT_REFERENCE_MODEL = 'llama3'; export const ozwellMethods = { // Test Ozwell API credentials - async testOzwellCredentials(apiKey) { + async testOzwellCredentials({ apiKey, baseUrl, model }) { check(apiKey, String); + check(baseUrl, String); + check(model, String); if (!this.userId) throw new Meteor.Error('not-authorized'); try { - const response = await axios.post(`${OZWELL_API_BASE}/test-credentials`, {}, { + const url = baseUrl.endsWith('/') ? `${baseUrl}chat/completions` : `${baseUrl}/chat/completions`; + await axios.post(url, { + model, + messages: [ + { role: 'system', content: 'You are a connection test assistant.' }, + { role: 'user', content: 'Reply with OK.' } + ], + }, { headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + timeout: 5000, }); - return { success: true, message: response.data.message }; + return { success: true }; } catch (error) { - console.error('Ozwell credentials test failed:', error.response?.data); - throw new Meteor.Error('ozwell-error', 'Invalid API credentials'); + console.error('Reference server credentials test failed:', error.response?.data || error.message); + throw new Meteor.Error('ozwell-error', 'Connection test failed. Check base URL, model, and API key.'); } }, - // Save user's Ozwell API key - async saveOzwellApiKey(apiKey) { + // Save user's Ozwell configuration + async saveOzwellConfiguration({ apiKey, baseUrl, model }) { check(apiKey, String); + check(baseUrl, String); + check(model, String); if (!this.userId) throw new Meteor.Error('not-authorized'); - // First test the credentials - await ozwellMethods.testOzwellCredentials.call(this, apiKey); + await ozwellMethods.testOzwellCredentials.call(this, { apiKey, baseUrl, model }); - // Save to user profile await Meteor.users.updateAsync(this.userId, { $set: { 'profile.ozwellApiKey': apiKey, - 'profile.ozwellEnabled': true + 'profile.ozwellBaseUrl': baseUrl, + 'profile.ozwellModel': model, + 'profile.ozwellEnabled': true, } }); @@ -372,4 +385,4 @@ export const ozwellMethods = { { sort: { updatedAt: -1 }, limit } ).fetchAsync(); } -}; \ No newline at end of file +}; diff --git a/server/methods/referenceAssistant.js b/server/methods/referenceAssistant.js new file mode 100644 index 0000000..98361b7 --- /dev/null +++ b/server/methods/referenceAssistant.js @@ -0,0 +1,60 @@ +import { Meteor } from 'meteor/meteor'; +import { check, Match } from 'meteor/check'; +import axios from 'axios'; + +export const referenceAssistantMethods = { + async callReferenceAssistant(params) { + check(params, { + messages: [Match.ObjectIncluding({ + role: String, + content: String, + })], + metadata: Match.Maybe(Object), + options: Match.Maybe(Object), + }); + + if (!this.userId) throw new Meteor.Error('not-authorized'); + + const user = await Meteor.users.findOneAsync(this.userId); + const profile = user?.profile || {}; + + const baseUrl = params.options?.baseUrl || profile.ozwellBaseUrl || Meteor.settings?.referenceServer?.baseUrl || process.env.REFERENCE_SERVER_BASE_URL || 'http://localhost:3000/v1'; + const apiKey = params.options?.apiKey || profile.ozwellApiKey || process.env.REFERENCE_SERVER_API_KEY; + const model = params.options?.model || profile.ozwellModel || Meteor.settings?.referenceServer?.model || process.env.REFERENCE_SERVER_MODEL || 'llama3'; + + if (!apiKey) { + throw new Meteor.Error('reference-server-config-missing', 'Please add your Ozwell API key in Settings.'); + } + + try { + const url = baseUrl.endsWith('/') ? `${baseUrl}chat/completions` : `${baseUrl}/chat/completions`; + const { data } = await axios.post(url, { + model, + messages: params.messages, + ...params.options, + }, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + }); + const content = data?.choices?.[0]?.message?.content; + + if (!content) { + throw new Meteor.Error('reference-server-empty', 'Reference server returned no content'); + } + + return { + content: content.trim(), + raw: data, + }; + } catch (error) { + if (error instanceof Meteor.Error) { + throw error; + } + + const errorMessage = error.response?.data?.error || error.response?.data || error.message; + throw new Meteor.Error('reference-server-unavailable', errorMessage || 'Failed to reach reference server'); + } + }, +}; From 65d2f46d9348191d37dd58635f711c7ac46c22e3 Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:59:29 -0400 Subject: [PATCH 09/57] Add side panel layout and suggestion picker to OzwellModal Rewrote the toggle between modal and side panel layouts for the Ozwell AI Assistant. Adds support for extracting and displaying multiple suggestions from assistant responses, allowing users to select and insert a preferred suggestion. Improves prompt button styling and accessibility, and refactors event handling for layout switching and suggestion selection. --- client/components/ozwell/OzwellModal.html | 53 +++++++++++----- client/components/ozwell/OzwellModal.js | 77 ++++++++++++++++++++++- 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html index 8167004..a4a9699 100644 --- a/client/components/ozwell/OzwellModal.html +++ b/client/components/ozwell/OzwellModal.html @@ -1,17 +1,22 @@ diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js index 10755d6..832cbb0 100644 --- a/client/components/ozwell/OzwellModal.js +++ b/client/components/ozwell/OzwellModal.js @@ -59,6 +59,9 @@ Template.ozwellModal.onCreated(function () { template.isGenerating = new ReactiveVar(false); template.errorMessage = new ReactiveVar(null); template.headerSubtitle = new ReactiveVar('Ready to help with your work.'); + template.suggestions = new ReactiveVar([]); + template.selectedSuggestionIndex = new ReactiveVar(0); + template.layoutMode = new ReactiveVar('modal'); // modal | sidecar // Expose template instance for global access window.ozwellModalInstance = template; @@ -126,6 +129,24 @@ Template.ozwellModal.onCreated(function () { template.messages.set([...history, { ...message, createdAt: new Date() }]); }; + const extractSuggestions = (text = '') => { + if (!text) return []; + let parts = text.split(/\n\s*\n+/).map((part) => part.trim()).filter(Boolean); + + if (parts.length <= 1) { + const bulletParts = text.split(/(?:^|\n)\s*(?:[-*•]|\d+\.)\s+/).map((part) => part.trim()).filter(Boolean); + if (bulletParts.length > 1) { + parts = bulletParts; + } + } + + if (parts.length <= 1) { + return []; + } + + return parts; + }; + const buildServerMessages = () => { const messages = []; const system = template.systemMessage.get(); @@ -167,6 +188,8 @@ Template.ozwellModal.onCreated(function () { template.canSave.set(false); template.isGenerating.set(false); template.errorMessage.set(null); + template.suggestions.set([]); + template.selectedSuggestionIndex.set(0); }; template.performAutofill = function ({ closeModal = true } = {}) { @@ -208,6 +231,7 @@ Template.ozwellModal.onCreated(function () { template.currentContext.set(null); template.currentTeamId.set(null); template.headerSubtitle.set('Ready to help with your work.'); + template.layoutMode.set('modal'); }; template.loadPrompts = function () { @@ -234,6 +258,7 @@ Template.ozwellModal.onCreated(function () { template.currentTeamId.set(context.teamId || null); template.headerSubtitle.set(context.teamName ? `Project: ${context.teamName}` : 'Ready to help with your work.'); template.isOzwellOpen.set(true); + template.layoutMode.set('modal'); }; template.initializeConversation = async function (prompt) { @@ -267,6 +292,9 @@ Template.ozwellModal.onCreated(function () { template.isGenerating.set(true); template.canSave.set(false); template.errorMessage.set(null); + template.suggestions.set([]); + template.selectedSuggestionIndex.set(0); + template.generatedContent.set(null); const metadata = { teamId: template.currentTeamId.get(), @@ -283,7 +311,17 @@ Template.ozwellModal.onCreated(function () { const assistantContent = result?.content; if (assistantContent) { addMessage({ role: 'assistant', content: assistantContent }); - template.generatedContent.set(assistantContent.trim()); + const suggestions = extractSuggestions(assistantContent.trim()); + if (suggestions.length > 0) { + template.suggestions.set(suggestions); + template.selectedSuggestionIndex.set(0); + template.generatedContent.set(suggestions[0]); + } else { + template.suggestions.set([]); + template.selectedSuggestionIndex.set(0); + template.generatedContent.set(assistantContent.trim()); + } + template.canSave.set(true); } else { template.errorMessage.set('The assistant returned no content. Please try again.'); @@ -312,6 +350,9 @@ Template.ozwellModal.helpers({ messages() { return Template.instance().messages.get(); }, + layoutIsSidecar() { + return Template.instance().layoutMode.get() === 'sidecar'; + }, composerText() { return Template.instance().composerText.get(); }, @@ -336,6 +377,13 @@ Template.ozwellModal.helpers({ const instance = Template.instance(); return instance.messages.get().length === 0 && instance.isGenerating.get(); }, + suggestions() { + const instance = Template.instance(); + return instance.suggestions.get().map((text, index) => ({ text, index })); + }, + suggestionsAvailable() { + return Template.instance().suggestions.get().length > 1; + }, messageWrapperClass(role) { return role === 'user' ? 'justify-end' : 'justify-start'; }, @@ -357,6 +405,9 @@ Template.ozwellModal.helpers({ }, eq(a, b) { return a === b; + }, + checked(index) { + return Template.instance().selectedSuggestionIndex.get() === index ? 'checked' : ''; } }); @@ -364,6 +415,11 @@ Template.ozwellModal.events({ 'click #ozwell-close'(event, template) { template.closeModal(); }, + 'click #ozwell-toggle-layout'(event, template) { + event.preventDefault(); + const next = template.layoutMode.get() === 'sidecar' ? 'modal' : 'sidecar'; + template.layoutMode.set(next); + }, 'click #ozwell-backdrop'(event, template) { if (event.target.id === 'ozwell-backdrop') { template.closeModal(); @@ -405,6 +461,16 @@ Template.ozwellModal.events({ 'input #ozwell-message-input'(event, template) { template.composerText.set(event.target.value); }, + 'keydown #ozwell-message-input'(event, template) { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + if (template.isGenerating.get()) return; + const value = event.target.value; + template.sendChatMessage(value); + event.target.value = ''; + template.composerText.set(''); + } + }, 'click #ozwell-insert'(event, template) { event.preventDefault(); if (template.canSave.get()) { @@ -416,6 +482,15 @@ Template.ozwellModal.events({ if (template.canSave.get()) { template.performAutofill({ closeModal: true }); } + }, + 'change input[name="ozwell-suggestion"]'(event, template) { + const index = Number(event.target.value); + const suggestions = template.suggestions.get(); + if (!Number.isNaN(index) && suggestions[index]) { + template.selectedSuggestionIndex.set(index); + template.generatedContent.set(suggestions[index]); + template.canSave.set(true); + } } }); From 0ab769ea9ad27b267e90cca93c8c47aac6304403 Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:52:24 -0400 Subject: [PATCH 10/57] Enhance Ozwell modal context and suggestions UI Improves OzwellButton to collect related form field values and passes them to the modal for richer context. Refactors OzwellModal to display and toggle context summary, improves suggestion extraction logic, and updates UI for better usability. Minor layout adjustments in TicketsPage and code cleanup in main.js. --- client/components/ozwell/OzwellButton.js | 34 +++++++- client/components/ozwell/OzwellModal.html | 88 +++++++++++-------- client/components/ozwell/OzwellModal.js | 98 ++++++++++++++++++---- client/components/tickets/TicketsPage.html | 6 +- client/main.js | 2 +- 5 files changed, 166 insertions(+), 62 deletions(-) diff --git a/client/components/ozwell/OzwellButton.js b/client/components/ozwell/OzwellButton.js index 484e1d0..d951e4a 100644 --- a/client/components/ozwell/OzwellButton.js +++ b/client/components/ozwell/OzwellButton.js @@ -10,11 +10,21 @@ Template.ozwellButton.events({ // Find the associated input element const button = event.currentTarget; - const container = button.closest('.input-group, .form-control, form, .card'); + const immediateWrapper = button.closest('.relative, .input-group, .form-control'); let inputElement = null; - // Look for text inputs, textareas in the same container - if (container) { + if (immediateWrapper) { + const candidateList = Array.from(immediateWrapper.children).filter((element) => + element.matches('input[type="text"], textarea, [contenteditable="true"]') + ); + if (candidateList.length > 0) { + inputElement = candidateList[0]; + } + } + + const container = !inputElement ? button.closest('.input-group, .form-control, form, .card') : null; + + if (!inputElement && container) { inputElement = container.querySelector('input[type="text"], textarea, [contenteditable="true"]'); } @@ -31,6 +41,8 @@ Template.ozwellButton.events({ return; } + const form = button.closest('form'); + // Get context from the current page - try multiple ways let context = {}; @@ -69,6 +81,20 @@ Template.ozwellButton.events({ // Add current text from the input element context.currentText = inputElement.value || inputElement.textContent || ''; + context.fieldName = inputElement.getAttribute('name') || inputElement.getAttribute('id') || 'unknown-field'; + + if (form) { + const relatedFields = {}; + const formData = new FormData(form); + formData.forEach((value, key) => { + if (key !== context.fieldName && typeof value === 'string' && value.trim().length > 0) { + relatedFields[key] = value; + } + }); + if (Object.keys(relatedFields).length > 0) { + context.relatedFields = relatedFields; + } + } console.log('Opening Ozwell with context:', context); @@ -79,4 +105,4 @@ Template.ozwellButton.events({ console.error('Ozwell not available'); } } -}); \ No newline at end of file +}); diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html index a4a9699..f58cec3 100644 --- a/client/components/ozwell/OzwellModal.html +++ b/client/components/ozwell/OzwellModal.html @@ -20,55 +20,69 @@

Ozwell AI Assistant

{{#if selectedPrompt}} -
- Project Context -
- {{contextSummary}} +
+
+ Project Context + {{#if summaryVisible}} +
+ {{contextSummary}} +
+ {{/if}}
+
-
- {{#each messages}} -
-
-

{{content}}

+
+
+ {{#each messages}} +
+
+

{{content}}

+
-
- {{/each}} + {{/each}} - {{#if showEmptyState}} -
-
-
-

Preparing your first draft…

+ {{#if showEmptyState}} +
+
+
+

Preparing your first draft…

+
-
- {{/if}} + {{/if}} - {{#if isGenerating}} -
-
- - Generating response… + {{#if isGenerating}} +
+
+ + Generating response… +
+ {{/if}}
- {{/if}} -
- {{#if suggestionsAvailable}} -
-

Pick a suggestion to insert:

-
- {{#each suggestions}} - - {{/each}} + {{#if suggestionsAvailable}} +
+

Pick a suggestion to insert

+
+ {{#each suggestions}} + + {{/each}} +
+ {{/if}}
- {{/if}} {{#if errorMessage}}
{{errorMessage}}
diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js index 832cbb0..6d020e4 100644 --- a/client/components/ozwell/OzwellModal.js +++ b/client/components/ozwell/OzwellModal.js @@ -62,6 +62,7 @@ Template.ozwellModal.onCreated(function () { template.suggestions = new ReactiveVar([]); template.selectedSuggestionIndex = new ReactiveVar(0); template.layoutMode = new ReactiveVar('modal'); // modal | sidecar + template.summaryVisible = new ReactiveVar(false); // Expose template instance for global access window.ozwellModalInstance = template; @@ -121,6 +122,16 @@ Template.ozwellModal.onCreated(function () { summary.push(`Current input: ${context.currentText}`); } + if (context.relatedFields && typeof context.relatedFields === 'object') { + const relatedLines = Object.entries(context.relatedFields) + .filter(([, value]) => typeof value === 'string' && value.trim().length > 0) + .map(([key, value]) => `• ${key}: ${value}`); + if (relatedLines.length > 0) { + summary.push('Other form values:'); + summary.push(...relatedLines); + } + } + return summary.length > 0 ? summary.join('\n') : 'No additional project context provided.'; }; @@ -129,31 +140,72 @@ Template.ozwellModal.onCreated(function () { template.messages.set([...history, { ...message, createdAt: new Date() }]); }; + const stripQuotes = (value = '') => value.replace(/^["'“”‘’\s]+|["'“”‘’\s]+$/g, '').trim(); + const extractSuggestions = (text = '') => { if (!text) return []; - let parts = text.split(/\n\s*\n+/).map((part) => part.trim()).filter(Boolean); - if (parts.length <= 1) { - const bulletParts = text.split(/(?:^|\n)\s*(?:[-*•]|\d+\.)\s+/).map((part) => part.trim()).filter(Boolean); - if (bulletParts.length > 1) { - parts = bulletParts; + const bulletRegex = /(?:^|\n)\s*(?:\d+\.|[-*•])\s+([\s\S]*?)(?=(?:\n\s*(?:\d+\.|[-*•])\s+)|$)/g; + const collectBullets = (source = '') => { + const results = []; + let match; + while ((match = bulletRegex.exec(source)) !== null) { + const suggestion = stripQuotes(match[1]); + if (suggestion) { + results.push(suggestion); + } } + return results; + }; + + const bullets = collectBullets(text); + if (bullets.length > 0) { + return bullets; } - if (parts.length <= 1) { - return []; + const rawParagraphs = text.split(/\n\s*\n+/).map((part) => part.trim()).filter(Boolean); + const paragraphs = rawParagraphs.filter((part, index) => { + if (index === 0 && /:\s*$/.test(part)) { + return false; + } + if (index === rawParagraphs.length - 1 && part.toLowerCase().startsWith('feel free')) { + return false; + } + return true; + }); + + const expanded = []; + paragraphs.forEach((part) => { + const innerBullets = collectBullets(part); + if (innerBullets.length > 0) { + expanded.push(...innerBullets); + } else { + const stripped = stripQuotes(part); + if (stripped) { + expanded.push(stripped); + } + } + }); + + const uniqueExpanded = expanded.filter((value, index, arr) => arr.indexOf(value) === index); + + if (uniqueExpanded.length > 1) { + return uniqueExpanded; } - return parts; + return []; }; const buildServerMessages = () => { const messages = []; - const system = template.systemMessage.get(); + const baseSystemMessage = template.systemMessage.get(); const summary = template.contextSummary.get(); - if (system) { - messages.push({ role: 'system', content: system }); + if (baseSystemMessage) { + messages.push({ + role: 'system', + content: `${baseSystemMessage}\nInstructions: Provide only polished, ready-to-paste suggestions. Avoid Markdown, template placeholders, or meta commentary. If generating multiple options, return each as its own numbered bullet.` + }); } if (summary) { @@ -295,6 +347,7 @@ Template.ozwellModal.onCreated(function () { template.suggestions.set([]); template.selectedSuggestionIndex.set(0); template.generatedContent.set(null); + template.summaryVisible.set(false); const metadata = { teamId: template.currentTeamId.get(), @@ -328,11 +381,11 @@ Template.ozwellModal.onCreated(function () { } } catch (error) { console.error('Failed to generate content from reference server:', error); - template.errorMessage.set(error?.reason || 'Failed to generate content. Please try again.'); - } finally { - template.isGenerating.set(false); - } - }; + template.errorMessage.set(error?.reason || 'Failed to generate content. Please try again.'); + } finally { + template.isGenerating.set(false); + } +}; template.loadPrompts(); }); @@ -353,6 +406,9 @@ Template.ozwellModal.helpers({ layoutIsSidecar() { return Template.instance().layoutMode.get() === 'sidecar'; }, + summaryVisible() { + return Template.instance().summaryVisible.get(); + }, composerText() { return Template.instance().composerText.get(); }, @@ -382,7 +438,11 @@ Template.ozwellModal.helpers({ return instance.suggestions.get().map((text, index) => ({ text, index })); }, suggestionsAvailable() { - return Template.instance().suggestions.get().length > 1; + return Template.instance().suggestions.get().length > 0; + }, + suggestionMaxHeight() { + const instance = Template.instance(); + return instance.layoutMode.get() === 'sidecar' ? '12rem' : '8rem'; }, messageWrapperClass(role) { return role === 'user' ? 'justify-end' : 'justify-start'; @@ -420,6 +480,10 @@ Template.ozwellModal.events({ const next = template.layoutMode.get() === 'sidecar' ? 'modal' : 'sidecar'; template.layoutMode.set(next); }, + 'click #toggle-context'(event, template) { + event.preventDefault(); + template.summaryVisible.set(!template.summaryVisible.get()); + }, 'click #ozwell-backdrop'(event, template) { if (event.target.id === 'ozwell-backdrop') { template.closeModal(); diff --git a/client/components/tickets/TicketsPage.html b/client/components/tickets/TicketsPage.html index 3b71ed7..f51cc5d 100644 --- a/client/components/tickets/TicketsPage.html +++ b/client/components/tickets/TicketsPage.html @@ -22,13 +22,13 @@

My Activities & Tasks

-
+
{{> ozwellButton}}
-
+
{{> ozwellButton}}
@@ -67,4 +67,4 @@

My Activities & Tasks

Create or join a project to start tracking your activities and time.

{{/if}} - \ No newline at end of file + diff --git a/client/main.js b/client/main.js index 58de517..2ea61ec 100644 --- a/client/main.js +++ b/client/main.js @@ -31,4 +31,4 @@ import './components/ozwell/OzwellButton.js'; // Import currentTime from MainLayout -//import { currentTime } from './components/layout/MainLayout.js'; \ No newline at end of file +//import { currentTime } from './components/layout/MainLayout.js'; From 2ddf841c32fb8cff52d9734ea03086a6ec7c5dd6 Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:26:22 -0400 Subject: [PATCH 11/57] Update OzwellModal header background style Cleared out the gradient style for one that matches the existing app design scheme --- client/components/ozwell/OzwellModal.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html index f58cec3..9260f29 100644 --- a/client/components/ozwell/OzwellModal.html +++ b/client/components/ozwell/OzwellModal.html @@ -1,7 +1,7 @@