-
Create or join a project to start tracking your activities and time.
+
+
+
+
+ {{/if}}
+
+ {{#each tickets}}
+
+
{{title}}
+
{{formatTime displayTime}}
+ {{#if github}}
+
Reference
+ {{/if}}
+
+
+ {{/each}}
+
+ {{else}}
+
+
Create or join a project to start tracking your activities and time.
+
{{/if}}
\ No newline at end of file
diff --git a/client/components/tickets/TicketsPage.js b/client/components/tickets/TicketsPage.js
index 313ccd6..5442baa 100644
--- a/client/components/tickets/TicketsPage.js
+++ b/client/components/tickets/TicketsPage.js
@@ -44,9 +44,9 @@ const ticketManager = {
try {
templateInstance.activeTicketId.set(ticketId);
const now = utils.now();
-
+
await utils.meteorCall('updateTicketStart', ticketId, now);
-
+
if (clockEvent) {
await utils.meteorCall('clockEventAddTicket', clockEvent._id, ticketId, now);
}
@@ -61,7 +61,7 @@ const ticketManager = {
try {
const now = utils.now();
await utils.meteorCall('updateTicketStop', ticketId, now);
-
+
if (clockEvent) {
await utils.meteorCall('clockEventStopTicket', clockEvent._id, ticketId, now);
}
@@ -75,12 +75,12 @@ const ticketManager = {
// Switch from one ticket to another
switchTicket: async (newTicketId, templateInstance, clockEvent) => {
const currentActiveId = templateInstance.activeTicketId.get();
-
+
if (currentActiveId) {
const success = await ticketManager.stopTicket(currentActiveId, clockEvent);
if (!success) return false;
}
-
+
await ticketManager.startTicket(newTicketId, templateInstance, clockEvent);
return true;
}
@@ -118,17 +118,51 @@ Template.tickets.onCreated(function () {
this.activeTicketId = new ReactiveVar(null);
this.clockedIn = new ReactiveVar(false);
+ // Add getOzwellContext method to this template instance
+ this.getOzwellContext = () => {
+ const teamId = this.selectedTeamId.get();
+ const team = Teams.findOne(teamId);
+ const activeTicketId = this.activeTicketId.get();
+ const activeTicket = activeTicketId ? Tickets.findOne(activeTicketId) : null;
+
+ // Get recent tickets for context
+ const recentTickets = Tickets.find(
+ { teamId },
+ { sort: { updatedAt: -1 }, limit: 5 }
+ ).fetch();
+
+ return {
+ teamId,
+ teamName: team?.name || 'Unknown Project',
+ user: {
+ username: Meteor.user()?.username || 'Unknown User'
+ },
+ currentTicket: activeTicket ? {
+ title: activeTicket.title,
+ description: activeTicket.github || '',
+ status: 'active',
+ totalTime: activeTicket.totalTime || 0
+ } : null,
+ recentActivity: recentTickets.map(ticket => ({
+ title: ticket.title,
+ description: ticket.github || '',
+ totalTime: ticket.totalTime || 0,
+ lastUpdated: ticket.updatedAt || ticket.createdAt
+ }))
+ };
+ };
+
this.autorun(() => {
const teamIds = Teams.find({}).map(t => t._id);
let teamId = this.selectedTeamId.get();
-
+
if (!teamId && teamIds.length > 0) {
this.selectedTeamId.set(teamIds[0]);
teamId = this.selectedTeamId.get();
}
-
+
this.subscribe('teamTickets', teamIds);
-
+
if (teamId) {
const activeSession = ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null });
if (activeSession) {
@@ -152,14 +186,14 @@ Template.tickets.helpers({
tickets() {
const teamId = Template.instance().selectedTeamId.get();
if (!teamId) return [];
-
+
const activeTicketId = Template.instance().activeTicketId.get();
const now = currentTime.get();
-
+
return Tickets.find({ teamId }).fetch().map(ticket => {
const isActive = ticket._id === activeTicketId && ticket.startTimestamp;
const elapsed = isActive ? Math.max(0, Math.floor((now - ticket.startTimestamp) / 1000)) : 0;
-
+
return {
...ticket,
displayTime: (ticket.accumulatedTime || 0) + elapsed
@@ -191,7 +225,7 @@ Template.tickets.helpers({
currentActiveTicketInfo() {
const activeTicketId = Template.instance().activeTicketId.get();
if (!activeTicketId) return null;
-
+
const ticket = Tickets.findOne(activeTicketId);
return ticket ? {
id: ticket._id,
@@ -204,7 +238,7 @@ Template.tickets.helpers({
const teamId = Template.instance().selectedTeamId.get();
const hasActiveSession = teamId ? !!ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null }) : false;
const hasOtherActiveTicket = Template.instance().activeTicketId.get() && Template.instance().activeTicketId.get() !== ticketId;
-
+
if (isActive) return 'btn btn-outline btn-neutral';
if (hasActiveSession && !hasOtherActiveTicket) return 'btn btn-outline btn-neutral';
if (hasActiveSession && hasOtherActiveTicket) return 'btn btn-disabled';
@@ -215,7 +249,7 @@ Template.tickets.helpers({
const teamId = Template.instance().selectedTeamId.get();
const hasActiveSession = teamId ? !!ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null }) : false;
const hasOtherActiveTicket = Template.instance().activeTicketId.get() && Template.instance().activeTicketId.get() !== ticketId;
-
+
if (isActive) return 'Click to stop this activity';
if (hasActiveSession && !hasOtherActiveTicket) return 'Click to start this activity';
if (hasActiveSession && hasOtherActiveTicket) return 'Stop the current activity first';
@@ -239,10 +273,10 @@ Template.tickets.events({
'paste [name="title"]'(e) {
setTimeout(() => extractUrlTitle(e.target.value, e.target), 0);
},
-
+
async 'submit #createTicketForm'(e, t) {
e.preventDefault();
-
+
const formData = {
teamId: t.selectedTeamId.get(),
title: e.target.title.value.trim(),
@@ -251,23 +285,23 @@ Template.tickets.events({
minutes: parseInt(e.target.minutes.value) || 0,
seconds: parseInt(e.target.seconds.value) || 0
};
-
+
if (!formData.title) {
alert('Ticket title is required.');
return;
}
-
+
try {
const accumulatedTime = utils.calculateAccumulatedTime(formData.hours, formData.minutes, formData.seconds);
- const ticketId = await utils.meteorCall('createTicket', {
- teamId: formData.teamId,
- title: formData.title,
- github: formData.github,
- accumulatedTime
+ const ticketId = await utils.meteorCall('createTicket', {
+ teamId: formData.teamId,
+ title: formData.title,
+ github: formData.github,
+ accumulatedTime
});
-
+
t.showCreateTicketForm.set(false);
-
+
if (accumulatedTime > 0) {
const clockEvent = ClockEvents.findOne({ userId: Meteor.userId(), teamId: formData.teamId, endTime: null });
await ticketManager.startTicket(ticketId, t, clockEvent);
@@ -276,35 +310,35 @@ Template.tickets.events({
utils.handleError(error, 'Error creating ticket');
}
},
-
+
async 'click .activate-ticket'(e, t) {
const ticketId = e.currentTarget.dataset.id;
const isActive = t.activeTicketId.get() === ticketId;
const teamId = t.selectedTeamId.get();
const clockEvent = ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null });
-
+
if (!isActive) {
if (!clockEvent) {
alert('Please start a session before starting an activity.');
return;
}
-
+
await ticketManager.switchTicket(ticketId, t, clockEvent);
} else {
await ticketManager.stopTicket(ticketId, clockEvent);
t.activeTicketId.set(null);
}
},
-
+
'click #clockInBtn'(e, t) {
const teamId = t.selectedTeamId.get();
sessionManager.startSession(teamId);
},
-
+
async 'click #clockOutBtn'(e, t) {
const teamId = t.selectedTeamId.get();
const activeTicketId = t.activeTicketId.get();
-
+
const success = await sessionManager.stopSession(teamId, activeTicketId);
if (success) {
t.activeTicketId.set(null);
diff --git a/client/main.js b/client/main.js
index b6a498c..58de517 100644
--- a/client/main.js
+++ b/client/main.js
@@ -11,6 +11,11 @@ import './components/layout/MainLayout.html';
import './components/teams/TeamsPage.html';
import './components/tickets/TicketsPage.html';
import './components/home/HomePage.html';
+import './components/settings/SettingsPage.html';
+
+// Import Ozwell components
+import './components/ozwell/OzwellModal.html';
+import './components/ozwell/OzwellButton.html';
// Import component JS files
import './components/auth/AuthPage.js';
@@ -18,6 +23,11 @@ import './components/layout/MainLayout.js';
import './components/teams/TeamsPage.js';
import './components/tickets/TicketsPage.js';
import './components/home/HomePage.js';
+import './components/settings/SettingsPage.js';
+
+// Import Ozwell component JS files
+import './components/ozwell/OzwellModal.js';
+import './components/ozwell/OzwellButton.js';
// Import currentTime from MainLayout
diff --git a/collections.js b/collections.js
index 853e1f9..5af1846 100644
--- a/collections.js
+++ b/collections.js
@@ -5,3 +5,9 @@ export const Teams = new Mongo.Collection('teams');
export const Sessions = new Mongo.Collection('sessions');
export const ClockEvents = new Mongo.Collection('clockevents');
+
+// Ozwell Integration Collections
+export const OzwellWorkspaces = new Mongo.Collection('ozwellworkspaces');
+export const OzwellUsers = new Mongo.Collection('ozwellusers');
+export const OzwellConversations = new Mongo.Collection('ozwellconversations');
+export const OzwellPrompts = new Mongo.Collection('ozwellprompts');
diff --git a/server/main.js b/server/main.js
index bc4871d..56c6269 100644
--- a/server/main.js
+++ b/server/main.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
-import { Tickets, Teams, Sessions, ClockEvents } from '../collections.js';
+import { Tickets, Teams, Sessions, ClockEvents, OzwellPrompts } from '../collections.js';
// Import authentication methods
import { authMethods } from './methods/auth.js';
// Import team methods
@@ -8,6 +8,9 @@ import { teamMethods } from './methods/teams.js';
// Import ticket and clock event methods
import { ticketMethods } from './methods/tickets.js';
import { clockEventMethods } from './methods/clockEvents.js';
+// Import Ozwell methods
+import { ozwellMethods } from './methods/ozwell.js';
+import { ozwellPromptMethods } from './methods/ozwellPrompts.js';
Meteor.startup(async () => {
// Code to run on server startup
if (await Tickets.find().countAsync() === 0) {
@@ -27,6 +30,104 @@ Meteor.startup(async () => {
const code = Math.random().toString(36).substr(2, 8).toUpperCase();
await Teams.updateAsync(team._id, { $set: { code } });
}
+
+ // Initialize Ozwell default prompts directly (not as a method call since we're in startup)
+ const promptCount = await OzwellPrompts.find().countAsync();
+ if (promptCount === 0) {
+ const defaultPrompts = [
+ {
+ id: 'draft-time-entry',
+ title: 'Help me draft a time entry',
+ description: 'Get assistance writing a detailed time entry for your work',
+ template: `Help me write a detailed time entry for my work today. Here's what I'm working on:
+
+Project: {{teamName}}
+{{#if currentTicket}}
+Current Activity: {{currentTicket.title}}
+{{#if currentTicket.description}}
+Notes/Reference: {{currentTicket.description}}
+{{/if}}
+{{/if}}
+
+{{#if recentActivity}}
+Recent activities in this project:
+{{#each recentActivity}}
+- {{title}}{{#if description}} ({{description}}){{/if}}
+{{/each}}
+{{/if}}
+
+Please help me write a professional time entry that describes what I accomplished, any challenges I faced, and next steps. Make it detailed enough for project tracking but concise for time logging.`,
+ category: 'time-tracking',
+ contexts: ['ticket-form', 'time-entry'],
+ icon: 'clock',
+ systemMessage: 'You are a professional time tracking assistant. Help users write clear, detailed time entries that capture their work accomplishments, challenges, and progress. Focus on being specific and actionable.',
+ createdAt: new Date()
+ },
+ {
+ id: 'summarize-daily-activity',
+ title: 'Summarize my activity today',
+ description: 'Create a summary of your work activities for the day',
+ template: `Please create a summary of my work activities for today.
+
+Project: {{teamName}}
+User: {{user.username}}
+
+{{#if recentActivity}}
+Activities worked on:
+{{#each recentActivity}}
+- {{title}}{{#if description}} - {{description}}{{/if}}
+ Time spent: {{totalTime}} seconds
+ Last updated: {{lastUpdated}}
+{{/each}}
+{{/if}}
+
+Please provide:
+1. A brief overview of what I accomplished today
+2. Key highlights or milestones reached
+3. Any blockers or challenges encountered
+4. Suggested priorities for tomorrow
+
+Make it suitable for sharing with team members or for personal reflection.`,
+ category: 'reporting',
+ contexts: ['end-of-day', 'summary'],
+ icon: 'chart-bar',
+ systemMessage: 'You are a professional work summary assistant. Help users create clear, organized summaries of their daily work that highlight accomplishments, identify challenges, and suggest next steps.',
+ createdAt: new Date()
+ },
+ {
+ id: 'improve-activity-description',
+ title: 'Improve my activity description',
+ description: 'Get help making your activity description more clear and detailed',
+ template: `Please help me improve this activity description:
+
+Current text: "{{currentText}}"
+
+{{#if currentTicket}}
+Activity: {{currentTicket.title}}
+{{/if}}
+Project: {{teamName}}
+
+Please help me:
+1. Make the description more clear and specific
+2. Add relevant technical details if appropriate
+3. Ensure it's useful for future reference
+4. Make it professional and well-structured
+
+Keep the core meaning but enhance clarity, detail, and usefulness for project tracking.`,
+ category: 'writing',
+ contexts: ['ticket-form', 'note-taking'],
+ icon: 'pencil',
+ systemMessage: 'You are a professional writing assistant specializing in technical documentation. Help users write clear, detailed, and well-structured activity descriptions that are useful for project tracking and future reference.',
+ createdAt: new Date()
+ }
+ ];
+
+ for (const prompt of defaultPrompts) {
+ await OzwellPrompts.insertAsync(prompt);
+ }
+
+ console.log('Initialized', defaultPrompts.length, 'default Ozwell prompts');
+ }
});
Meteor.publish('userTeams', function () {
@@ -81,6 +182,8 @@ Meteor.methods({
...teamMethods,
...ticketMethods,
...clockEventMethods,
+ ...ozwellMethods,
+ ...ozwellPromptMethods,
'participants.create'(name) {
check(name, String);
diff --git a/server/methods/auth.js b/server/methods/auth.js
index c63c723..11581f3 100644
--- a/server/methods/auth.js
+++ b/server/methods/auth.js
@@ -1,14 +1,15 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
+import { check } from 'meteor/check';
export const authMethods = {
- createUserAccount({ username, password }) {
+ async createUserAccount({ username, password }) {
if (!username || !password) {
throw new Meteor.Error('invalid-data', 'Username and password are required');
}
-
+
try {
- const userId = Accounts.createUser({ username, password });
+ const userId = await Accounts.createUserAsync({ username, password });
console.log('User created:', { userId, username }); // Log user creation details
return userId;
} catch (error) {
@@ -16,4 +17,17 @@ export const authMethods = {
throw new Meteor.Error('server-error', 'Failed to create user');
}
},
+
+ async updateUserProfile(updates) {
+ check(updates, Object);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ try {
+ await Meteor.users.updateAsync(this.userId, { $set: updates });
+ return { success: true };
+ } catch (error) {
+ console.error('Error updating user profile:', error);
+ throw new Meteor.Error('server-error', 'Failed to update profile');
+ }
+ }
};
\ No newline at end of file
diff --git a/server/methods/ozwell.js b/server/methods/ozwell.js
new file mode 100644
index 0000000..dc46582
--- /dev/null
+++ b/server/methods/ozwell.js
@@ -0,0 +1,375 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { OzwellWorkspaces, OzwellUsers, OzwellConversations, OzwellPrompts, Teams, Tickets, ClockEvents } from '../../collections.js';
+import axios from 'axios';
+
+const OZWELL_API_BASE = 'https://ai.bluehive.com/api/v1';
+
+export const ozwellMethods = {
+ // Test Ozwell API credentials
+ async testOzwellCredentials(apiKey) {
+ check(apiKey, String);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ try {
+ const response = await axios.post(`${OZWELL_API_BASE}/test-credentials`, {}, {
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ return { success: true, message: response.data.message };
+ } catch (error) {
+ console.error('Ozwell credentials test failed:', error.response?.data);
+ throw new Meteor.Error('ozwell-error', 'Invalid API credentials');
+ }
+ },
+
+ // Save user's Ozwell API key
+ async saveOzwellApiKey(apiKey) {
+ check(apiKey, String);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // First test the credentials
+ await ozwellMethods.testOzwellCredentials.call(this, apiKey);
+
+ // Save to user profile
+ await Meteor.users.updateAsync(this.userId, {
+ $set: {
+ 'profile.ozwellApiKey': apiKey,
+ 'profile.ozwellEnabled': true
+ }
+ });
+
+ return { success: true };
+ },
+
+ // Create or get Ozwell workspace for a team
+ async getOrCreateOzwellWorkspace(teamId) {
+ check(teamId, String);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // Check if user is member of team
+ const team = await Teams.findOneAsync({ _id: teamId, members: this.userId });
+ if (!team) throw new Meteor.Error('not-authorized', 'Not a team member');
+
+ // Get user's API key
+ const user = await Meteor.users.findOneAsync(this.userId);
+ const apiKey = user?.profile?.ozwellApiKey;
+ if (!apiKey) throw new Meteor.Error('ozwell-error', 'Ozwell API key not configured');
+
+ // Check if workspace already exists for this team
+ let workspace = await OzwellWorkspaces.findOneAsync({ teamId });
+
+ if (!workspace) {
+ // Create new workspace
+ try {
+ const response = await axios.post(`${OZWELL_API_BASE}/workspaces/create`, {
+ name: `TimeHarbor - ${team.name}`,
+ metaData: {
+ externalId: teamId,
+ teamName: team.name
+ }
+ }, {
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ workspace = {
+ teamId,
+ workspaceId: response.data.workspaceId,
+ name: team.name,
+ createdAt: new Date(),
+ createdBy: this.userId
+ };
+
+ workspace._id = await OzwellWorkspaces.insertAsync(workspace);
+ } catch (error) {
+ console.error('Failed to create Ozwell workspace:', error.response?.data);
+ throw new Meteor.Error('ozwell-error', 'Failed to create workspace');
+ }
+ }
+
+ return workspace;
+ },
+
+ // Create or get Ozwell user for current user in workspace
+ async getOrCreateOzwellUser(workspaceId) {
+ check(workspaceId, String);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // Get user's API key
+ const user = await Meteor.users.findOneAsync(this.userId);
+ const apiKey = user?.profile?.ozwellApiKey;
+ if (!apiKey) throw new Meteor.Error('ozwell-error', 'Ozwell API key not configured');
+
+ // Check if user already exists in this workspace
+ let ozwellUser = await OzwellUsers.findOneAsync({
+ userId: this.userId,
+ workspaceId
+ });
+
+ if (!ozwellUser) {
+ // Create new user in Ozwell workspace
+ try {
+ const response = await axios.post(`${OZWELL_API_BASE}/workspaces/${workspaceId}/create-user`, {}, {
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ ozwellUser = {
+ userId: this.userId,
+ workspaceId,
+ ozwellUserId: response.data.userId,
+ username: user.username,
+ createdAt: new Date()
+ };
+
+ ozwellUser._id = await OzwellUsers.insertAsync(ozwellUser);
+ } catch (error) {
+ console.error('Failed to create Ozwell user:', error.response?.data);
+ throw new Meteor.Error('ozwell-error', 'Failed to create user');
+ }
+ }
+
+ return ozwellUser;
+ },
+
+ // Create Ozwell session for user
+ async createOzwellSession(teamId, forceNewSession = false) {
+ check(teamId, String);
+ check(forceNewSession, Boolean);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // Get user's API key
+ const user = await Meteor.users.findOneAsync(this.userId);
+ const apiKey = user?.profile?.ozwellApiKey;
+ if (!apiKey) throw new Meteor.Error('ozwell-error', 'Ozwell API key not configured');
+
+ // Get or create workspace
+ const workspace = await ozwellMethods.getOrCreateOzwellWorkspace.call(this, teamId);
+
+ // Get or create user
+ const ozwellUser = await ozwellMethods.getOrCreateOzwellUser.call(this, workspace.workspaceId);
+
+ // Create user session
+ try {
+ const response = await axios.post(`${OZWELL_API_BASE}/workspaces/${workspace.workspaceId}/create-user-session`, {
+ userId: ozwellUser.ozwellUserId,
+ metaData: {
+ createListenSession: true,
+ forceNewSession,
+ embedType: 'iframe-basic'
+ }
+ }, {
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ return {
+ loginUrl: response.data.loginUrl,
+ loginToken: response.data.loginToken,
+ workspaceId: workspace.workspaceId,
+ userId: ozwellUser.ozwellUserId
+ };
+ } catch (error) {
+ console.error('Failed to create Ozwell session:', error.response?.data);
+ throw new Meteor.Error('ozwell-error', 'Failed to create session');
+ }
+ },
+
+ // Get page context for MCP
+ async getPageContext(teamId, ticketId = null) {
+ check(teamId, String);
+ check(ticketId, Match.Maybe(String));
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // Get team info
+ const team = await Teams.findOneAsync({ _id: teamId, members: this.userId });
+ if (!team) throw new Meteor.Error('not-authorized', 'Not a team member');
+
+ const context = {
+ team: {
+ name: team.name,
+ id: team._id,
+ memberCount: team.members?.length || 0
+ },
+ user: {
+ username: (await Meteor.users.findOneAsync(this.userId))?.username
+ }
+ };
+
+ // Add ticket context if provided
+ if (ticketId) {
+ const ticket = await Tickets.findOneAsync({ _id: ticketId, teamId });
+ if (ticket) {
+ context.currentTicket = {
+ title: ticket.title,
+ description: ticket.github || '',
+ status: ticket.status || 'active',
+ totalTime: ticket.totalTime || 0
+ };
+ }
+ }
+
+ // Add recent team activity
+ const recentTickets = await Tickets.find(
+ { teamId },
+ { sort: { updatedAt: -1 }, limit: 5 }
+ ).fetchAsync();
+
+ context.recentActivity = recentTickets.map(ticket => ({
+ title: ticket.title,
+ description: ticket.github || '',
+ totalTime: ticket.totalTime || 0,
+ lastUpdated: ticket.updatedAt
+ }));
+
+ return context;
+ },
+
+ // Search user's history
+ async searchUserHistory(query, teamId = null) {
+ check(query, String);
+ check(teamId, Match.Maybe(String));
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ const searchRegex = new RegExp(query, 'i');
+ const filter = { userId: this.userId };
+
+ if (teamId) {
+ filter.teamId = teamId;
+ }
+
+ // Search in tickets
+ const tickets = await Tickets.find({
+ ...filter,
+ $or: [
+ { title: searchRegex },
+ { github: searchRegex }
+ ]
+ }, {
+ sort: { updatedAt: -1 },
+ limit: 10
+ }).fetchAsync();
+
+ // Search in clock events
+ const clockEvents = await ClockEvents.find({
+ ...filter,
+ $or: [
+ { 'tickets.title': searchRegex },
+ { 'tickets.github': searchRegex }
+ ]
+ }, {
+ sort: { startTime: -1 },
+ limit: 10
+ }).fetchAsync();
+
+ return {
+ tickets: tickets.map(t => ({
+ title: t.title,
+ description: t.github || '',
+ totalTime: t.totalTime || 0,
+ date: t.updatedAt || t.createdAt
+ })),
+ sessions: clockEvents.map(ce => ({
+ startTime: ce.startTime,
+ endTime: ce.endTime,
+ totalTime: ce.totalTime || 0,
+ ticketCount: ce.tickets?.length || 0
+ }))
+ };
+ },
+
+ // Search project history
+ async searchProjectHistory(query, teamId) {
+ check(query, String);
+ check(teamId, String);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // Check team membership
+ const team = await Teams.findOneAsync({ _id: teamId, members: this.userId });
+ if (!team) throw new Meteor.Error('not-authorized', 'Not a team member');
+
+ const searchRegex = new RegExp(query, 'i');
+
+ // Search all team tickets
+ const tickets = await Tickets.find({
+ teamId,
+ $or: [
+ { title: searchRegex },
+ { github: searchRegex }
+ ]
+ }, {
+ sort: { updatedAt: -1 },
+ limit: 15
+ }).fetchAsync();
+
+ // Get usernames for context
+ const userIds = [...new Set(tickets.map(t => t.userId))];
+ const users = await Meteor.users.find(
+ { _id: { $in: userIds } },
+ { fields: { username: 1 } }
+ ).fetchAsync();
+
+ const userMap = {};
+ users.forEach(u => userMap[u._id] = u.username);
+
+ return {
+ teamName: team.name,
+ results: tickets.map(t => ({
+ title: t.title,
+ description: t.github || '',
+ author: userMap[t.userId] || 'Unknown',
+ totalTime: t.totalTime || 0,
+ date: t.updatedAt || t.createdAt
+ }))
+ };
+ },
+
+ // Save conversation
+ async saveOzwellConversation(teamId, messages, metadata = {}) {
+ check(teamId, String);
+ check(messages, Array);
+ check(metadata, Object);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // Check team membership
+ const team = await Teams.findOneAsync({ _id: teamId, members: this.userId });
+ if (!team) throw new Meteor.Error('not-authorized', 'Not a team member');
+
+ const conversation = {
+ teamId,
+ userId: this.userId,
+ messages,
+ metadata,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+
+ return await OzwellConversations.insertAsync(conversation);
+ },
+
+ // Get conversation history
+ async getOzwellConversations(teamId, limit = 10) {
+ check(teamId, String);
+ check(limit, Number);
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ // Check team membership
+ const team = await Teams.findOneAsync({ _id: teamId, members: this.userId });
+ if (!team) throw new Meteor.Error('not-authorized', 'Not a team member');
+
+ return await OzwellConversations.find(
+ { teamId, userId: this.userId },
+ { sort: { updatedAt: -1 }, limit }
+ ).fetchAsync();
+ }
+};
\ No newline at end of file
diff --git a/server/methods/ozwellPrompts.js b/server/methods/ozwellPrompts.js
new file mode 100644
index 0000000..911c849
--- /dev/null
+++ b/server/methods/ozwellPrompts.js
@@ -0,0 +1,183 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { OzwellPrompts } from '../../collections.js';
+
+export const ozwellPromptMethods = {
+ // Get all available prompts
+ async getOzwellPrompts() {
+ if (!this.userId) throw new Meteor.Error('not-authorized');
+
+ return await OzwellPrompts.find({}).fetchAsync();
+ },
+
+ // Initialize default prompts (called on startup)
+ async initializeDefaultPrompts() {
+ const promptCount = await OzwellPrompts.find().countAsync();
+
+ if (promptCount === 0) {
+ const defaultPrompts = [
+ {
+ id: 'draft-time-entry',
+ title: 'Help me draft a time entry',
+ description: 'Get assistance writing a detailed time entry for your work',
+ template: `Help me write a detailed time entry for my work today. Here's what I'm working on:
+
+Project: {{teamName}}
+{{#if currentTicket}}
+Current Activity: {{currentTicket.title}}
+{{#if currentTicket.description}}
+Notes/Reference: {{currentTicket.description}}
+{{/if}}
+{{/if}}
+
+{{#if recentActivity}}
+Recent activities in this project:
+{{#each recentActivity}}
+- {{title}}{{#if description}} ({{description}}){{/if}}
+{{/each}}
+{{/if}}
+
+Please help me write a professional time entry that describes what I accomplished, any challenges I faced, and next steps. Make it detailed enough for project tracking but concise for time logging.`,
+ category: 'time-tracking',
+ contexts: ['ticket-form', 'time-entry'],
+ icon: 'clock',
+ systemMessage: 'You are a professional time tracking assistant. Help users write clear, detailed time entries that capture their work accomplishments, challenges, and progress. Focus on being specific and actionable.',
+ createdAt: new Date()
+ },
+ {
+ id: 'summarize-daily-activity',
+ title: 'Summarize my activity today',
+ description: 'Create a summary of your work activities for the day',
+ template: `Please create a summary of my work activities for today.
+
+Project: {{teamName}}
+User: {{user.username}}
+
+{{#if recentActivity}}
+Activities worked on:
+{{#each recentActivity}}
+- {{title}}{{#if description}} - {{description}}{{/if}}
+ Time spent: {{totalTime}} seconds
+ Last updated: {{lastUpdated}}
+{{/each}}
+{{/if}}
+
+Please provide:
+1. A brief overview of what I accomplished today
+2. Key highlights or milestones reached
+3. Any blockers or challenges encountered
+4. Suggested priorities for tomorrow
+
+Make it suitable for sharing with team members or for personal reflection.`,
+ category: 'reporting',
+ contexts: ['end-of-day', 'summary'],
+ icon: 'chart-bar',
+ systemMessage: 'You are a professional work summary assistant. Help users create clear, organized summaries of their daily work that highlight accomplishments, identify challenges, and suggest next steps.',
+ createdAt: new Date()
+ },
+ {
+ id: 'cross-link-related-work',
+ title: 'Find related work and cross-links',
+ description: 'Analyze current work and suggest connections to related activities',
+ template: `I'm working on: "{{currentText}}"
+
+Project context:
+{{#if currentTicket}}
+Current Activity: {{currentTicket.title}}
+{{#if currentTicket.description}}
+Description: {{currentTicket.description}}
+{{/if}}
+{{/if}}
+
+Project: {{teamName}}
+
+{{#if recentActivity}}
+Other recent activities in this project:
+{{#each recentActivity}}
+- {{title}}{{#if description}} ({{description}}){{/if}}
+{{/each}}
+{{/if}}
+
+Please help me:
+1. Identify connections between my current work and other activities
+2. Suggest relevant cross-references or links to include
+3. Recommend related work that might be helpful to reference
+4. Improve the description to better connect with existing project work
+
+Focus on making my work more discoverable and connected to the broader project context.`,
+ category: 'organization',
+ contexts: ['ticket-form', 'note-taking'],
+ icon: 'link',
+ systemMessage: 'You are a project organization assistant. Help users connect their current work to related activities, suggest meaningful cross-references, and improve project coherence through better linking and categorization.',
+ createdAt: new Date()
+ },
+ {
+ id: 'improve-activity-description',
+ title: 'Improve my activity description',
+ description: 'Get help making your activity description more clear and detailed',
+ template: `Please help me improve this activity description:
+
+Current text: "{{currentText}}"
+
+{{#if currentTicket}}
+Activity: {{currentTicket.title}}
+{{/if}}
+Project: {{teamName}}
+
+Please help me:
+1. Make the description more clear and specific
+2. Add relevant technical details if appropriate
+3. Ensure it's useful for future reference
+4. Make it professional and well-structured
+
+Keep the core meaning but enhance clarity, detail, and usefulness for project tracking.`,
+ category: 'writing',
+ contexts: ['ticket-form', 'note-taking'],
+ icon: 'pencil',
+ systemMessage: 'You are a professional writing assistant specializing in technical documentation. Help users write clear, detailed, and well-structured activity descriptions that are useful for project tracking and future reference.',
+ createdAt: new Date()
+ },
+ {
+ id: 'plan-work-session',
+ title: 'Help me plan my work session',
+ description: 'Get assistance planning and organizing your upcoming work',
+ template: `Help me plan my work session for {{teamName}}.
+
+{{#if currentTicket}}
+I'm planning to work on: {{currentTicket.title}}
+{{#if currentTicket.description}}
+Current notes: {{currentTicket.description}}
+{{/if}}
+{{/if}}
+
+{{#if recentActivity}}
+Recent project activities:
+{{#each recentActivity}}
+- {{title}}{{#if description}} ({{description}}){{/if}}
+{{/each}}
+{{/if}}
+
+Please help me:
+1. Break down the work into manageable tasks
+2. Suggest a logical order for completing tasks
+3. Identify potential challenges or dependencies
+4. Recommend time estimates for different parts
+5. Suggest any preparatory work or research needed
+
+Focus on creating a clear, actionable plan that will help me work efficiently and track progress.`,
+ category: 'planning',
+ contexts: ['session-start', 'ticket-form'],
+ icon: 'clipboard-list',
+ systemMessage: 'You are a work planning assistant. Help users break down their work into manageable tasks, organize them logically, and create actionable plans that improve productivity and progress tracking.',
+ createdAt: new Date()
+ }
+ ];
+
+ for (const prompt of defaultPrompts) {
+ await OzwellPrompts.insertAsync(prompt);
+ }
+
+ console.log('Initialized', defaultPrompts.length, 'default Ozwell prompts');
+ }
+ }
+};
\ No newline at end of file
From 312a5ae38123e8076a1f6a09725ee2115026c0ef Mon Sep 17 00:00:00 2001
From: Aditya Damerla <75409196+zesty-genius128@users.noreply.github.com>
Date: Fri, 19 Sep 2025 15:37:19 -0400
Subject: [PATCH 03/19] Add 'Paste and Save' feature to OzwellModal
Introduces a new 'Paste and Save' button in the Ozwell modal footer when a session URL is present. This feature allows users to copy AI-generated content from the chat, paste it from the clipboard, and save it, with validation to prevent saving console logs or irrelevant content. Includes user instructions and fallback for restricted clipboard access.
---
client/components/ozwell/OzwellModal.html | 41 +++++++++++++------
client/components/ozwell/OzwellModal.js | 49 +++++++++++++++++++++++
2 files changed, 78 insertions(+), 12 deletions(-)
diff --git a/client/components/ozwell/OzwellModal.html b/client/components/ozwell/OzwellModal.html
index 2c04e18..c9299b1 100644
--- a/client/components/ozwell/OzwellModal.html
+++ b/client/components/ozwell/OzwellModal.html
@@ -86,19 +86,36 @@
Choose how I can help you: