+ {{#if recentsEnabled}}
{{#unless useMcpMode}}
- {{#if hasRecentConversations}}
-
+ {{/if}}
+ {{/unless}}
{{/if}}
+ {{#unless useMcpMode}}
{{#if selectedPrompt}}
@@ -180,7 +184,7 @@
How can Ozwell help?
{{/if}}
{{else}}
-
+
{{/unless}}
diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js
index d6b83d2..5271b5f 100644
--- a/client/components/ozwell/OzwellModal.js
+++ b/client/components/ozwell/OzwellModal.js
@@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor';
import './OzwellModal.html';
const DEFAULT_SYSTEM_MESSAGE = 'You are a helpful assistant for time tracking and project management.';
+const ENABLE_RECENT_CHATS = false;
const FALLBACK_PROMPTS = [
{
@@ -263,8 +264,10 @@ Template.ozwellModal.onCreated(function () {
template.suggestions.set([]);
template.selectedSuggestionIndex.set(0);
template.summaryVisible.set(false);
- template.currentConversationId.set(null);
- template.conversationLabel.set('');
+ if (ENABLE_RECENT_CHATS) {
+ template.currentConversationId.set(null);
+ template.conversationLabel.set('');
+ }
template.useMcpMode.set(false);
};
@@ -337,7 +340,9 @@ Template.ozwellModal.onCreated(function () {
template.headerSubtitle.set(context.teamName ? `Project: ${context.teamName}` : 'Ready to help with your work.');
template.isOzwellOpen.set(true);
template.layoutMode.set('modal');
- template.loadRecentConversations();
+ if (ENABLE_RECENT_CHATS) {
+ template.loadRecentConversations();
+ }
};
template.initializeConversation = async function (prompt) {
@@ -353,17 +358,19 @@ Template.ozwellModal.onCreated(function () {
template.generatedContent.set(null);
template.canSave.set(false);
template.errorMessage.set(null);
- template.currentConversationId.set(null);
- const promptTitle = prompt?.title || '';
- const userPreview = userMessage.replace(/\s+/g, ' ').trim().substring(0, 60);
- let conversationLabel = userPreview || promptTitle || 'Conversation';
+ if (ENABLE_RECENT_CHATS) {
+ template.currentConversationId.set(null);
+ const promptTitle = prompt?.title || '';
+ const userPreview = userMessage.replace(/\s+/g, ' ').trim().substring(0, 60);
+ let conversationLabel = userPreview || promptTitle || 'Conversation';
+
+ if (promptTitle && userPreview && prompt?.id !== 'custom') {
+ conversationLabel = `${promptTitle} — ${userPreview}`.substring(0, 120);
+ }
- if (promptTitle && userPreview && prompt?.id !== 'custom') {
- conversationLabel = `${promptTitle} — ${userPreview}`.substring(0, 120);
+ template.conversationLabel.set(conversationLabel);
}
- template.conversationLabel.set(conversationLabel);
-
if (prompt?.title) {
template.headerSubtitle.set(prompt.title);
}
@@ -385,7 +392,7 @@ Template.ozwellModal.onCreated(function () {
template.selectedSuggestionIndex.set(0);
template.generatedContent.set(null);
template.summaryVisible.set(false);
- if (!template.conversationLabel.get()) {
+ if (ENABLE_RECENT_CHATS && !template.conversationLabel.get()) {
template.conversationLabel.set(trimmed.substring(0, 60));
}
@@ -416,7 +423,9 @@ Template.ozwellModal.onCreated(function () {
}
template.canSave.set(true);
- template.persistConversation();
+ if (ENABLE_RECENT_CHATS) {
+ template.persistConversation();
+ }
} else {
template.errorMessage.set('The assistant returned no content. Please try again.');
}
@@ -429,6 +438,7 @@ Template.ozwellModal.onCreated(function () {
};
template.loadRecentConversations = function () {
+ if (!ENABLE_RECENT_CHATS) return;
const teamId = template.currentTeamId.get();
const fieldName = template.currentFieldName.get();
if (!teamId) {
@@ -447,6 +457,7 @@ Template.ozwellModal.onCreated(function () {
};
template.resumeConversation = function (conversationId) {
+ if (!ENABLE_RECENT_CHATS) return;
Meteor.call('getOzwellConversation', conversationId, (err, conversation) => {
if (err || !conversation) {
console.error('Failed to load conversation:', err);
@@ -479,6 +490,7 @@ Template.ozwellModal.onCreated(function () {
};
template.persistConversation = function () {
+ if (!ENABLE_RECENT_CHATS) return;
const teamId = template.currentTeamId.get();
const fieldName = template.currentFieldName.get();
if (!teamId || !fieldName) return;
@@ -616,10 +628,13 @@ Template.ozwellModal.helpers({
return Template.instance().useMcpMode.get();
},
recentConversations() {
- return Template.instance().recentConversations.get();
+ return ENABLE_RECENT_CHATS ? Template.instance().recentConversations.get() : [];
},
hasRecentConversations() {
- return Template.instance().recentConversations.get().length > 0;
+ return ENABLE_RECENT_CHATS && Template.instance().recentConversations.get().length > 0;
+ },
+ recentsEnabled() {
+ return ENABLE_RECENT_CHATS;
},
composerText() {
return Template.instance().composerText.get();
@@ -714,6 +729,7 @@ Template.ozwellModal.events({
template.summaryVisible.set(!template.summaryVisible.get());
},
'click .resume-conversation'(event, template) {
+ if (!ENABLE_RECENT_CHATS) return;
event.preventDefault();
const conversationId = event.currentTarget.getAttribute('data-id');
if (conversationId) {
@@ -721,6 +737,7 @@ Template.ozwellModal.events({
}
},
'click #ozwell-new-chat'(event, template) {
+ if (!ENABLE_RECENT_CHATS) return;
event.preventDefault();
template.resetConversation();
template.selectedPrompt.set(null);
diff --git a/public/tests/mcp-smoke-test.js b/public/tests/mcp-smoke-test.js
new file mode 100644
index 0000000..5f2706f
--- /dev/null
+++ b/public/tests/mcp-smoke-test.js
@@ -0,0 +1,175 @@
+export function runMcpSmokeTest(options = {}) {
+ const {
+ prompt = 'Demo prompt: Summarize recent progress.',
+ teamName = options.teamName || (window?.ozwellModalInstance?.currentContext?.get?.()?.teamName) || 'Sample Project',
+ username = options.username || (window?.ozwellModalInstance?.currentContext?.get?.()?.user?.username) || (Meteor.user()?.username || 'User')
+ } = options;
+
+ const context = {
+ teamName,
+ user: { username }
+ };
+
+ const overlay = document.createElement('div');
+ overlay.style.position = 'fixed';
+ overlay.style.bottom = '16px';
+ overlay.style.right = '16px';
+ overlay.style.width = '420px';
+ overlay.style.height = '620px';
+ overlay.style.background = 'rgba(17,24,39,0.95)';
+ overlay.style.border = '1px solid rgba(79,70,229,0.4)';
+ overlay.style.borderRadius = '18px';
+ overlay.style.boxShadow = '0 20px 45px rgba(15,23,42,0.4)';
+ overlay.style.zIndex = '9999';
+ overlay.style.display = 'flex';
+ overlay.style.flexDirection = 'column';
+ overlay.style.color = '#f9fafb';
+ overlay.style.fontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
+
+ const header = document.createElement('div');
+ header.style.display = 'flex';
+ header.style.justifyContent = 'space-between';
+ header.style.alignItems = 'center';
+ header.style.padding = '12px 16px';
+ header.style.borderBottom = '1px solid rgba(255,255,255,0.08)';
+ header.innerHTML = '
MCP Smoke Test';
+
+ const closeBtn = document.createElement('button');
+ closeBtn.textContent = '×';
+ closeBtn.style.background = 'transparent';
+ closeBtn.style.border = 'none';
+ closeBtn.style.color = '#f9fafb';
+ closeBtn.style.fontSize = '20px';
+ closeBtn.style.cursor = 'pointer';
+ closeBtn.onclick = () => {
+ window.removeEventListener('message', handler);
+ overlay.remove();
+ };
+ header.appendChild(closeBtn);
+
+ const frame = document.createElement('iframe');
+ frame.src = '/ozwell-frame.html';
+ frame.style.flex = '1';
+ frame.style.border = '0';
+ frame.style.borderBottom = '1px solid rgba(255,255,255,0.08)';
+
+ const controlBar = document.createElement('div');
+ controlBar.style.display = 'flex';
+ controlBar.style.gap = '8px';
+ controlBar.style.padding = '12px 16px';
+
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.value = prompt;
+ input.placeholder = 'Type prompt...';
+ input.style.flex = '1';
+ input.style.borderRadius = '12px';
+ input.style.border = '1px solid rgba(255,255,255,0.12)';
+ input.style.background = 'rgba(255,255,255,0.06)';
+ input.style.color = '#f9fafb';
+ input.style.padding = '10px 12px';
+
+ const sendBtn = document.createElement('button');
+ sendBtn.textContent = 'Send prompt';
+ sendBtn.style.borderRadius = '12px';
+ sendBtn.style.border = 'none';
+ sendBtn.style.background = '#4f46e5';
+ sendBtn.style.color = '#fff';
+ sendBtn.style.padding = '10px 18px';
+ sendBtn.style.cursor = 'pointer';
+
+ const logArea = document.createElement('pre');
+ logArea.style.margin = '0';
+ logArea.style.padding = '12px 16px';
+ logArea.style.background = 'rgba(255,255,255,0.03)';
+ logArea.style.borderTop = '1px solid rgba(255,255,255,0.08)';
+ logArea.style.maxHeight = '120px';
+ logArea.style.overflow = 'auto';
+ logArea.style.fontSize = '12px';
+ logArea.textContent = 'Waiting for client hello...\n';
+
+ controlBar.appendChild(input);
+ controlBar.appendChild(sendBtn);
+
+ overlay.appendChild(header);
+ overlay.appendChild(frame);
+ overlay.appendChild(controlBar);
+ overlay.appendChild(logArea);
+
+ document.body.appendChild(overlay);
+
+ let ready = false;
+
+ function log(line) {
+ logArea.textContent += `${line}\n`;
+ logArea.scrollTop = logArea.scrollHeight;
+ }
+
+ function buildMessages(promptText) {
+ const systemMessage = `You are a helpful assistant for time tracking and project management.\nInstructions: Provide only polished, ready-to-paste suggestions. Avoid Markdown, template placeholders, or meta commentary.`;
+ const summary = [`Project: ${context.teamName}`, `User: ${context.user.username}`].join('\n');
+ return [
+ { role: 'system', content: systemMessage },
+ { role: 'system', content: `Project context:\n${summary}` },
+ { role: 'user', content: promptText }
+ ];
+ }
+
+ function reply(message) {
+ if (!frame.contentWindow) return;
+ frame.contentWindow.postMessage({ source: 'ozwell-modal-bridge', ...message }, '*');
+ }
+
+ function handler(event) {
+ const data = event.data;
+ if (!data || data.source !== 'ozwell-mcp-frame') return;
+
+ if (data.type === 'client-hello') {
+ log('Client hello received');
+ ready = true;
+ reply({ type: 'mcp-ready', contextSummary: `${context.teamName} (manual test)` });
+ return;
+ }
+
+ if (data.type === 'model-request') {
+ const promptText = data.payload?.prompt || '';
+ log(`Model request -> ${promptText}`);
+ Meteor.call('callReferenceAssistant', {
+ messages: buildMessages(promptText),
+ metadata: { transport: 'mcp-smoke-test', teamName: context.teamName }
+ }, (err, result) => {
+ if (err) {
+ log(`Error: ${err.reason || err.message}`);
+ reply({ type: 'model-error', error: err.reason || err.message });
+ } else {
+ const content = result?.content || '';
+ log(`Model response <- ${content}`);
+ reply({ type: 'model-response', payload: { content } });
+ }
+ });
+ return;
+ }
+
+ if (data.type === 'model-response') {
+ log(`Frame echo response: ${data.payload?.content}`);
+ }
+
+ if (data.type === 'model-error') {
+ log(`Frame error: ${data.error}`);
+ }
+ }
+
+ window.addEventListener('message', handler);
+
+ sendBtn.onclick = () => {
+ if (!ready) {
+ log('Frame not ready yet.');
+ return;
+ }
+ const promptText = input.value.trim();
+ if (!promptText) return;
+ reply({ type: 'model-request', payload: { prompt: promptText } });
+ };
+
+ log('Smoke test harness attached. Wait for client hello, then click "Send prompt".');
+}
From 4836ee2b021ecddfc68b9a80b4eb99e4d2dd551e Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Wed, 24 Sep 2025 20:43:43 -0400
Subject: [PATCH 18/57] Add MCP stub transport comments and clarify usage
Added explanatory comments to OzwellModal.js and ozwell-frame.html to clarify that the current MCP bridge implementation is a stub and does not follow the real MCP wire protocol. These notes provide guidance for future integration with the official MCP transport.
---
client/components/ozwell/OzwellModal.js | 36 ++++++++++++++-----------
public/ozwell-frame.html | 4 +++
2 files changed, 24 insertions(+), 16 deletions(-)
diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js
index 5271b5f..29f5674 100644
--- a/client/components/ozwell/OzwellModal.js
+++ b/client/components/ozwell/OzwellModal.js
@@ -68,7 +68,7 @@ Template.ozwellModal.onCreated(function () {
template.currentConversationId = new ReactiveVar(null);
template.conversationLabel = new ReactiveVar('');
template.currentFieldName = new ReactiveVar('general');
- template.useMcpMode = new ReactiveVar(false);
+ template.useMcpMode = new ReactiveVar(false); // NOTE(uid_future-mcp): MCP toggle currently drives a stub transport; see comments below.
// Expose template instance for global access
window.ozwellModalInstance = template;
@@ -539,25 +539,29 @@ Template.ozwellModal.onCreated(function () {
});
};
- template.setupMcpBridge = function () {
- if (template.mcpListener) return;
+ template.setupMcpBridge = function () {
+ if (template.mcpListener) return;
- template.mcpListener = function (event) {
- const data = event.data;
- if (!data || data.source !== 'ozwell-mcp-frame') return;
+ template.mcpListener = function (event) {
+ const data = event.data;
+ if (!data || data.source !== 'ozwell-mcp-frame') return;
- const iframe = document.getElementById('ozwell-mcp-frame');
- if (!iframe || !iframe.contentWindow) return;
+ const iframe = document.getElementById('ozwell-mcp-frame');
+ if (!iframe || !iframe.contentWindow) return;
- const reply = (message) => {
- iframe.contentWindow.postMessage({ source: 'ozwell-modal-bridge', ...message }, '*');
- };
+ const reply = (message) => {
+ iframe.contentWindow.postMessage({ source: 'ozwell-modal-bridge', ...message }, '*');
+ };
- if (data.type === 'client-hello') {
- const summary = buildContextSummary(template.currentContext.get() || {});
- reply({ type: 'mcp-ready', contextSummary: summary });
- return;
- }
+ // NOTE(uid_future-mcp): This handler does NOT implement the real MCP spec. It simply
+ // proxies the iframe's prompt string directly to callReferenceAssistant, and wraps the
+ // REST response in a minimal model-response shape. When the reference server (or Ozwell)
+ // exposes true MCP endpoints, replace this stub with actual MCP serialization.
+ if (data.type === 'client-hello') {
+ const summary = buildContextSummary(template.currentContext.get() || {});
+ reply({ type: 'mcp-ready', contextSummary: summary });
+ return;
+ }
if (data.type === 'model-request') {
const promptText = data.payload?.prompt || '';
diff --git a/public/ozwell-frame.html b/public/ozwell-frame.html
index ac08684..e3d42ef 100644
--- a/public/ozwell-frame.html
+++ b/public/ozwell-frame.html
@@ -83,6 +83,10 @@
+
+
+
{{> Template.dynamic template=currentScreen}}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/chat-wrapper.css b/public/chat-wrapper.css
new file mode 100644
index 0000000..2752e84
--- /dev/null
+++ b/public/chat-wrapper.css
@@ -0,0 +1,148 @@
+/* Ozwell Chat Widget - Floating Wrapper Styles */
+
+/* Floating Chat Button */
+#ozwell-chat-button {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border: none;
+ cursor: pointer;
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.2s, box-shadow 0.2s;
+ z-index: 9998;
+ font-size: 28px;
+}
+
+#ozwell-chat-button:hover {
+ transform: scale(1.1);
+ box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6);
+}
+
+#ozwell-chat-button.hidden {
+ display: none;
+}
+
+/* Chat Container */
+#ozwell-chat-container {
+ position: fixed;
+ bottom: 100px;
+ right: 20px;
+ width: 400px;
+ height: 600px;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
+ display: none;
+ flex-direction: column;
+ z-index: 9999;
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
+
+#ozwell-chat-container.open {
+ display: flex;
+}
+
+#ozwell-chat-container.minimized {
+ height: 60px;
+ overflow: hidden;
+}
+
+#ozwell-chat-container.dragging {
+ opacity: 0.8;
+ cursor: move;
+}
+
+/* Chat Header */
+#ozwell-chat-header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: move;
+ user-select: none;
+}
+
+#ozwell-chat-header h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ flex: 1;
+}
+
+#ozwell-chat-controls {
+ display: flex;
+ gap: 8px;
+}
+
+#ozwell-chat-controls button {
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
+ color: white;
+ width: 28px;
+ height: 28px;
+ border-radius: 4px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+ font-size: 16px;
+}
+
+#ozwell-chat-controls button:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+/* Chat Content (iframe container) */
+#ozwell-chat-content {
+ flex: 1;
+ overflow: hidden;
+ background: white;
+}
+
+#ozwell-chat-content iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ #ozwell-chat-container {
+ width: calc(100vw - 40px);
+ height: calc(100vh - 140px);
+ right: 20px;
+ bottom: 100px;
+ }
+
+ #ozwell-chat-button {
+ bottom: 20px;
+ right: 20px;
+ }
+}
+
+/* Animation for opening */
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+#ozwell-chat-container.open {
+ animation: slideIn 0.3s ease;
+}
diff --git a/public/chat-wrapper.js b/public/chat-wrapper.js
new file mode 100644
index 0000000..a25c083
--- /dev/null
+++ b/public/chat-wrapper.js
@@ -0,0 +1,260 @@
+/**
+ * Ozwell Chat Widget - Draggable Wrapper
+ * Provides floating button and draggable chat window
+ */
+
+class ChatWrapper {
+ constructor() {
+ this.isOpen = false;
+ this.isMinimized = false;
+ this.isDragging = false;
+ this.dragOffset = { x: 0, y: 0 };
+
+ this.init();
+ }
+
+ init() {
+ // Immediately hide any auto-created Ozwell iframes (they load before our wrapper)
+ this.hideAutoCreatedOzwellIframes();
+
+ // Only create widget if user is logged in (Meteor specific check)
+ if (typeof Meteor !== 'undefined' && !Meteor.userId()) {
+ // User not logged in, don't create widget
+ // Check again when user logs in
+ const checkLogin = setInterval(() => {
+ if (Meteor.userId()) {
+ clearInterval(checkLogin);
+ this.createElements();
+ this.attachEventListeners();
+ }
+ }, 500);
+ return;
+ }
+
+ this.createElements();
+ this.attachEventListeners();
+ }
+
+ hideAutoCreatedOzwellIframes() {
+ // Watch for Ozwell iframes being created and hide them temporarily
+ const hideIframeIfNotInContainer = (iframe) => {
+ // Only hide if it's not already in our container
+ if (!iframe.closest('#ozwell-chat-content')) {
+ iframe.style.visibility = 'hidden';
+ iframe.style.position = 'fixed';
+ iframe.style.left = '-9999px';
+ iframe.style.top = '-9999px';
+ }
+ };
+
+ // Check for existing iframes
+ const checkExisting = () => {
+ const iframes = document.querySelectorAll('iframe[src*="widget.html"]');
+ iframes.forEach(hideIframeIfNotInContainer);
+ };
+
+ // Initial check
+ checkExisting();
+
+ // Watch for new iframes (but only run once per iframe)
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ mutation.addedNodes.forEach((node) => {
+ if (node.tagName === 'IFRAME' && node.src && node.src.includes('widget.html')) {
+ hideIframeIfNotInContainer(node);
+ }
+ });
+ });
+ });
+
+ observer.observe(document.body, { childList: true, subtree: true });
+ this.iframeObserver = observer;
+ }
+
+ createElements() {
+ // Create floating button
+ this.button = document.createElement('button');
+ this.button.id = 'ozwell-chat-button';
+ this.button.innerHTML = '💬';
+ this.button.title = 'Open TimeHarbor Assistant';
+ document.body.appendChild(this.button);
+
+ // Create chat container
+ this.container = document.createElement('div');
+ this.container.id = 'ozwell-chat-container';
+ this.container.innerHTML = `
+
+
+
+
+ `;
+ document.body.appendChild(this.container);
+
+ // Store references
+ this.header = document.getElementById('ozwell-chat-header');
+ this.content = document.getElementById('ozwell-chat-content');
+ this.minimizeBtn = document.getElementById('ozwell-minimize-btn');
+ this.closeBtn = document.getElementById('ozwell-close-btn');
+ }
+
+ attachEventListeners() {
+ // Button click - toggle open/close
+ this.button.addEventListener('click', () => this.toggle());
+
+ // Header drag functionality
+ this.header.addEventListener('mousedown', (e) => this.startDrag(e));
+ document.addEventListener('mousemove', (e) => this.drag(e));
+ document.addEventListener('mouseup', () => this.stopDrag());
+
+ // Control buttons
+ this.minimizeBtn.addEventListener('click', () => this.toggleMinimize());
+ this.closeBtn.addEventListener('click', () => this.close());
+
+ // Prevent text selection while dragging
+ this.header.addEventListener('selectstart', (e) => e.preventDefault());
+ }
+
+ toggle() {
+ if (this.isOpen) {
+ this.close();
+ } else {
+ this.open();
+ }
+ }
+
+ open() {
+ this.isOpen = true;
+ this.container.classList.add('open');
+ this.button.classList.add('hidden');
+
+ // Load Ozwell widget if configured
+ this.loadWidget();
+ }
+
+ close() {
+ this.isOpen = false;
+ this.isMinimized = false;
+ this.container.classList.remove('open', 'minimized');
+ this.button.classList.remove('hidden');
+ }
+
+ toggleMinimize() {
+ this.isMinimized = !this.isMinimized;
+ if (this.isMinimized) {
+ this.container.classList.add('minimized');
+ this.minimizeBtn.innerHTML = '□';
+ this.minimizeBtn.title = 'Maximize';
+ } else {
+ this.container.classList.remove('minimized');
+ this.minimizeBtn.innerHTML = '−';
+ this.minimizeBtn.title = 'Minimize';
+ }
+ }
+
+ startDrag(e) {
+ if (e.target.closest('button')) return; // Don't drag when clicking buttons
+
+ this.isDragging = true;
+ this.container.classList.add('dragging');
+
+ const rect = this.container.getBoundingClientRect();
+ this.dragOffset.x = e.clientX - rect.left;
+ this.dragOffset.y = e.clientY - rect.top;
+ }
+
+ drag(e) {
+ if (!this.isDragging) return;
+
+ e.preventDefault();
+
+ let newX = e.clientX - this.dragOffset.x;
+ let newY = e.clientY - this.dragOffset.y;
+
+ // Keep within viewport bounds
+ const maxX = window.innerWidth - this.container.offsetWidth;
+ const maxY = window.innerHeight - this.container.offsetHeight;
+
+ newX = Math.max(0, Math.min(newX, maxX));
+ newY = Math.max(0, Math.min(newY, maxY));
+
+ this.container.style.left = newX + 'px';
+ this.container.style.top = newY + 'px';
+ this.container.style.right = 'auto';
+ this.container.style.bottom = 'auto';
+ }
+
+ stopDrag() {
+ if (this.isDragging) {
+ this.isDragging = false;
+ this.container.classList.remove('dragging');
+ }
+ }
+
+ loadWidget() {
+ // Check if Ozwell widget is configured
+ if (!window.OzwellChatConfig) {
+ console.warn('OzwellChatConfig not found. Widget will not load.');
+ this.content.innerHTML = '
Chat widget configuration missing.
';
+ return;
+ }
+
+ // Check if widget already loaded
+ if (this.content.querySelector('iframe')) {
+ return;
+ }
+
+ // Wait for Ozwell embed script to load
+ const waitForOzwell = setInterval(() => {
+ if (window.OzwellChat && window.OzwellChat.iframe) {
+ clearInterval(waitForOzwell);
+
+ // Get the Ozwell iframe
+ const ozwellIframe = window.OzwellChat.iframe;
+
+ // Remove from its current location
+ if (ozwellIframe.parentElement && ozwellIframe.parentElement !== this.content) {
+ ozwellIframe.remove();
+ }
+
+ // Move Ozwell iframe into our container
+ this.content.appendChild(ozwellIframe);
+
+ // IMPORTANT: Reset all styles to make it visible in our container
+ ozwellIframe.style.width = '100%';
+ ozwellIframe.style.height = '100%';
+ ozwellIframe.style.border = 'none';
+ ozwellIframe.style.display = 'block';
+ ozwellIframe.style.visibility = 'visible';
+ ozwellIframe.style.position = 'relative';
+ ozwellIframe.style.left = '0';
+ ozwellIframe.style.top = '0';
+
+ console.log('Ozwell widget loaded successfully');
+ }
+ }, 100);
+
+ // Timeout after 5 seconds
+ setTimeout(() => {
+ clearInterval(waitForOzwell);
+ if (!this.content.querySelector('iframe')) {
+ console.error('Ozwell widget failed to load');
+ this.content.innerHTML = '
Failed to load chat widget. Please refresh.
';
+ }
+ }, 5000);
+ }
+}
+
+// Auto-initialize when DOM is ready
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ window.chatWrapper = new ChatWrapper();
+ });
+} else {
+ window.chatWrapper = new ChatWrapper();
+}
diff --git a/public/ozwell-iframe-sync.js b/public/ozwell-iframe-sync.js
new file mode 100644
index 0000000..171f91d
--- /dev/null
+++ b/public/ozwell-iframe-sync.js
@@ -0,0 +1,150 @@
+/**
+ * Ozwell iframe-sync Integration for TimeHarbor
+ * Syncs ticket form state with the chat widget in real-time
+ */
+
+class OzwellStateSync {
+ constructor() {
+ this.broker = null;
+ this.formFields = {
+ title: null,
+ description: null,
+ hours: null,
+ minutes: null,
+ seconds: null,
+ team: null
+ };
+ this.init();
+ }
+
+ init() {
+ // Wait for user to be logged in and form to be available
+ const checkFormReady = setInterval(() => {
+ if (typeof Meteor !== 'undefined' && Meteor.userId()) {
+ // Try to find form fields
+ this.formFields.title = document.querySelector('input[name="title"]');
+ this.formFields.description = document.querySelector('textarea[name="github"]');
+ this.formFields.hours = document.querySelector('input[name="hours"]');
+ this.formFields.minutes = document.querySelector('input[name="minutes"]');
+ this.formFields.seconds = document.querySelector('input[name="seconds"]');
+ this.formFields.team = document.querySelector('select[name="team"]');
+
+ // Check if at least title field exists (main indicator form is loaded)
+ if (this.formFields.title) {
+ clearInterval(checkFormReady);
+ this.initializeBroker();
+ this.attachListeners();
+ console.log('[iframe-sync] State sync initialized');
+ }
+ }
+ }, 500);
+ }
+
+ initializeBroker() {
+ // Initialize broker with current form state
+ this.broker = new IframeSyncBroker({
+ ticketForm: this.getCurrentFormState()
+ });
+
+ console.log('[iframe-sync] Broker initialized with state:', this.getCurrentFormState());
+ }
+
+ getCurrentFormState() {
+ return {
+ title: this.formFields.title?.value || '',
+ description: this.formFields.description?.value || '',
+ hours: this.formFields.hours?.value || '0',
+ minutes: this.formFields.minutes?.value || '0',
+ seconds: this.formFields.seconds?.value || '0',
+ team: this.formFields.team?.value || '',
+ teamName: this.formFields.team?.selectedOptions[0]?.text || ''
+ };
+ }
+
+ updateState(field, value) {
+ if (!this.broker) {
+ console.warn('[iframe-sync] Broker not initialized yet');
+ return;
+ }
+
+ const update = {
+ ticketForm: {
+ [field]: value
+ }
+ };
+
+ // For team changes, also include the team name
+ if (field === 'team') {
+ update.ticketForm.teamName = this.formFields.team?.selectedOptions[0]?.text || '';
+ }
+
+ this.broker.stateChange(update);
+ console.log(`[iframe-sync] State updated: ${field} = ${value}`);
+ }
+
+ attachListeners() {
+ // Title field
+ if (this.formFields.title) {
+ this.formFields.title.addEventListener('input', (e) => {
+ this.updateState('title', e.target.value);
+ });
+ }
+
+ // Description field
+ if (this.formFields.description) {
+ this.formFields.description.addEventListener('input', (e) => {
+ this.updateState('description', e.target.value);
+ });
+ }
+
+ // Hours field
+ if (this.formFields.hours) {
+ this.formFields.hours.addEventListener('input', (e) => {
+ this.updateState('hours', e.target.value);
+ });
+ }
+
+ // Minutes field
+ if (this.formFields.minutes) {
+ this.formFields.minutes.addEventListener('input', (e) => {
+ this.updateState('minutes', e.target.value);
+ });
+ }
+
+ // Seconds field
+ if (this.formFields.seconds) {
+ this.formFields.seconds.addEventListener('input', (e) => {
+ this.updateState('seconds', e.target.value);
+ });
+ }
+
+ // Team selection
+ if (this.formFields.team) {
+ this.formFields.team.addEventListener('change', (e) => {
+ this.updateState('team', e.target.value);
+ });
+ }
+
+ console.log('[iframe-sync] Event listeners attached to form fields');
+ }
+
+ // Public method to manually trigger state sync (useful after tool execution)
+ syncCurrentState() {
+ if (!this.broker) return;
+
+ this.broker.stateChange({
+ ticketForm: this.getCurrentFormState()
+ });
+
+ console.log('[iframe-sync] Manual state sync triggered');
+ }
+}
+
+// Auto-initialize when DOM is ready
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ window.ozwellStateSync = new OzwellStateSync();
+ });
+} else {
+ window.ozwellStateSync = new OzwellStateSync();
+}
diff --git a/public/ozwell-mcp-tools.js b/public/ozwell-mcp-tools.js
new file mode 100644
index 0000000..f490ae1
--- /dev/null
+++ b/public/ozwell-mcp-tools.js
@@ -0,0 +1,373 @@
+/**
+ * Ozwell MCP Tools for TimeHarbor
+ * Provides tools for the AI assistant to interact with ticket forms and retrieve history
+ */
+
+// MCP Tools Definitions (OpenAI function calling format)
+const mcpTools = [
+ {
+ type: 'function',
+ function: {
+ name: 'update_ticket_title',
+ description: 'Updates the title field of the current ticket form',
+ parameters: {
+ type: 'object',
+ properties: {
+ title: {
+ type: 'string',
+ description: 'The new title for the ticket'
+ }
+ },
+ required: ['title']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'update_ticket_description',
+ description: 'Updates the description/reference notes field of the current ticket form',
+ parameters: {
+ type: 'object',
+ properties: {
+ description: {
+ type: 'string',
+ description: 'The new description/reference notes for the ticket'
+ }
+ },
+ required: ['description']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'update_ticket_time',
+ description: 'Updates the time fields (hours, minutes, seconds) of the current ticket form',
+ parameters: {
+ type: 'object',
+ properties: {
+ hours: {
+ type: 'number',
+ description: 'Hours spent (0-23)'
+ },
+ minutes: {
+ type: 'number',
+ description: 'Minutes spent (0-59)'
+ },
+ seconds: {
+ type: 'number',
+ description: 'Seconds spent (0-59)'
+ }
+ },
+ required: []
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'get_project_history',
+ description: 'Retrieves recent tickets for the current project/team to provide context about past work',
+ parameters: {
+ type: 'object',
+ properties: {
+ days: {
+ type: 'number',
+ description: 'Number of days to look back (default: 7)',
+ default: 7
+ },
+ limit: {
+ type: 'number',
+ description: 'Maximum number of tickets to retrieve (default: 20)',
+ default: 20
+ }
+ },
+ required: []
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'get_conversation_history',
+ description: 'Retrieves past chat conversations for the current project to provide context from previous interactions',
+ parameters: {
+ type: 'object',
+ properties: {
+ limit: {
+ type: 'number',
+ description: 'Maximum number of conversations to retrieve (default: 10)',
+ default: 10
+ }
+ },
+ required: []
+ }
+ }
+ }
+];
+
+// Add tools to OzwellChatConfig (widget reads from here)
+if (window.OzwellChatConfig) {
+ window.OzwellChatConfig.tools = mcpTools;
+ console.log('[MCP Tools] Added', mcpTools.length, 'tools to OzwellChatConfig');
+} else {
+ console.error('[MCP Tools] window.OzwellChatConfig not found! Tools will not be available.');
+}
+
+// Tool Handler Functions
+const toolHandlers = {
+ update_ticket_title: async (params) => {
+ const titleInput = document.querySelector('[name="title"]');
+ if (!titleInput) {
+ return { success: false, error: 'Title input field not found' };
+ }
+
+ titleInput.value = params.title;
+
+ // Trigger change event for any listeners
+ titleInput.dispatchEvent(new Event('input', { bubbles: true }));
+ titleInput.dispatchEvent(new Event('change', { bubbles: true }));
+
+ // Sync state with widget
+ if (window.ozwellStateSync) {
+ window.ozwellStateSync.syncCurrentState();
+ }
+
+ return {
+ success: true,
+ message: `Title updated to: ${params.title}`
+ };
+ },
+
+ update_ticket_description: async (params) => {
+ const descInput = document.querySelector('[name="github"]');
+ if (!descInput) {
+ return { success: false, error: 'Description input field not found' };
+ }
+
+ descInput.value = params.description;
+
+ // Trigger change event for any listeners
+ descInput.dispatchEvent(new Event('input', { bubbles: true }));
+ descInput.dispatchEvent(new Event('change', { bubbles: true }));
+
+ // Sync state with widget
+ if (window.ozwellStateSync) {
+ window.ozwellStateSync.syncCurrentState();
+ }
+
+ return {
+ success: true,
+ message: `Description updated`
+ };
+ },
+
+ update_ticket_time: async (params) => {
+ const hoursInput = document.querySelector('[name="hours"]');
+ const minutesInput = document.querySelector('[name="minutes"]');
+ const secondsInput = document.querySelector('[name="seconds"]');
+
+ const updated = [];
+
+ if (params.hours !== undefined && hoursInput) {
+ hoursInput.value = params.hours;
+ hoursInput.dispatchEvent(new Event('input', { bubbles: true }));
+ hoursInput.dispatchEvent(new Event('change', { bubbles: true }));
+ updated.push(`hours: ${params.hours}`);
+ }
+
+ if (params.minutes !== undefined && minutesInput) {
+ minutesInput.value = params.minutes;
+ minutesInput.dispatchEvent(new Event('input', { bubbles: true }));
+ minutesInput.dispatchEvent(new Event('change', { bubbles: true }));
+ updated.push(`minutes: ${params.minutes}`);
+ }
+
+ if (params.seconds !== undefined && secondsInput) {
+ secondsInput.value = params.seconds;
+ secondsInput.dispatchEvent(new Event('input', { bubbles: true }));
+ secondsInput.dispatchEvent(new Event('change', { bubbles: true }));
+ updated.push(`seconds: ${params.seconds}`);
+ }
+
+ if (updated.length === 0) {
+ return { success: false, error: 'No time fields found or updated' };
+ }
+
+ // Sync state with widget
+ if (window.ozwellStateSync) {
+ window.ozwellStateSync.syncCurrentState();
+ }
+
+ return {
+ success: true,
+ message: `Time updated: ${updated.join(', ')}`
+ };
+ },
+
+ get_project_history: async (params) => {
+ // Get current team ID from the page
+ const teamSelect = document.querySelector('[name="team"]');
+ if (!teamSelect || !teamSelect.value) {
+ return {
+ success: false,
+ error: 'No team selected. Please select a team first.'
+ };
+ }
+
+ const teamId = teamSelect.value;
+ const days = params.days || 7;
+ const limit = params.limit || 20;
+
+ try {
+ // Call Meteor method to get project history
+ const history = await new Promise((resolve, reject) => {
+ Meteor.call('getRecentProjectTickets', teamId, days, limit, (error, result) => {
+ if (error) reject(error);
+ else resolve(result);
+ });
+ });
+
+ return {
+ success: true,
+ data: history,
+ message: `Retrieved ${history.length} recent tickets from the last ${days} days`
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to retrieve project history: ${error.message}`
+ };
+ }
+ },
+
+ get_conversation_history: async (params) => {
+ // Get current team ID from the page
+ const teamSelect = document.querySelector('[name="team"]');
+ if (!teamSelect || !teamSelect.value) {
+ return {
+ success: false,
+ error: 'No team selected. Please select a team first.'
+ };
+ }
+
+ const teamId = teamSelect.value;
+ const limit = params.limit || 10;
+
+ try {
+ // Call Meteor method to get conversation history
+ const history = await new Promise((resolve, reject) => {
+ Meteor.call('getProjectChatHistory', teamId, limit, (error, result) => {
+ if (error) reject(error);
+ else resolve(result);
+ });
+ });
+
+ return {
+ success: true,
+ data: history,
+ message: `Retrieved ${history.length} previous conversations`
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to retrieve conversation history: ${error.message}`
+ };
+ }
+ }
+};
+
+// Initialize MCP Tools Integration
+class OzwellMCPIntegration {
+ constructor() {
+ this.widgetIframe = null;
+ this.init();
+ }
+
+ init() {
+ // Wait for widget to send tool execution requests
+ window.addEventListener('message', (event) => {
+ // Security check: Only accept messages from our widget iframe or reference server
+ const validOrigins = [
+ 'http://localhost:3000',
+ 'http://127.0.0.1:3000',
+ 'null' // Widget iframe may have null origin due to CORS/iframe loading
+ ];
+
+ if (!validOrigins.includes(event.origin)) {
+ // Silently ignore Meteor and other internal messages
+ return;
+ }
+
+ // For null origin, verify it's from our widget iframe
+ if (event.origin === 'null') {
+ const widgetIframe = document.querySelector('iframe[src*="widget.html"]');
+ if (!widgetIframe || event.source !== widgetIframe.contentWindow) {
+ return;
+ }
+ }
+
+ const { type, toolCall } = event.data;
+
+ if (type === 'CALL_TOOL') {
+ // Widget is requesting to execute a tool
+ console.log('[MCP Tools] Tool call requested:', toolCall);
+ this.executeTool(toolCall);
+ }
+ });
+
+ console.log('[MCP Tools] Integration initialized, listening for tool calls');
+ }
+
+ async executeTool(toolCall) {
+ const { name, arguments: args } = toolCall;
+ console.log(`[MCP Tools] Executing: ${name}`, args);
+
+ const handler = toolHandlers[name];
+
+ if (!handler) {
+ this.sendToolResult({
+ success: false,
+ error: `Unknown tool: ${name}`
+ });
+ return;
+ }
+
+ try {
+ const result = await handler(args);
+ this.sendToolResult(result);
+ } catch (error) {
+ this.sendToolResult({
+ success: false,
+ error: `Tool execution failed: ${error.message}`
+ });
+ }
+ }
+
+ sendToolResult(result) {
+ if (!this.widgetIframe) {
+ console.error('[MCP Tools] Cannot send tool result: widget iframe not found');
+ return;
+ }
+
+ this.widgetIframe.contentWindow.postMessage(
+ {
+ type: 'TOOL_RESULT',
+ result: result
+ },
+ '*'
+ );
+
+ console.log('[MCP Tools] Tool result sent:', result.message || result.error);
+ }
+}
+
+// Auto-initialize when DOM is ready
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ window.ozwellMCPIntegration = new OzwellMCPIntegration();
+ });
+} else {
+ window.ozwellMCPIntegration = new OzwellMCPIntegration();
+}
From dc469e24e79bdc01c951f0db0ffc7cb58b3cec55 Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Thu, 16 Oct 2025 13:17:49 -0400
Subject: [PATCH 30/57] feat: Update AI model and enhance iframe-sync
integration for improved state management
---
client/main.html | 2 +-
public/ozwell-iframe-sync.js | 35 +++++++++++++++++++++++++----------
public/ozwell-mcp-tools.js | 26 ++++++++++++++++++--------
3 files changed, 44 insertions(+), 19 deletions(-)
diff --git a/client/main.html b/client/main.html
index e764d40..5a5d72e 100644
--- a/client/main.html
+++ b/client/main.html
@@ -15,7 +15,7 @@
endpoint: 'http://localhost:3000/embed/chat',
title: 'TimeHarbor Assistant',
placeholder: 'Ask about your tickets...',
- model: 'llama3.1:8b'
+ model: 'qwen2.5:14b'
};
diff --git a/public/ozwell-iframe-sync.js b/public/ozwell-iframe-sync.js
index 171f91d..061eb11 100644
--- a/public/ozwell-iframe-sync.js
+++ b/public/ozwell-iframe-sync.js
@@ -6,6 +6,7 @@
class OzwellStateSync {
constructor() {
this.broker = null;
+ this.client = null;
this.formFields = {
title: null,
description: null,
@@ -32,7 +33,7 @@ class OzwellStateSync {
// Check if at least title field exists (main indicator form is loaded)
if (this.formFields.title) {
clearInterval(checkFormReady);
- this.initializeBroker();
+ this.initializeBrokerAndClient();
this.attachListeners();
console.log('[iframe-sync] State sync initialized');
}
@@ -40,13 +41,25 @@ class OzwellStateSync {
}, 500);
}
- initializeBroker() {
- // Initialize broker with current form state
- this.broker = new IframeSyncBroker({
+ initializeBrokerAndClient() {
+ // Initialize broker (no parameters - just a message relay)
+ this.broker = new IframeSyncBroker();
+
+ // Initialize client for parent page (parent needs a client too!)
+ this.client = new IframeSyncClient('timeharbor-parent', (state) => {
+ console.log('[iframe-sync] Received state update:', state);
+ // Parent can receive state updates from widget here if needed
+ });
+
+ // Register client with broker
+ this.client.ready();
+
+ // Send initial form state
+ this.client.stateChange({
ticketForm: this.getCurrentFormState()
});
- console.log('[iframe-sync] Broker initialized with state:', this.getCurrentFormState());
+ console.log('[iframe-sync] Broker and client initialized with state:', this.getCurrentFormState());
}
getCurrentFormState() {
@@ -62,8 +75,8 @@ class OzwellStateSync {
}
updateState(field, value) {
- if (!this.broker) {
- console.warn('[iframe-sync] Broker not initialized yet');
+ if (!this.client) {
+ console.warn('[iframe-sync] Client not initialized yet');
return;
}
@@ -78,7 +91,8 @@ class OzwellStateSync {
update.ticketForm.teamName = this.formFields.team?.selectedOptions[0]?.text || '';
}
- this.broker.stateChange(update);
+ // Use CLIENT to update state (not broker!)
+ this.client.stateChange(update);
console.log(`[iframe-sync] State updated: ${field} = ${value}`);
}
@@ -130,9 +144,10 @@ class OzwellStateSync {
// Public method to manually trigger state sync (useful after tool execution)
syncCurrentState() {
- if (!this.broker) return;
+ if (!this.client) return;
- this.broker.stateChange({
+ // Use CLIENT to update state (not broker!)
+ this.client.stateChange({
ticketForm: this.getCurrentFormState()
});
diff --git a/public/ozwell-mcp-tools.js b/public/ozwell-mcp-tools.js
index f490ae1..116b1fc 100644
--- a/public/ozwell-mcp-tools.js
+++ b/public/ozwell-mcp-tools.js
@@ -308,10 +308,15 @@ class OzwellMCPIntegration {
}
}
- const { type, toolCall } = event.data;
-
- if (type === 'CALL_TOOL') {
- // Widget is requesting to execute a tool
+ const { source, type, tool, payload } = event.data;
+
+ // Match Ozwell's convention: type='tool_call', tool=name, payload=args
+ if (source === 'ozwell-chat-widget' && type === 'tool_call') {
+ // Convert to expected format
+ const toolCall = {
+ name: tool,
+ arguments: payload
+ };
console.log('[MCP Tools] Tool call requested:', toolCall);
this.executeTool(toolCall);
}
@@ -346,14 +351,19 @@ class OzwellMCPIntegration {
}
sendToolResult(result) {
- if (!this.widgetIframe) {
- console.error('[MCP Tools] Cannot send tool result: widget iframe not found');
+ // Find widget iframe dynamically
+ const widgetIframe = document.querySelector('iframe[src*="widget.html"]');
+
+ if (!widgetIframe) {
+ console.warn('[MCP Tools] Widget iframe not found, cannot send result');
return;
}
- this.widgetIframe.contentWindow.postMessage(
+ // Send result back to widget (Ozwell convention)
+ widgetIframe.contentWindow.postMessage(
{
- type: 'TOOL_RESULT',
+ source: 'ozwell-chat-parent',
+ type: 'tool_result',
result: result
},
'*'
From 427d4900ab6d1b0f0c5cb203ada999849cf15714 Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Fri, 17 Oct 2025 11:37:20 -0400
Subject: [PATCH 31/57] feat: Enhance AI assistant with current ticket form
retrieval and update usage rules
---
client/main.html | 44 +++++++++++++++++++++++++++++++++++++-
public/ozwell-mcp-tools.js | 40 +++++++++++++++++++++++++++++++++-
2 files changed, 82 insertions(+), 2 deletions(-)
diff --git a/client/main.html b/client/main.html
index 5a5d72e..b971eb1 100644
--- a/client/main.html
+++ b/client/main.html
@@ -15,7 +15,49 @@
endpoint: 'http://localhost:3000/embed/chat',
title: 'TimeHarbor Assistant',
placeholder: 'Ask about your tickets...',
- model: 'qwen2.5:14b'
+ model: 'qwen2.5:14b',
+ systemPrompt: `You are a helpful assistant for TimeHarbor ticket tracking.
+
+**IMPORTANT: Always respond in English. Never use Thai or any other language.**
+
+CRITICAL TOOL USAGE RULES - READ CAREFULLY:
+
+1. WHEN TO USE TOOLS:
+ ✅ User says: "update", "change", "set", "modify", "fill in", "add to", "use this"
+ ✅ User confirms: "yes", "ok do it", "proceed", "apply it"
+ ✅ User checks: "what is the current...", "show me what's filled"
+
+2. WHEN NOT TO USE TOOLS:
+ ❌ User asks: "suggest", "give examples", "what are some", "can you recommend", "show me options"
+ ❌ User explores: "what should I put", "any ideas for", "help me think of"
+ ❌ NEVER call tools just because you're discussing a topic - wait for explicit action request
+
+3. CONTEXT RESET RULE:
+ After providing suggestions/examples, DO NOT assume the next message wants to use them.
+ Each message is independent - check if it contains an action verb.
+
+4. READ BEFORE UPDATE:
+ Use get_current_ticket_form when user asks "what is...", "check...", "show current..."
+
+EXAMPLES:
+
+Conversation flow that works correctly:
+User: "suggest some titles"
+You: [text list, NO tools]
+User: "give examples for description"
+You: [text list, NO tools] ← RESET context, don't assume we're still updating
+User: "ok use option 3"
+You: [call update tool] ✓
+
+User: "what time is set?"
+You: [call get_current_ticket_form] ✓
+
+Common mistakes to AVOID:
+❌ User: "examples?" → You call update tool (WRONG)
+❌ Conversation is about tickets → You call tools without action word (WRONG)
+❌ You just gave suggestions → Next message auto-updates (WRONG - reset context!)
+
+Remember: Suggestions = TEXT only. Updates = TOOLS only. Check current values = get_current_ticket_form.`
};
diff --git a/public/ozwell-mcp-tools.js b/public/ozwell-mcp-tools.js
index 116b1fc..601fd4a 100644
--- a/public/ozwell-mcp-tools.js
+++ b/public/ozwell-mcp-tools.js
@@ -5,6 +5,18 @@
// MCP Tools Definitions (OpenAI function calling format)
const mcpTools = [
+ {
+ type: 'function',
+ function: {
+ name: 'get_current_ticket_form',
+ description: 'Retrieves the current values from all ticket form fields (title, description, hours, minutes, seconds, team). Use this when the user asks what is currently filled in or to check current values.',
+ parameters: {
+ type: 'object',
+ properties: {},
+ required: []
+ }
+ }
+ },
{
type: 'function',
function: {
@@ -110,13 +122,39 @@ const mcpTools = [
// Add tools to OzwellChatConfig (widget reads from here)
if (window.OzwellChatConfig) {
window.OzwellChatConfig.tools = mcpTools;
- console.log('[MCP Tools] Added', mcpTools.length, 'tools to OzwellChatConfig');
+ console.log('[MCP Tools] Added', mcpTools.length, 'tools to OzwellChatConfig:', mcpTools.map(t => t.function.name).join(', '));
} else {
console.error('[MCP Tools] window.OzwellChatConfig not found! Tools will not be available.');
}
// Tool Handler Functions
const toolHandlers = {
+ get_current_ticket_form: async (params) => {
+ const titleInput = document.querySelector('[name="title"]');
+ const descInput = document.querySelector('[name="github"]');
+ const hoursInput = document.querySelector('[name="hours"]');
+ const minutesInput = document.querySelector('[name="minutes"]');
+ const secondsInput = document.querySelector('[name="seconds"]');
+ const teamSelect = document.querySelector('[name="team"]');
+
+ const result = {
+ success: true,
+ data: {
+ title: titleInput?.value || '',
+ description: descInput?.value || '',
+ hours: hoursInput?.value || '0',
+ minutes: minutesInput?.value || '0',
+ seconds: secondsInput?.value || '0',
+ team: teamSelect?.value || '',
+ teamName: teamSelect?.selectedOptions[0]?.text || 'No team selected'
+ },
+ message: 'Retrieved current ticket form values'
+ };
+
+ console.log('[MCP Tools] get_current_ticket_form result:', result);
+ return result;
+ },
+
update_ticket_title: async (params) => {
const titleInput = document.querySelector('[name="title"]');
if (!titleInput) {
From cb75b26a6ebc84a6ca4fc51203ef743806198cfb Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Mon, 20 Oct 2025 18:57:29 -0400
Subject: [PATCH 32/57] feat: Add project time statistics and recent ticket
retrieval functions for MCP tool
---
client/main.html | 70 ++++++++++++++++++++++++--------------
public/ozwell-mcp-tools.js | 65 +++++++++++++++++++++++++++++++----
server/methods/ozwell.js | 69 +++++++++++++++++++++++++++++++++++++
3 files changed, 172 insertions(+), 32 deletions(-)
diff --git a/client/main.html b/client/main.html
index b971eb1..b5d6c63 100644
--- a/client/main.html
+++ b/client/main.html
@@ -22,42 +22,60 @@
CRITICAL TOOL USAGE RULES - READ CAREFULLY:
-1. WHEN TO USE TOOLS:
- ✅ User says: "update", "change", "set", "modify", "fill in", "add to", "use this"
- ✅ User confirms: "yes", "ok do it", "proceed", "apply it"
- ✅ User checks: "what is the current...", "show me what's filled"
-
-2. WHEN NOT TO USE TOOLS:
- ❌ User asks: "suggest", "give examples", "what are some", "can you recommend", "show me options"
- ❌ User explores: "what should I put", "any ideas for", "help me think of"
- ❌ NEVER call tools just because you're discussing a topic - wait for explicit action request
-
-3. CONTEXT RESET RULE:
- After providing suggestions/examples, DO NOT assume the next message wants to use them.
- Each message is independent - check if it contains an action verb.
-
-4. READ BEFORE UPDATE:
- Use get_current_ticket_form when user asks "what is...", "check...", "show current..."
+You have TWO types of tools:
+
+A) READ TOOLS (for getting information):
+ - get_project_history: Gets recent tickets from this project
+ - get_current_ticket_form: Gets current form field values
+ - get_project_time_stats: Gets time statistics (total time, ticket count)
+ - get_conversation_history: Gets past chat conversations
+
+B) UPDATE TOOLS (for modifying data):
+ - update_ticket_title: Updates the title field
+ - update_ticket_description: Updates the description field
+ - update_ticket_time: Updates time fields
+
+WHEN TO USE READ TOOLS:
+✅ User asks: "suggest", "give examples", "what are some", "recommend", "show options"
+✅ User checks: "what is the current...", "show me what's filled", "check..."
+✅ User asks about time: "how much time", "total time", "time spent", "hours logged"
+✅ ALWAYS call get_project_history FIRST when providing suggestions - this gives context about their past work
+✅ Call get_current_ticket_form when they ask about current values
+✅ Call get_project_time_stats when they ask about time tracking or hours spent
+
+WHEN TO USE UPDATE TOOLS:
+✅ User says: "update", "change", "set", "modify", "fill in", "add to", "use this"
+✅ User confirms: "yes", "ok do it", "proceed", "apply it"
+❌ NEVER use UPDATE tools for suggestions or questions
+
+CONTEXT RESET RULE:
+After providing suggestions, DO NOT assume the next message wants to apply them.
+Each message is independent - check if it contains an action verb.
EXAMPLES:
-Conversation flow that works correctly:
+Correct flow:
User: "suggest some titles"
-You: [text list, NO tools]
-User: "give examples for description"
-You: [text list, NO tools] ← RESET context, don't assume we're still updating
-User: "ok use option 3"
-You: [call update tool] ✓
+You: [call get_project_history] → [provide context-aware suggestions based on history]
User: "what time is set?"
-You: [call get_current_ticket_form] ✓
+You: [call get_current_ticket_form] → [show current values]
+
+User: "how much time have I spent?"
+You: [call get_project_time_stats] → [show total time and ticket count]
+
+User: "ok use option 3"
+You: [call update_ticket_title] ✓
Common mistakes to AVOID:
-❌ User: "examples?" → You call update tool (WRONG)
-❌ Conversation is about tickets → You call tools without action word (WRONG)
+❌ User asks for suggestions → You give generic examples without calling get_project_history (WRONG)
+❌ User says "examples?" → You call update tool (WRONG - that's a READ operation)
❌ You just gave suggestions → Next message auto-updates (WRONG - reset context!)
-Remember: Suggestions = TEXT only. Updates = TOOLS only. Check current values = get_current_ticket_form.`
+Remember:
+- Suggestions/Questions = READ TOOLS first, then respond
+- Updates/Actions = UPDATE TOOLS
+- Always get context from get_project_history before suggesting titles/descriptions`
};
diff --git a/public/ozwell-mcp-tools.js b/public/ozwell-mcp-tools.js
index 601fd4a..9629745 100644
--- a/public/ozwell-mcp-tools.js
+++ b/public/ozwell-mcp-tools.js
@@ -99,6 +99,24 @@ const mcpTools = [
}
}
},
+ {
+ type: 'function',
+ function: {
+ name: 'get_project_time_stats',
+ description: 'Gets time statistics for the current project (total time spent, number of tickets). Use this when the user asks about time spent, hours logged, or time tracking.',
+ parameters: {
+ type: 'object',
+ properties: {
+ days: {
+ type: 'number',
+ description: 'Number of days to look back (default: 30)',
+ default: 30
+ }
+ },
+ required: []
+ }
+ }
+ },
{
type: 'function',
function: {
@@ -135,7 +153,7 @@ const toolHandlers = {
const hoursInput = document.querySelector('[name="hours"]');
const minutesInput = document.querySelector('[name="minutes"]');
const secondsInput = document.querySelector('[name="seconds"]');
- const teamSelect = document.querySelector('[name="team"]');
+ const teamSelect = document.querySelector('#teamSelect');
const result = {
success: true,
@@ -246,7 +264,7 @@ const toolHandlers = {
get_project_history: async (params) => {
// Get current team ID from the page
- const teamSelect = document.querySelector('[name="team"]');
+ const teamSelect = document.querySelector('#teamSelect');
if (!teamSelect || !teamSelect.value) {
return {
success: false,
@@ -255,13 +273,13 @@ const toolHandlers = {
}
const teamId = teamSelect.value;
- const days = params.days || 7;
+ const days = params.days || 30;
const limit = params.limit || 20;
try {
// Call Meteor method to get project history
const history = await new Promise((resolve, reject) => {
- Meteor.call('getRecentProjectTickets', teamId, days, limit, (error, result) => {
+ Meteor.call('getRecentProjectTickets', { teamId, days, limit }, (error, result) => {
if (error) reject(error);
else resolve(result);
});
@@ -269,7 +287,7 @@ const toolHandlers = {
return {
success: true,
- data: history,
+ tickets: history,
message: `Retrieved ${history.length} recent tickets from the last ${days} days`
};
} catch (error) {
@@ -280,9 +298,44 @@ const toolHandlers = {
}
},
+ get_project_time_stats: async (params) => {
+ // Get current team ID from the page
+ const teamSelect = document.querySelector('#teamSelect');
+ if (!teamSelect || !teamSelect.value) {
+ return {
+ success: false,
+ error: 'No team selected. Please select a team first.'
+ };
+ }
+
+ const teamId = teamSelect.value;
+ const days = params.days || 30;
+
+ try {
+ // Call Meteor method to get time statistics
+ const stats = await new Promise((resolve, reject) => {
+ Meteor.call('getProjectTimeStats', { teamId, days }, (error, result) => {
+ if (error) reject(error);
+ else resolve(result);
+ });
+ });
+
+ return {
+ success: true,
+ ...stats,
+ message: `Retrieved time stats: ${stats.totalFormatted} across ${stats.ticketCount} tickets in the last ${stats.days} days`
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to retrieve time statistics: ${error.message}`
+ };
+ }
+ },
+
get_conversation_history: async (params) => {
// Get current team ID from the page
- const teamSelect = document.querySelector('[name="team"]');
+ const teamSelect = document.querySelector('#teamSelect');
if (!teamSelect || !teamSelect.value) {
return {
success: false,
diff --git a/server/methods/ozwell.js b/server/methods/ozwell.js
index 75dd8ed..26a5979 100644
--- a/server/methods/ozwell.js
+++ b/server/methods/ozwell.js
@@ -455,5 +455,74 @@ export const ozwellMethods = {
if (!team) throw new Meteor.Error('not-authorized', 'Not a team member');
return conversation;
+ },
+
+ // Get recent project tickets for MCP tool (context-aware suggestions)
+ async getRecentProjectTickets({ teamId, days = 30, limit = 20 }) {
+ check(teamId, String);
+ check(days, Number);
+ check(limit, Number);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // Verify team membership
+ const team = await Teams.findOneAsync({ _id: teamId, members: this.userId });
+ if (!team) throw new Meteor.Error('not-authorized', 'Not a team member');
+
+ // Calculate cutoff date
+ const cutoffDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
+
+ // Query tickets created within the time window
+ const tickets = await Tickets.find({
+ teamId,
+ createdAt: { $gte: cutoffDate }
+ }, {
+ sort: { createdAt: -1 },
+ limit
+ }).fetchAsync();
+
+ // Return formatted data for AI context
+ return tickets.map(t => ({
+ title: t.title,
+ description: t.github || '',
+ time: t.accumulatedTime || 0,
+ createdAt: t.createdAt
+ }));
+ },
+
+ // Get project time statistics for MCP tool (time tracking queries)
+ async getProjectTimeStats({ teamId, days = 30 }) {
+ check(teamId, String);
+ check(days, Number);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // Verify team membership
+ const team = await Teams.findOneAsync({ _id: teamId, members: this.userId });
+ if (!team) throw new Meteor.Error('not-authorized', 'Not a team member');
+
+ // Calculate cutoff date
+ const cutoffDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
+
+ // Query tickets created within the time window
+ const tickets = await Tickets.find({
+ teamId,
+ createdAt: { $gte: cutoffDate }
+ }).fetchAsync();
+
+ // Calculate total time in seconds
+ const totalSeconds = tickets.reduce((sum, ticket) => {
+ return sum + (ticket.accumulatedTime || 0);
+ }, 0);
+
+ // Format as "Xh Ym"
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const totalFormatted = `${hours}h ${minutes}m`;
+
+ return {
+ totalSeconds,
+ totalFormatted,
+ ticketCount: tickets.length,
+ days
+ };
}
};
From 3c44e8995f25b7a27f3e11267217f7b5d9c2ee2d Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Tue, 21 Oct 2025 12:23:11 -0400
Subject: [PATCH 33/57] feat: Improve drag functionality and performance for
Ozwell Chat Widget
---
public/chat-wrapper.css | 9 ++++++++-
public/chat-wrapper.js | 45 +++++++++++++++++++++++++++++++----------
2 files changed, 42 insertions(+), 12 deletions(-)
diff --git a/public/chat-wrapper.css b/public/chat-wrapper.css
index 2752e84..06f973f 100644
--- a/public/chat-wrapper.css
+++ b/public/chat-wrapper.css
@@ -56,8 +56,15 @@
}
#ozwell-chat-container.dragging {
- opacity: 0.8;
+ opacity: 0.9;
cursor: move;
+ user-select: none;
+ will-change: left, top;
+ pointer-events: auto;
+}
+
+#ozwell-chat-container.dragging * {
+ pointer-events: none;
}
/* Chat Header */
diff --git a/public/chat-wrapper.js b/public/chat-wrapper.js
index a25c083..1b41946 100644
--- a/public/chat-wrapper.js
+++ b/public/chat-wrapper.js
@@ -9,6 +9,8 @@ class ChatWrapper {
this.isMinimized = false;
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
+ this.currentPosition = { x: 0, y: 0 };
+ this.rafId = null;
this.init();
}
@@ -163,9 +165,17 @@ class ChatWrapper {
this.isDragging = true;
this.container.classList.add('dragging');
+ // Cache container dimensions (they don't change during drag)
+ this.containerWidth = this.container.offsetWidth;
+ this.containerHeight = this.container.offsetHeight;
+
const rect = this.container.getBoundingClientRect();
this.dragOffset.x = e.clientX - rect.left;
this.dragOffset.y = e.clientY - rect.top;
+
+ // Store initial position
+ this.currentPosition.x = rect.left;
+ this.currentPosition.y = rect.top;
}
drag(e) {
@@ -173,26 +183,39 @@ class ChatWrapper {
e.preventDefault();
- let newX = e.clientX - this.dragOffset.x;
- let newY = e.clientY - this.dragOffset.y;
+ // Just store the mouse position, don't update DOM yet
+ const newX = e.clientX - this.dragOffset.x;
+ const newY = e.clientY - this.dragOffset.y;
- // Keep within viewport bounds
- const maxX = window.innerWidth - this.container.offsetWidth;
- const maxY = window.innerHeight - this.container.offsetHeight;
+ // Keep within viewport bounds (use cached dimensions)
+ const maxX = window.innerWidth - this.containerWidth;
+ const maxY = window.innerHeight - this.containerHeight;
- newX = Math.max(0, Math.min(newX, maxX));
- newY = Math.max(0, Math.min(newY, maxY));
+ this.currentPosition.x = Math.max(0, Math.min(newX, maxX));
+ this.currentPosition.y = Math.max(0, Math.min(newY, maxY));
- this.container.style.left = newX + 'px';
- this.container.style.top = newY + 'px';
- this.container.style.right = 'auto';
- this.container.style.bottom = 'auto';
+ // Request animation frame for smooth updates
+ if (!this.rafId) {
+ this.rafId = requestAnimationFrame(() => this.updatePosition());
+ }
+ }
+
+ updatePosition() {
+ // Update DOM only once per frame using transform (GPU accelerated)
+ this.container.style.transform = `translate(${this.currentPosition.x}px, ${this.currentPosition.y}px)`;
+ this.rafId = null;
}
stopDrag() {
if (this.isDragging) {
this.isDragging = false;
this.container.classList.remove('dragging');
+
+ // Cancel any pending animation frame
+ if (this.rafId) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
}
}
From 23faf0b9782597b5fe9d51816264f2d83d71495f Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Tue, 21 Oct 2025 13:50:25 -0400
Subject: [PATCH 34/57] feat: Refine drag behavior and transition effects for
Ozwell Chat Widget
---
public/chat-wrapper.css | 6 ++++--
public/chat-wrapper.js | 8 +++++++-
2 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/public/chat-wrapper.css b/public/chat-wrapper.css
index 06f973f..3a69664 100644
--- a/public/chat-wrapper.css
+++ b/public/chat-wrapper.css
@@ -43,7 +43,8 @@
flex-direction: column;
z-index: 9999;
overflow: hidden;
- transition: all 0.3s ease;
+ transition: opacity 0.3s ease, box-shadow 0.3s ease;
+ transform: translate(0, 0);
}
#ozwell-chat-container.open {
@@ -59,8 +60,9 @@
opacity: 0.9;
cursor: move;
user-select: none;
- will-change: left, top;
+ will-change: transform;
pointer-events: auto;
+ transition: none;
}
#ozwell-chat-container.dragging * {
diff --git a/public/chat-wrapper.js b/public/chat-wrapper.js
index 1b41946..6baf0a9 100644
--- a/public/chat-wrapper.js
+++ b/public/chat-wrapper.js
@@ -173,9 +173,15 @@ class ChatWrapper {
this.dragOffset.x = e.clientX - rect.left;
this.dragOffset.y = e.clientY - rect.top;
- // Store initial position
+ // Store current absolute position (where it actually is on screen)
this.currentPosition.x = rect.left;
this.currentPosition.y = rect.top;
+
+ // Remove bottom/right positioning to prevent conflicts with transform
+ this.container.style.bottom = 'auto';
+ this.container.style.right = 'auto';
+ this.container.style.left = '0';
+ this.container.style.top = '0';
}
drag(e) {
From e00aa5254a8ebb04adf480fed5821311d3112936 Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Tue, 21 Oct 2025 15:59:54 -0400
Subject: [PATCH 35/57] Refactor Ozwell integration: remove OzwellModal and
related components, clean up server methods, and update TicketsPage to remove
OzwellButton, switching everything to the chatbot
---
client/components/layout/MainLayout.html | 3 -
client/components/ozwell/OzwellButton.html | 9 -
client/components/ozwell/OzwellButton.js | 108 ---
client/components/ozwell/OzwellModal.html | 195 -----
client/components/ozwell/OzwellModal.js | 905 ---------------------
client/components/tickets/TicketsPage.html | 2 -
client/main.js | 8 -
server/methods/ozwell.js | 65 --
server/methods/ozwellPrompts.js | 183 -----
server/methods/referenceAssistant.js | 60 --
10 files changed, 1538 deletions(-)
delete mode 100644 client/components/ozwell/OzwellButton.html
delete mode 100644 client/components/ozwell/OzwellButton.js
delete mode 100644 client/components/ozwell/OzwellModal.html
delete mode 100644 client/components/ozwell/OzwellModal.js
delete mode 100644 server/methods/ozwellPrompts.js
delete mode 100644 server/methods/referenceAssistant.js
diff --git a/client/components/layout/MainLayout.html b/client/components/layout/MainLayout.html
index 854570d..78a2c75 100644
--- a/client/components/layout/MainLayout.html
+++ b/client/components/layout/MainLayout.html
@@ -35,7 +35,4 @@
TimeHarbor
-
-
- {{> ozwellModal}}
\ No newline at end of file
diff --git a/client/components/ozwell/OzwellButton.html b/client/components/ozwell/OzwellButton.html
deleted file mode 100644
index 972e83d..0000000
--- a/client/components/ozwell/OzwellButton.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/client/components/ozwell/OzwellButton.js b/client/components/ozwell/OzwellButton.js
deleted file mode 100644
index d951e4a..0000000
--- a/client/components/ozwell/OzwellButton.js
+++ /dev/null
@@ -1,108 +0,0 @@
-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 immediateWrapper = button.closest('.relative, .input-group, .form-control');
- let inputElement = null;
-
- 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"]');
- }
-
- // 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;
- }
-
- const form = button.closest('form');
-
- // 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 || '';
- 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);
-
- // Open Ozwell modal
- if (window.openOzwell) {
- window.openOzwell(inputElement, context);
- } else {
- console.error('Ozwell not available');
- }
- }
-});
diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html
deleted file mode 100644
index 83ba02b..0000000
--- a/client/components/ozwell/OzwellModal.html
+++ /dev/null
@@ -1,195 +0,0 @@
-
-
-
-
-
-
Ozwell AI Assistant
-
{{headerSubtitle}}
-
-
-
-
-
-
-
-
- {{#if recentsEnabled}}
- {{#unless useMcpMode}}
- {{#if hasRecentConversations}}
-
- Recent chats
-
-
-
- {{#each recentConversations}}
-
- {{/each}}
-
- {{else}}
-
-
-
- {{/if}}
- {{/unless}}
- {{/if}}
-
- {{#unless useMcpMode}}
- {{#if selectedPrompt}}
-
-
-
Project Context
- {{#if summaryVisible}}
-
- {{contextSummary}}
-
- {{/if}}
-
-
-
-
-
-
- {{#each messages}}
-
- {{/each}}
-
- {{#if showEmptyState}}
-
-
-
-
Preparing your first draft…
-
-
- {{/if}}
-
- {{#if isGenerating}}
-
-
-
- Generating response…
-
-
- {{/if}}
-
-
- {{#if suggestionsAvailable}}
-
-
Pick a suggestion to insert
-
- {{#each suggestions}}
-
- {{/each}}
-
-
- {{/if}}
-
-
- {{#if errorMessage}}
-
{{errorMessage}}
- {{/if}}
-
-
-
-
-
-
-
-
- {{else}}
-
-
-
How can Ozwell help?
-
Pick a starter prompt to kick off the conversation.
-
-
- {{#each availablePrompts}}
-
- {{/each}}
-
-
-
-
-
- {{/if}}
- {{else}}
-
- {{/unless}}
-
- {{#unless layoutIsSidecar}}
-
- {{/unless}}
-
-
diff --git a/client/components/ozwell/OzwellModal.js b/client/components/ozwell/OzwellModal.js
deleted file mode 100644
index a1bc888..0000000
--- a/client/components/ozwell/OzwellModal.js
+++ /dev/null
@@ -1,905 +0,0 @@
-import { Template } from 'meteor/templating';
-import { ReactiveVar } from 'meteor/reactive-var';
-import { Meteor } from 'meteor/meteor';
-import { Tracker } from 'meteor/tracker';
-
-import './OzwellModal.html';
-
-const DEFAULT_SYSTEM_MESSAGE = 'You are a helpful assistant for time tracking and project management.';
-const ENABLE_RECENT_CHATS = false;
-const EMBED_SCRIPT_URL = 'https://ozwellai-reference-server.opensource.mieweb.org/embed/embed.js';
-const EMBED_ENDPOINT = 'https://ozwellai-reference-server.opensource.mieweb.org/embed/chat';
-const EMBED_WIDGET_SRC = 'https://ozwellai-reference-server.opensource.mieweb.org/embed/widget.html';
-
-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 () {
- const template = this;
-
- 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.');
- template.suggestions = new ReactiveVar([]);
- template.selectedSuggestionIndex = new ReactiveVar(0);
- template.layoutMode = new ReactiveVar('modal'); // modal | sidecar
- template.summaryVisible = new ReactiveVar(false);
- template.recentConversations = new ReactiveVar([]);
- template.currentConversationId = new ReactiveVar(null);
- template.conversationLabel = new ReactiveVar('');
- template.currentFieldName = new ReactiveVar('general');
- template.useMcpMode = new ReactiveVar(false); // NOTE(uid_future-mcp): MCP toggle currently drives a stub transport; see comments below.
-
- // 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');
- };
-
- const buildContextSummary = (context = {}) => {
- const summary = [];
-
- if (context.teamName) {
- summary.push(`Project: ${context.teamName}`);
- }
-
- if (context.user?.username) {
- summary.push(`User: ${context.user.username}`);
- }
-
- if (context.currentTicket?.title) {
- summary.push(`Current Activity: ${context.currentTicket.title}`);
- if (context.currentTicket.description) {
- summary.push(`Details: ${context.currentTicket.description}`);
- }
- }
-
- if (context.projectStats?.formattedProjectTime) {
- summary.push(`Total time on project: ${context.projectStats.formattedProjectTime}`);
- }
-
- if (context.projectStats?.formattedTimeToday) {
- summary.push(`Time spent today: ${context.projectStats.formattedTimeToday}`);
- }
-
- 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})`);
- }
- if (item.description) {
- pieces.push(` – ${item.description}`);
- }
- summary.push(pieces.join(''));
- });
- }
-
- if (context.currentText) {
- 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.';
- };
-
- const addMessage = (message) => {
- const history = template.messages.get();
- template.messages.set([...history, { ...message, createdAt: new Date() }]);
- };
-
- const stripQuotes = (value = '') => value.replace(/^["'“”‘’\s]+|["'“”‘’\s]+$/g, '').trim();
-
- const extractSuggestions = (text = '') => {
- if (!text) return [];
-
- 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;
- }
-
- 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 [];
- };
-
- const buildServerMessages = () => {
- const messages = [];
- const baseSystemMessage = template.systemMessage.get();
- const summary = template.contextSummary.get();
-
- 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) {
- messages.push({ role: 'system', content: `Project context:\n${summary}` });
- }
-
- template.messages.get().forEach((msg) => {
- if (msg.role === 'user' || msg.role === 'assistant') {
- messages.push({ role: msg.role, content: msg.content });
- }
- });
-
- return messages;
- };
-
- const buildMessagesForPrompt = (promptText) => {
- const contextSummary = buildContextSummary(template.currentContext.get() || {});
- const system = template.systemMessage.get() || DEFAULT_SYSTEM_MESSAGE;
- const msgs = [];
- msgs.push({
- role: 'system',
- content: `${system}\nInstructions: Provide only polished, ready-to-paste suggestions. Avoid Markdown, template placeholders, or meta commentary.`
- });
- if (contextSummary) {
- msgs.push({ role: 'system', content: `Project context:\n${contextSummary}` });
- }
- msgs.push({ role: 'user', content: promptText });
- return msgs;
- };
-
- const callReferenceAssistant = (payload) => new Promise((resolve, reject) => {
- Meteor.call('callReferenceAssistant', payload, (error, result) => {
- if (error) {
- reject(error);
- } else {
- resolve(result);
- }
- });
- });
-
- 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);
- template.suggestions.set([]);
- template.selectedSuggestionIndex.set(0);
- template.summaryVisible.set(false);
- if (ENABLE_RECENT_CHATS) {
- template.currentConversationId.set(null);
- template.conversationLabel.set('');
- }
- template.useMcpMode.set(false);
- };
-
- template.performAutofill = function ({ closeModal = true } = {}) {
- const content = template.generatedContent.get();
- const inputElement = template.currentInputElement.get();
-
- if (!content) {
- alert('No assistant content is available yet. Generate a suggestion first.');
- return;
- }
-
- if (!inputElement) {
- alert('Unable to find the original input field to update.');
- return;
- }
-
- if (inputElement.tagName === 'TEXTAREA' || inputElement.tagName === 'INPUT') {
- inputElement.value = content;
- } else if (inputElement.contentEditable === 'true') {
- inputElement.textContent = content;
- }
-
- const inputEvent = new Event('input', { bubbles: true });
- inputElement.dispatchEvent(inputEvent);
- const changeEvent = new Event('change', { bubbles: true });
- inputElement.dispatchEvent(changeEvent);
- inputElement.focus();
-
- if (closeModal) {
- template.closeModal();
- }
- };
-
- 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.');
- template.layoutMode.set('modal');
- template.unmountEmbeddedChat();
- };
-
- template.loadPrompts = function () {
- Meteor.call('getOzwellPrompts', (err, prompts) => {
- if (!err && prompts) {
- template.availablePrompts.set(prompts);
- } else {
- template.availablePrompts.set(FALLBACK_PROMPTS);
- }
- });
- };
-
- template.openOzwell = function (inputElement, context = {}) {
- const user = Meteor.user();
- if (!user?.profile?.ozwellEnabled) {
- alert('Please configure Ozwell in your settings first.');
- return;
- }
-
- template.selectedPrompt.set(null);
- template.resetConversation();
- template.currentInputElement.set(inputElement);
- template.currentContext.set(context);
- template.currentTeamId.set(context.teamId || null);
- template.currentFieldName.set(context.fieldName || 'general');
- template.headerSubtitle.set(context.teamName ? `Project: ${context.teamName}` : 'Ready to help with your work.');
- template.isOzwellOpen.set(true);
- template.layoutMode.set('modal');
- if (ENABLE_RECENT_CHATS) {
- template.loadRecentConversations();
- }
- };
-
- 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;
-
- 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);
- if (ENABLE_RECENT_CHATS) {
- template.currentConversationId.set(null);
- const promptTitle = prompt?.title || '';
- const userPreview = userMessage.replace(/\s+/g, ' ').trim().substring(0, 60);
- let conversationLabel = userPreview || promptTitle || 'Conversation';
-
- if (promptTitle && userPreview && prompt?.id !== 'custom') {
- conversationLabel = `${promptTitle} — ${userPreview}`.substring(0, 120);
- }
-
- template.conversationLabel.set(conversationLabel);
- }
-
- if (prompt?.title) {
- template.headerSubtitle.set(prompt.title);
- }
-
- await template.sendChatMessage(userMessage);
- };
-
- template.sendChatMessage = async function (content) {
- const trimmed = (content || '').trim();
- if (!trimmed) return;
- if (template.isGenerating.get()) return;
-
- addMessage({ role: 'user', content: trimmed });
- template.composerText.set('');
- template.isGenerating.set(true);
- template.canSave.set(false);
- template.errorMessage.set(null);
- template.suggestions.set([]);
- template.selectedSuggestionIndex.set(0);
- template.generatedContent.set(null);
- template.summaryVisible.set(false);
- if (ENABLE_RECENT_CHATS && !template.conversationLabel.get()) {
- template.conversationLabel.set(trimmed.substring(0, 60));
- }
-
- 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 });
- 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);
- if (ENABLE_RECENT_CHATS) {
- template.persistConversation();
- }
- } 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.loadRecentConversations = function () {
- if (!ENABLE_RECENT_CHATS) return;
- const teamId = template.currentTeamId.get();
- const fieldName = template.currentFieldName.get();
- if (!teamId) {
- template.recentConversations.set([]);
- return;
- }
-
- Meteor.call('getOzwellConversations', { teamId, fieldName, limit: 5 }, (err, conversations) => {
- if (err) {
- console.error('Failed to load Ozwell conversations:', err);
- template.recentConversations.set([]);
- } else {
- template.recentConversations.set(conversations || []);
- }
- });
- };
-
- template.resumeConversation = function (conversationId) {
- if (!ENABLE_RECENT_CHATS) return;
- Meteor.call('getOzwellConversation', conversationId, (err, conversation) => {
- if (err || !conversation) {
- console.error('Failed to load conversation:', err);
- return;
- }
-
- template.systemMessage.set(conversation.metadata?.systemMessage || DEFAULT_SYSTEM_MESSAGE);
- template.messages.set(conversation.messages || []);
- template.generatedContent.set(null);
- template.suggestions.set([]);
- template.selectedSuggestionIndex.set(0);
- template.contextSummary.set(buildContextSummary(template.currentContext.get() || {}));
- template.canSave.set(conversation.messages?.some(msg => msg.role === 'assistant') || false);
- template.currentConversationId.set(conversation._id);
- template.conversationLabel.set(conversation.label || conversation.metadata?.promptTitle || 'Conversation');
- template.summaryVisible.set(false);
-
- const promptMeta = conversation.metadata?.promptTitle ? {
- id: conversation.metadata?.promptId || 'existing',
- title: conversation.metadata?.promptTitle,
- systemMessage: conversation.metadata?.systemMessage || DEFAULT_SYSTEM_MESSAGE
- } : template.selectedPrompt.get();
-
- template.selectedPrompt.set(promptMeta || {
- id: 'existing',
- title: conversation.label || 'Previous Conversation',
- systemMessage: conversation.metadata?.systemMessage || DEFAULT_SYSTEM_MESSAGE
- });
- });
- };
-
- template.persistConversation = function () {
- if (!ENABLE_RECENT_CHATS) return;
- const teamId = template.currentTeamId.get();
- const fieldName = template.currentFieldName.get();
- if (!teamId || !fieldName) return;
-
- const messages = template.messages.get();
- if (!messages || messages.length === 0) return;
-
- let label = template.conversationLabel.get();
- if (!label) {
- const firstUserMessage = messages.find(msg => msg.role === 'user');
- if (firstUserMessage?.content) {
- label = firstUserMessage.content.substring(0, 120);
- template.conversationLabel.set(label);
- } else {
- label = 'Conversation';
- }
- }
-
- const payload = {
- conversationId: template.currentConversationId.get(),
- teamId,
- fieldName,
- messages,
- metadata: {
- promptId: template.selectedPrompt.get()?.id,
- promptTitle: template.selectedPrompt.get()?.title,
- systemMessage: template.systemMessage.get()
- },
- label
- };
-
- if (!payload.conversationId) {
- delete payload.conversationId;
- }
-
- Meteor.call('saveOzwellConversation', payload, (err, savedId) => {
- if (err) {
- console.error('Failed to save conversation:', err);
- return;
- }
-
- if (savedId) {
- template.currentConversationId.set(savedId);
- template.loadRecentConversations();
- }
- });
- };
-
- template.getEmbedConfig = function () {
- return {
- containerId: 'ozwell-embed-container',
- title: 'Ozwell Assistant',
- placeholder: 'Ask me anything...',
- model: 'llama3',
- endpoint: EMBED_ENDPOINT,
- widgetUrl: EMBED_WIDGET_SRC,
- };
- };
-
- template.ensureEmbedScript = function () {
- if (document.querySelector('script[data-ozwell-embed]')) {
- console.log('[Ozwell] Embed script already loaded');
- return Promise.resolve();
- }
-
- return new Promise((resolve) => {
- window.OzwellChatConfig = template.getEmbedConfig();
- console.log('[Ozwell] Loading embed script from:', EMBED_SCRIPT_URL);
- console.log('[Ozwell] Embed config:', window.OzwellChatConfig);
- const script = document.createElement('script');
- script.src = EMBED_SCRIPT_URL;
- script.dataset.ozwellEmbed = 'true';
- script.addEventListener('load', () => {
- console.log('[Ozwell] Embed script loaded successfully');
- resolve();
- });
- script.addEventListener('error', (e) => {
- console.error('[Ozwell] Failed to load embed script:', e);
- resolve();
- });
- document.body.appendChild(script);
- });
- };
-
- template.handleEmbedInsert = function (event) {
- if (!template.useMcpMode.get()) return;
- const detail = event.detail || {};
- const content = typeof detail.text === 'string' ? detail.text : '';
- if (!content) return;
-
- template.generatedContent.set(content);
- template.canSave.set(true);
- template.performAutofill({ closeModal: detail.close !== false });
- };
-
- template.handleEmbedClosed = function () {
- if (!template.useMcpMode.get()) return;
- template.closeModal();
- };
-
- template.mountEmbeddedChat = function () {
- const container = document.getElementById('ozwell-embed-container');
- if (!container) {
- console.error('[Ozwell] Container not found');
- return;
- }
-
- console.log('[Ozwell] Mounting embedded chat');
-
- if (!template.embedInsertListener) {
- template.embedInsertListener = template.handleEmbedInsert.bind(template);
- document.addEventListener('ozwell-chat-insert', template.embedInsertListener);
- }
-
- if (!template.embedClosedListener) {
- template.embedClosedListener = template.handleEmbedClosed.bind(template);
- document.addEventListener('ozwell-chat-closed', template.embedClosedListener);
- }
-
- const config = template.getEmbedConfig();
- window.OzwellChatConfig = config;
-
- template.ensureEmbedScript().then(() => {
- if (!window.OzwellChat) {
- console.error('[Ozwell] window.OzwellChat not available after script load');
- return;
- }
-
- // Clear any existing iframe to force fresh mount with new config
- if (window.OzwellChat.iframe) {
- console.log('[Ozwell] Removing existing iframe to force fresh mount');
- window.OzwellChat.iframe.remove();
- window.OzwellChat.iframe = null;
- }
-
- console.log('[Ozwell] Mounting new iframe with config:', config);
- window.OzwellChat.configure(config);
- window.OzwellChat.mount(config);
-
- // Move iframe to our container
- if (window.OzwellChat.iframe && window.OzwellChat.iframe.parentElement !== container) {
- container.appendChild(window.OzwellChat.iframe);
- }
- });
- };
-
- template.unmountEmbeddedChat = function () {
- if (template.embedInsertListener) {
- document.removeEventListener('ozwell-chat-insert', template.embedInsertListener);
- template.embedInsertListener = null;
- }
-
- if (template.embedClosedListener) {
- document.removeEventListener('ozwell-chat-closed', template.embedClosedListener);
- template.embedClosedListener = null;
- }
-
- const container = document.getElementById('ozwell-embed-container');
- if (container) {
- container.innerHTML = '';
- }
-
- if (window.OzwellChat?.iframe) {
- window.OzwellChat.iframe.remove();
- }
- };
-
- template.autorun(() => {
- const useEmbed = template.useMcpMode.get();
- if (useEmbed) {
- Tracker.afterFlush(() => template.mountEmbeddedChat());
- } else {
- template.unmountEmbeddedChat();
- }
- });
-
- template.loadPrompts();
-});
-
-Template.ozwellModal.helpers({
- isOzwellOpen() {
- return Template.instance().isOzwellOpen.get();
- },
- selectedPrompt() {
- return Template.instance().selectedPrompt.get();
- },
- availablePrompts() {
- return Template.instance().availablePrompts.get();
- },
- messages() {
- return Template.instance().messages.get();
- },
- layoutIsSidecar() {
- return Template.instance().layoutMode.get() === 'sidecar';
- },
- summaryVisible() {
- return Template.instance().summaryVisible.get();
- },
- useMcpMode() {
- return Template.instance().useMcpMode.get();
- },
- recentConversations() {
- return ENABLE_RECENT_CHATS ? Template.instance().recentConversations.get() : [];
- },
- hasRecentConversations() {
- return ENABLE_RECENT_CHATS && Template.instance().recentConversations.get().length > 0;
- },
- recentsEnabled() {
- return ENABLE_RECENT_CHATS;
- },
- 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();
- },
- suggestions() {
- const instance = Template.instance();
- return instance.suggestions.get().map((text, index) => ({ text, index }));
- },
- suggestionsAvailable() {
- 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';
- },
- 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;
- },
- checked(index) {
- return Template.instance().selectedSuggestionIndex.get() === index ? 'checked' : '';
- },
- isCurrentConversation(conversationId) {
- return Template.instance().currentConversationId.get() === conversationId;
- },
- formatConversationTimestamp(timestamp, label) {
- if (!timestamp) return label || '';
- const date = new Date(timestamp);
- const formatted = date.toLocaleString();
- if (!label) {
- return formatted;
- }
- return `${formatted}\n${label}`;
- }
-});
-
-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-toggle-mode'(event, template) {
- event.preventDefault();
- const next = !template.useMcpMode.get();
- template.useMcpMode.set(next);
- },
- 'click #toggle-context'(event, template) {
- event.preventDefault();
- template.summaryVisible.set(!template.summaryVisible.get());
- },
- 'click .resume-conversation'(event, template) {
- if (!ENABLE_RECENT_CHATS) return;
- event.preventDefault();
- const conversationId = event.currentTarget.getAttribute('data-id');
- if (conversationId) {
- template.resumeConversation(conversationId);
- }
- },
- 'click #ozwell-new-chat'(event, template) {
- if (!ENABLE_RECENT_CHATS) return;
- event.preventDefault();
- template.resetConversation();
- template.selectedPrompt.set(null);
- template.loadRecentConversations();
- },
- 'click #ozwell-backdrop'(event, template) {
- if (event.target.id === 'ozwell-backdrop') {
- template.closeModal();
- }
- },
- 'click #ozwell-cancel'(event, template) {
- template.closeModal();
- },
- 'click .prompt-btn'(event, template) {
- event.preventDefault();
- const promptId = event.currentTarget.getAttribute('data-prompt-id');
- const prompts = template.availablePrompts.get();
- const selectedPrompt = prompts.find((prompt) => prompt.id === promptId);
-
- if (selectedPrompt) {
- template.selectedPrompt.set(selectedPrompt);
- template.initializeConversation(selectedPrompt);
- }
- },
- 'click #use-custom-prompt'(event, template) {
- event.preventDefault();
- 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.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);
- },
- '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()) {
- template.performAutofill({ closeModal: false });
- }
- },
- 'click #ozwell-save-close'(event, template) {
- event.preventDefault();
- 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);
- }
- }
-});
-
-Template.ozwellModal.onDestroyed(function () {
- this.unmountEmbeddedChat();
-});
-
-Template.ozwellModal.onDestroyed(function () {
- if (window.ozwellModalInstance === this) {
- window.ozwellModalInstance = null;
- }
-});
-
-window.openOzwell = function (inputElement, context = {}) {
- if (window.ozwellModalInstance) {
- window.ozwellModalInstance.openOzwell(inputElement, context);
- } else {
- console.error('Ozwell modal not available');
- }
-};
diff --git a/client/components/tickets/TicketsPage.html b/client/components/tickets/TicketsPage.html
index 9ea801c..3f868d9 100644
--- a/client/components/tickets/TicketsPage.html
+++ b/client/components/tickets/TicketsPage.html
@@ -22,11 +22,9 @@
My Activities & Tasks
- {{> ozwellButton}}
- {{> ozwellButton}}
diff --git a/client/main.js b/client/main.js
index f2ab987..653464a 100644
--- a/client/main.js
+++ b/client/main.js
@@ -11,10 +11,6 @@ import './components/calendar/CalendarPage.html';
import './components/admin/admin.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';
import './components/layout/MainLayout.js';
@@ -25,10 +21,6 @@ import './components/calendar/CalendarPage.js';
import './components/admin/admin.js';
import './components/settings/SettingsPage.js';
-// Import Ozwell component JS files
-import './components/ozwell/OzwellModal.js';
-import './components/ozwell/OzwellButton.js';
-
// Import routing configuration
import './routes.js';
// Import currentTime from MainLayout
diff --git a/server/methods/ozwell.js b/server/methods/ozwell.js
index 26a5979..3b5bb67 100644
--- a/server/methods/ozwell.js
+++ b/server/methods/ozwell.js
@@ -347,71 +347,6 @@ export const ozwellMethods = {
};
},
- // Save conversation
- async saveOzwellConversation(conversation) {
- check(conversation, Match.ObjectIncluding({
- teamId: String,
- fieldName: String,
- messages: Array,
- metadata: Match.Maybe(Object),
- label: Match.Maybe(String),
- conversationId: Match.Maybe(String)
- }));
- const {
- conversationId = null,
- teamId,
- fieldName,
- messages,
- metadata = {},
- label
- } = conversation;
- 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 sanitizedLabel = (label || metadata?.promptTitle || messages.find(msg => msg.role === 'user')?.content || 'Conversation')
- .toString()
- .substring(0, 120);
-
- if (conversationId) {
- const existing = await OzwellConversations.findOneAsync({
- _id: conversationId,
- teamId,
- userId: this.userId
- });
-
- if (!existing) {
- throw new Meteor.Error('not-found', 'Conversation not found');
- }
-
- await OzwellConversations.updateAsync(conversationId, {
- $set: {
- messages,
- metadata,
- label: sanitizedLabel,
- updatedAt: new Date()
- }
- });
-
- return conversationId;
- }
-
- const doc = {
- teamId,
- fieldName,
- userId: this.userId,
- messages,
- metadata,
- label: sanitizedLabel,
- createdAt: new Date(),
- updatedAt: new Date()
- };
-
- return await OzwellConversations.insertAsync(doc);
- },
-
// Get conversation history (metadata only)
async getOzwellConversations({ teamId, fieldName = null, limit = 10 }) {
check(teamId, String);
diff --git a/server/methods/ozwellPrompts.js b/server/methods/ozwellPrompts.js
deleted file mode 100644
index 911c849..0000000
--- a/server/methods/ozwellPrompts.js
+++ /dev/null
@@ -1,183 +0,0 @@
-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
diff --git a/server/methods/referenceAssistant.js b/server/methods/referenceAssistant.js
deleted file mode 100644
index 98361b7..0000000
--- a/server/methods/referenceAssistant.js
+++ /dev/null
@@ -1,60 +0,0 @@
-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 716779430271bb9b6d7c12a582e7116c6248b895 Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Tue, 21 Oct 2025 20:52:05 -0400
Subject: [PATCH 36/57] refactor: remove old modal Ozwell-related collections,
HTML frame, and smoke test; clean up server methods
---
collections.js | 1 -
public/ozwell-frame.html | 149 ----------------------------
public/tests/mcp-smoke-test.js | 175 ---------------------------------
server/main.js | 104 +-------------------
server/methods/ozwell.js | 2 +-
5 files changed, 2 insertions(+), 429 deletions(-)
delete mode 100644 public/ozwell-frame.html
delete mode 100644 public/tests/mcp-smoke-test.js
diff --git a/collections.js b/collections.js
index 5af1846..b932840 100644
--- a/collections.js
+++ b/collections.js
@@ -10,4 +10,3 @@ export const ClockEvents = new Mongo.Collection('clockevents');
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/public/ozwell-frame.html b/public/ozwell-frame.html
deleted file mode 100644
index e3d42ef..0000000
--- a/public/ozwell-frame.html
+++ /dev/null
@@ -1,149 +0,0 @@
-
-
-
-
-
Ozwell MCP Frame
-
-
-
-
-
-
-
-
-
diff --git a/public/tests/mcp-smoke-test.js b/public/tests/mcp-smoke-test.js
deleted file mode 100644
index 5f2706f..0000000
--- a/public/tests/mcp-smoke-test.js
+++ /dev/null
@@ -1,175 +0,0 @@
-export function runMcpSmokeTest(options = {}) {
- const {
- prompt = 'Demo prompt: Summarize recent progress.',
- teamName = options.teamName || (window?.ozwellModalInstance?.currentContext?.get?.()?.teamName) || 'Sample Project',
- username = options.username || (window?.ozwellModalInstance?.currentContext?.get?.()?.user?.username) || (Meteor.user()?.username || 'User')
- } = options;
-
- const context = {
- teamName,
- user: { username }
- };
-
- const overlay = document.createElement('div');
- overlay.style.position = 'fixed';
- overlay.style.bottom = '16px';
- overlay.style.right = '16px';
- overlay.style.width = '420px';
- overlay.style.height = '620px';
- overlay.style.background = 'rgba(17,24,39,0.95)';
- overlay.style.border = '1px solid rgba(79,70,229,0.4)';
- overlay.style.borderRadius = '18px';
- overlay.style.boxShadow = '0 20px 45px rgba(15,23,42,0.4)';
- overlay.style.zIndex = '9999';
- overlay.style.display = 'flex';
- overlay.style.flexDirection = 'column';
- overlay.style.color = '#f9fafb';
- overlay.style.fontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
-
- const header = document.createElement('div');
- header.style.display = 'flex';
- header.style.justifyContent = 'space-between';
- header.style.alignItems = 'center';
- header.style.padding = '12px 16px';
- header.style.borderBottom = '1px solid rgba(255,255,255,0.08)';
- header.innerHTML = '
MCP Smoke Test';
-
- const closeBtn = document.createElement('button');
- closeBtn.textContent = '×';
- closeBtn.style.background = 'transparent';
- closeBtn.style.border = 'none';
- closeBtn.style.color = '#f9fafb';
- closeBtn.style.fontSize = '20px';
- closeBtn.style.cursor = 'pointer';
- closeBtn.onclick = () => {
- window.removeEventListener('message', handler);
- overlay.remove();
- };
- header.appendChild(closeBtn);
-
- const frame = document.createElement('iframe');
- frame.src = '/ozwell-frame.html';
- frame.style.flex = '1';
- frame.style.border = '0';
- frame.style.borderBottom = '1px solid rgba(255,255,255,0.08)';
-
- const controlBar = document.createElement('div');
- controlBar.style.display = 'flex';
- controlBar.style.gap = '8px';
- controlBar.style.padding = '12px 16px';
-
- const input = document.createElement('input');
- input.type = 'text';
- input.value = prompt;
- input.placeholder = 'Type prompt...';
- input.style.flex = '1';
- input.style.borderRadius = '12px';
- input.style.border = '1px solid rgba(255,255,255,0.12)';
- input.style.background = 'rgba(255,255,255,0.06)';
- input.style.color = '#f9fafb';
- input.style.padding = '10px 12px';
-
- const sendBtn = document.createElement('button');
- sendBtn.textContent = 'Send prompt';
- sendBtn.style.borderRadius = '12px';
- sendBtn.style.border = 'none';
- sendBtn.style.background = '#4f46e5';
- sendBtn.style.color = '#fff';
- sendBtn.style.padding = '10px 18px';
- sendBtn.style.cursor = 'pointer';
-
- const logArea = document.createElement('pre');
- logArea.style.margin = '0';
- logArea.style.padding = '12px 16px';
- logArea.style.background = 'rgba(255,255,255,0.03)';
- logArea.style.borderTop = '1px solid rgba(255,255,255,0.08)';
- logArea.style.maxHeight = '120px';
- logArea.style.overflow = 'auto';
- logArea.style.fontSize = '12px';
- logArea.textContent = 'Waiting for client hello...\n';
-
- controlBar.appendChild(input);
- controlBar.appendChild(sendBtn);
-
- overlay.appendChild(header);
- overlay.appendChild(frame);
- overlay.appendChild(controlBar);
- overlay.appendChild(logArea);
-
- document.body.appendChild(overlay);
-
- let ready = false;
-
- function log(line) {
- logArea.textContent += `${line}\n`;
- logArea.scrollTop = logArea.scrollHeight;
- }
-
- function buildMessages(promptText) {
- const systemMessage = `You are a helpful assistant for time tracking and project management.\nInstructions: Provide only polished, ready-to-paste suggestions. Avoid Markdown, template placeholders, or meta commentary.`;
- const summary = [`Project: ${context.teamName}`, `User: ${context.user.username}`].join('\n');
- return [
- { role: 'system', content: systemMessage },
- { role: 'system', content: `Project context:\n${summary}` },
- { role: 'user', content: promptText }
- ];
- }
-
- function reply(message) {
- if (!frame.contentWindow) return;
- frame.contentWindow.postMessage({ source: 'ozwell-modal-bridge', ...message }, '*');
- }
-
- function handler(event) {
- const data = event.data;
- if (!data || data.source !== 'ozwell-mcp-frame') return;
-
- if (data.type === 'client-hello') {
- log('Client hello received');
- ready = true;
- reply({ type: 'mcp-ready', contextSummary: `${context.teamName} (manual test)` });
- return;
- }
-
- if (data.type === 'model-request') {
- const promptText = data.payload?.prompt || '';
- log(`Model request -> ${promptText}`);
- Meteor.call('callReferenceAssistant', {
- messages: buildMessages(promptText),
- metadata: { transport: 'mcp-smoke-test', teamName: context.teamName }
- }, (err, result) => {
- if (err) {
- log(`Error: ${err.reason || err.message}`);
- reply({ type: 'model-error', error: err.reason || err.message });
- } else {
- const content = result?.content || '';
- log(`Model response <- ${content}`);
- reply({ type: 'model-response', payload: { content } });
- }
- });
- return;
- }
-
- if (data.type === 'model-response') {
- log(`Frame echo response: ${data.payload?.content}`);
- }
-
- if (data.type === 'model-error') {
- log(`Frame error: ${data.error}`);
- }
- }
-
- window.addEventListener('message', handler);
-
- sendBtn.onclick = () => {
- if (!ready) {
- log('Frame not ready yet.');
- return;
- }
- const promptText = input.value.trim();
- if (!promptText) return;
- reply({ type: 'model-request', payload: { prompt: promptText } });
- };
-
- log('Smoke test harness attached. Wait for client hello, then click "Send prompt".');
-}
diff --git a/server/main.js b/server/main.js
index 46521bd..75c98cf 100644
--- a/server/main.js
+++ b/server/main.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { ServiceConfiguration } from 'meteor/service-configuration';
-import { Tickets, Teams, Sessions, ClockEvents, OzwellPrompts } from '../collections.js';
+import { Tickets, Teams, Sessions, ClockEvents } from '../collections.js';
// Import authentication methods
import { authMethods } from './methods/auth.js';
// Import team methods
@@ -18,8 +18,6 @@ dotenv.config({ path: '.env' });
// Import Ozwell methods
import { ozwellMethods } from './methods/ozwell.js';
-import { ozwellPromptMethods } from './methods/ozwellPrompts.js';
-import { referenceAssistantMethods } from './methods/referenceAssistant.js';
Meteor.startup(async () => {
// Configure Google OAuth from environment variables
const googleClientId = process.env.GOOGLE_CLIENT_ID;
@@ -108,104 +106,6 @@ 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 () {
@@ -352,8 +252,6 @@ Meteor.methods({
...ticketMethods,
...clockEventMethods,
...ozwellMethods,
- ...ozwellPromptMethods,
- ...referenceAssistantMethods,
'participants.create'(name) {
check(name, String);
diff --git a/server/methods/ozwell.js b/server/methods/ozwell.js
index 3b5bb67..3701239 100644
--- a/server/methods/ozwell.js
+++ b/server/methods/ozwell.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
-import { OzwellWorkspaces, OzwellUsers, OzwellConversations, OzwellPrompts, Teams, Tickets, ClockEvents } from '../../collections.js';
+import { OzwellWorkspaces, OzwellUsers, OzwellConversations, Teams, Tickets, ClockEvents } from '../../collections.js';
import axios from 'axios';
const DEFAULT_REFERENCE_BASE_URL = 'http://localhost:3000/v1';
From 41301734a1c4315622cc56c73a2a9c5344a22d8e Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Wed, 22 Oct 2025 19:59:28 -0400
Subject: [PATCH 37/57] feat: Add setup guide for Ozwell AI Assistant
integration
---
OZWELL_SETUP.md | 192 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 192 insertions(+)
create mode 100644 OZWELL_SETUP.md
diff --git a/OZWELL_SETUP.md b/OZWELL_SETUP.md
new file mode 100644
index 0000000..fbf0d9b
--- /dev/null
+++ b/OZWELL_SETUP.md
@@ -0,0 +1,192 @@
+# Ozwell AI Assistant Setup
+
+Quick setup guide for the AI chat widget in TimeHarbor.
+
+---
+
+## Quick Start
+
+**Time required:** ~5 minutes
+
+1. **Install Ollama**
+ ```bash
+ # macOS
+ brew install ollama
+
+ # Linux
+ curl -fsSL https://ollama.com/install.sh | sh
+ ```
+
+2. **Pull the AI model**
+ ```bash
+ ollama pull qwen2.5:14b
+ ```
+
+3. **Clone & start reference server**
+ ```bash
+ # TODO: Add final clone URL after reference server PR is merged
+ # For now, contact maintainers for development branch access
+
+ cd reference-server
+ npm install
+ npm run dev
+ ```
+ The server will start at `http://localhost:3000`
+
+4. **Start TimeHarbor**
+ ```bash
+ cd timeharbor
+ meteor --port 3001
+ ```
+
+5. **Test the widget**
+ - Look for chat button (bottom-right corner)
+ - Click to open chat
+ - Try: "how much time have I spent?"
+
+---
+
+## What You Get
+
+- **Context-aware suggestions** - AI reads your actual project history
+- **Instant time stats** - Ask about hours logged across tickets
+- **Auto-fill forms** - AI can update ticket fields for you
+- **Project search** - Find recent tickets and activity
+
+---
+
+## Architecture
+
+```
+TimeHarbor (localhost:3001)
+ |
+ v
+Reference Server (localhost:3000)
+ |
+ v
+Ollama (qwen2.5:14b model)
+```
+
+All processing happens locally on your machine. No external API calls.
+
+---
+
+## Available MCP Tools
+
+The AI has access to these tools when chatting:
+
+| Tool | Purpose | Example Usage |
+|------|---------|---------------|
+| `get_project_history` | Retrieves recent tickets from current project | "suggest some ticket titles" |
+| `get_project_time_stats` | Calculates total time spent on project | "how much time have I spent?" |
+| `get_current_ticket_form` | Reads current form field values | "what's filled in right now?" |
+| `update_ticket_title` | Auto-fills the title field | "set title to Fix login bug" |
+| `update_ticket_description` | Auto-fills description field | "add description: Fixed auth issue" |
+| `update_ticket_time` | Sets hours/minutes/seconds | "set time to 2 hours 30 minutes" |
+| `get_conversation_history` | Retrieves past conversations (currently unused) | - |
+
+---
+
+## Adding Your Own Tool
+
+Want to add custom functionality? Here's a quick example:
+
+**1. Define the tool** in `/public/ozwell-mcp-tools.js`:
+```javascript
+{
+ type: 'function',
+ function: {
+ name: 'get_ticket_count',
+ description: 'Counts total tickets in current project',
+ parameters: {
+ type: 'object',
+ properties: {},
+ required: []
+ }
+ }
+}
+```
+
+**2. Add the handler** in the same file:
+```javascript
+const toolHandlers = {
+ // ... existing handlers
+
+ get_ticket_count: async (params) => {
+ const teamSelect = document.querySelector('#teamSelect');
+ const teamId = teamSelect?.value;
+
+ const count = await new Promise((resolve, reject) => {
+ Meteor.call('getTicketCount', { teamId }, (error, result) => {
+ if (error) reject(error);
+ else resolve(result);
+ });
+ });
+
+ return {
+ success: true,
+ count,
+ message: `Found ${count} tickets`
+ };
+ }
+};
+```
+
+**3. Add server method** in `/server/methods/ozwell.js`:
+```javascript
+async getTicketCount({ teamId }) {
+ check(teamId, String);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ const count = await Tickets.find({ teamId }).countAsync();
+ return count;
+}
+```
+
+That's it! The AI can now use your custom tool.
+
+---
+
+## Troubleshooting
+
+**Chat widget not appearing?**
+- Check reference server is running: `curl http://localhost:3000`
+- Verify browser console for errors (F12 > Console tab)
+
+**AI not calling tools?**
+- Check Ollama is running: `ollama list`
+- Verify model is downloaded: Should see `qwen2.5:14b` in list
+- Try restarting reference server
+
+**Reference server failing to start?**
+- Check Node.js version: `node --version` (needs v18+)
+- Verify Ollama is accessible: `curl http://localhost:11434`
+- Check logs in terminal for specific errors
+
+**Where's the reference server code?**
+- TODO: Add GitHub URL after PR merge
+- Contact TimeHarbor maintainers for current development branch
+
+---
+
+## File Reference
+
+Key files for Ozwell integration:
+
+- `/public/chat-wrapper.js` - Widget UI and drag behavior
+- `/public/ozwell-mcp-tools.js` - Tool definitions and handlers
+- `/public/ozwell-iframe-sync.js` - Form state synchronization
+- `/client/main.html` - Widget configuration
+- `/server/methods/ozwell.js` - Meteor server methods
+
+---
+
+## Next Steps
+
+After setup works:
+1. Try different queries to see tool capabilities
+2. Check browser console to see which tools get called
+3. Customize system prompt in `/client/main.html` if needed
+4. Add your own tools using the example above
+
+For issues or questions, create an issue in the TimeHarbor repository.
From ce61f099e82fc21ca02d91a9107f64d22e89175b Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Tue, 28 Oct 2025 19:44:00 -0400
Subject: [PATCH 38/57] fix: Update Ozwell integration scripts and improve
widget loading logic
---
client/main.html | 4 ++--
public/chat-wrapper.js | 22 +++++++++++++++++++---
2 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/client/main.html b/client/main.html
index 7f43773..bfcce6c 100644
--- a/client/main.html
+++ b/client/main.html
@@ -78,7 +78,7 @@
};
-
+
@@ -86,7 +86,7 @@
{{> Template.dynamic template=currentScreen}}
-
+
diff --git a/public/chat-wrapper.js b/public/chat-wrapper.js
index 6baf0a9..010ab9d 100644
--- a/public/chat-wrapper.js
+++ b/public/chat-wrapper.js
@@ -238,11 +238,20 @@ class ChatWrapper {
return;
}
- // Wait for Ozwell embed script to load
+ // Wait for Ozwell embed script to load, then mount the widget
const waitForOzwell = setInterval(() => {
- if (window.OzwellChat && window.OzwellChat.iframe) {
+ if (window.OzwellChat && typeof window.OzwellChat.mount === 'function') {
clearInterval(waitForOzwell);
+ // Mount the widget iframe (new loading pattern)
+ console.log('[TimeHarbor] Mounting Ozwell widget...');
+ window.OzwellChat.mount();
+
+ // Now wait for the iframe to be created
+ const waitForIframe = setInterval(() => {
+ if (window.OzwellChat.iframe) {
+ clearInterval(waitForIframe);
+
// Get the Ozwell iframe
const ozwellIframe = window.OzwellChat.iframe;
@@ -264,7 +273,14 @@ class ChatWrapper {
ozwellIframe.style.left = '0';
ozwellIframe.style.top = '0';
- console.log('Ozwell widget loaded successfully');
+ console.log('Ozwell widget loaded successfully');
+ }
+ }, 100);
+
+ // Timeout for iframe creation
+ setTimeout(() => {
+ clearInterval(waitForIframe);
+ }, 5000);
}
}, 100);
From d5394edc2266231bfd6ff8ec9a6a2a76eb6decc5 Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Wed, 5 Nov 2025 14:09:49 -0500
Subject: [PATCH 39/57] refactor: Remove settings page and related routing
logic
---
client/components/layout/MainLayout.html | 1 -
client/components/layout/MainLayout.js | 2 -
client/components/settings/SettingsPage.html | 131 ---------------
client/components/settings/SettingsPage.js | 165 -------------------
client/main.js | 2 -
client/routes.js | 12 --
6 files changed, 313 deletions(-)
delete mode 100644 client/components/settings/SettingsPage.html
delete mode 100644 client/components/settings/SettingsPage.js
diff --git a/client/components/layout/MainLayout.html b/client/components/layout/MainLayout.html
index 5c372b4..bab7987 100644
--- a/client/components/layout/MainLayout.html
+++ b/client/components/layout/MainLayout.html
@@ -9,7 +9,6 @@
TimeHarbor
Tickets
Calendar
Admin Review
-
Settings