diff --git a/NLP_Model/actual/__pycache__/DataProcess.cpython-314.pyc b/NLP_Model/actual/__pycache__/DataProcess.cpython-314.pyc index 93c805e41..a55ea9f50 100644 Binary files a/NLP_Model/actual/__pycache__/DataProcess.cpython-314.pyc and b/NLP_Model/actual/__pycache__/DataProcess.cpython-314.pyc differ diff --git a/NLP_Model/actual/__pycache__/MongoConnect.cpython-314.pyc b/NLP_Model/actual/__pycache__/MongoConnect.cpython-314.pyc index 8f6c29b48..7b827ce95 100644 Binary files a/NLP_Model/actual/__pycache__/MongoConnect.cpython-314.pyc and b/NLP_Model/actual/__pycache__/MongoConnect.cpython-314.pyc differ diff --git a/NLP_Model/actual/__pycache__/Summarizer.cpython-314.pyc b/NLP_Model/actual/__pycache__/Summarizer.cpython-314.pyc index ae59a92f5..4b25c927d 100644 Binary files a/NLP_Model/actual/__pycache__/Summarizer.cpython-314.pyc and b/NLP_Model/actual/__pycache__/Summarizer.cpython-314.pyc differ diff --git a/bolt_slack/app.js b/bolt_slack/app.js index 760fe7133..d85aa3f7a 100644 --- a/bolt_slack/app.js +++ b/bolt_slack/app.js @@ -13,6 +13,7 @@ const { HOME_CHANNEL_SELECT_ACTION_ID, HOME_REFRESH_ACTION_ID, HOME_SUMMARY_WEEK_SELECT_ACTION_ID, + HOME_GENERATE_SELECTED_WEEK_SUMMARY_ACTION_ID, HOME_USER_SUMMARY_SELECT_ACTION_ID, HOME_GENERATE_USER_SUMMARIES_ACTION_ID, HOME_GENERATE_SINGLE_USER_SUMMARY_ACTION_ID, @@ -143,6 +144,35 @@ async function getWorkspaceToken(teamId) { return null; } +function formatWeekRangeForMessage(weekKey) { + const normalizedWeekKey = typeof weekKey === 'string' ? weekKey.trim() : ''; + if (!normalizedWeekKey.startsWith('ws:')) { + return normalizedWeekKey || 'Unknown week'; + } + + const startIso = normalizedWeekKey.slice(3); + const start = new Date(startIso); + if (Number.isNaN(start.getTime())) { + return startIso || 'Unknown week'; + } + + const end = new Date(start); + end.setUTCDate(end.getUTCDate() + 6); + + const startText = start.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric' + }); + const endText = end.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric' + }); + + return `${startText} - ${endText}`; +} + async function authorizeWorkspace({ teamId }) { const botToken = await getWorkspaceToken(teamId); @@ -870,6 +900,107 @@ app.action(HOME_SUMMARY_WEEK_SELECT_ACTION_ID, async ({ ack, body, client, logge }); }); +// Home tab selected-week summary button: run weekly model for the selected week. +app.action(HOME_GENERATE_SELECTED_WEEK_SUMMARY_ACTION_ID, async ({ ack, body, client, logger }) => { + await ack(); + const { teamId, workspaceName } = getHomeWorkspaceContext(body); + + try { + const actionValue = body.actions && body.actions[0] && body.actions[0].value + ? body.actions[0].value + : encodeDashboardState({ channelName: '', selectedWeek: null, selectedUser: null }); + const { + channelName: selectedChannelName, + selectedWeek, + selectedUser + } = parseDashboardState(actionValue); + + if (!selectedChannelName) { + const dm = await client.conversations.open({ users: body.user.id }); + await client.chat.postMessage({ + channel: dm.channel.id, + text: 'Select a channel in Home first, then click *Generate Summary For Selected Week*.' + }); + return; + } + + const selectedWeekRaw = selectedWeek || getSelectedOptionValueFromViewState(body.view && body.view.state, HOME_SUMMARY_WEEK_SELECT_ACTION_ID); + if (!selectedWeekRaw || !selectedWeekRaw.startsWith('ws:')) { + const dm = await client.conversations.open({ users: body.user.id }); + await client.chat.postMessage({ + channel: dm.channel.id, + text: 'Select a week in Home first, then click *Generate Summary For Selected Week*.' + }); + return; + } + + const weekStart = selectedWeekRaw.slice(3); + const weekLabel = formatWeekRangeForMessage(selectedWeekRaw); + const databaseKey = await buildChannelKey(selectedChannelName, { client }); + + const dm = await client.conversations.open({ users: body.user.id }); + await client.chat.postMessage({ + channel: dm.channel.id, + text: `⏳ Starting weekly summarization for *#${selectedChannelName}* (${weekLabel})... I’ll send a follow-up when it finishes.` + }); + + void (async () => { + try { + const response = await apiClient.post( + `/api/summaries/${encodeURIComponent(databaseKey)}?weekStart=${encodeURIComponent(weekStart)}`, + null, + { timeout: 0 } + ); + + const savedCount = Number(response && response.data && response.data.savedCount); + await client.chat.postMessage({ + channel: dm.channel.id, + text: Number.isFinite(savedCount) + ? `✅ Weekly summarization completed for *#${selectedChannelName}* (${weekLabel}). Saved ${savedCount} summaries.` + : `✅ Weekly summarization completed for *#${selectedChannelName}* (${weekLabel}).` + }); + + publishHomeTab({ + client, + userId: body.user.id, + teamId, + workspaceName, + logger, + apiClient, + selectedChannelName, + selectedWeek: selectedWeekRaw, + selectedUser + }).catch((refreshError) => { + logger.error('Error refreshing Home after selected-week summary generation:', refreshError); + }); + } catch (generationError) { + logger.error('Background selected-week summary generation failed:', generationError); + + try { + await client.chat.postMessage({ + channel: dm.channel.id, + text: `❌ Failed weekly summarization for *#${selectedChannelName}* (${weekLabel}): ${generationError.message}` + }); + } catch (dmError) { + logger.error('Failed sending selected-week generation error via DM:', dmError); + } + } + })(); + } catch (error) { + logger.error('Error handling selected-week summary action:', error); + + try { + const dm = await client.conversations.open({ users: body.user.id }); + await client.chat.postMessage({ + channel: dm.channel.id, + text: `❌ Failed to generate selected-week summary: ${error.message}` + }); + } catch (dmError) { + logger.error('Failed sending selected-week handler error via DM:', dmError); + } + } +}); + // Home tab user summary dropdown: republish with selected user summary updates. app.action(HOME_USER_SUMMARY_SELECT_ACTION_ID, async ({ ack, body, client, logger }) => { await ack(); diff --git a/bolt_slack/homeDashboard.js b/bolt_slack/homeDashboard.js index 66e1bd02f..04b2ccf79 100644 --- a/bolt_slack/homeDashboard.js +++ b/bolt_slack/homeDashboard.js @@ -5,6 +5,7 @@ const { buildChannelKey } = require('../shared-utils/channelUtils'); const HOME_CHANNEL_SELECT_ACTION_ID = 'home_channel_select'; const HOME_REFRESH_ACTION_ID = 'home_refresh_button'; const HOME_SUMMARY_WEEK_SELECT_ACTION_ID = 'home_summary_week_select'; +const HOME_GENERATE_SELECTED_WEEK_SUMMARY_ACTION_ID = 'home_generate_selected_week_summary'; const HOME_USER_SUMMARY_SELECT_ACTION_ID = 'home_user_summary_select'; const HOME_GENERATE_USER_SUMMARIES_ACTION_ID = 'home_generate_user_summaries'; const HOME_GENERATE_SINGLE_USER_SUMMARY_ACTION_ID = 'home_generate_single_user_summary'; @@ -234,6 +235,64 @@ function getAvailableWeeks(summaries) { .map((bucket) => bucket.key); } +function getAvailableWeeksFromChannelCreation(channelCreatedAtTs) { + const now = new Date(); + if (Number.isNaN(now.getTime())) { + return []; + } + + const normalizedCreatedTs = Number(channelCreatedAtTs); + const createdAt = Number.isFinite(normalizedCreatedTs) && normalizedCreatedTs > 0 + ? new Date(normalizedCreatedTs * 1000) + : null; + const createdAtOrNow = createdAt && !Number.isNaN(createdAt.getTime()) + ? createdAt + : now; + + const startWeekIso = normalizeToUtcSundayStartIso(createdAtOrNow.toISOString()); + const currentWeekIso = normalizeToUtcSundayStartIso(now.toISOString()); + if (!startWeekIso || !currentWeekIso) { + return []; + } + + const startWeekMs = Date.parse(startWeekIso); + const currentWeekMs = Date.parse(currentWeekIso); + if (!Number.isFinite(startWeekMs) || !Number.isFinite(currentWeekMs)) { + return []; + } + + if (currentWeekMs < startWeekMs) { + return [`ws:${currentWeekIso}`]; + } + + const weekKeys = []; + const cursor = new Date(currentWeekIso); + while (cursor.getTime() >= startWeekMs) { + weekKeys.push(`ws:${cursor.toISOString().replace('.000Z', 'Z')}`); + cursor.setUTCDate(cursor.getUTCDate() - 7); + } + + return weekKeys; +} + +function formatWeekRangeForDisplay(weekKey) { + const normalizedWeekKey = typeof weekKey === 'string' ? weekKey.trim() : ''; + if (!normalizedWeekKey) { + return 'Unknown week'; + } + + if (normalizedWeekKey.startsWith('ws:')) { + return formatWeekRangeLabel(normalizedWeekKey.slice(3)) || normalizedWeekKey.slice(3); + } + + const weekInfo = getWeekInfo({ + week_start_utc: normalizedWeekKey.startsWith('ws:') ? normalizedWeekKey.slice(3) : null, + week_of: normalizedWeekKey.startsWith('wk:') ? normalizedWeekKey.slice(3) : null + }); + + return weekInfo.label || 'Unknown week'; +} + function buildWeekOptions(availableWeeks) { return availableWeeks.slice(0, MAX_STATIC_SELECT_OPTIONS).map((weekKey) => { const weekInfo = getWeekInfo({ @@ -372,7 +431,11 @@ async function getBotChannels(client, teamId) { const pageChannels = Array.isArray(response.channels) ? response.channels : []; for (const channel of pageChannels) { if (channel && channel.id && channel.name) { - channels.push({ id: channel.id, name: channel.name }); + channels.push({ + id: channel.id, + name: channel.name, + createdAtTs: Number(channel.created) + }); } } @@ -1030,7 +1093,7 @@ function buildUserSummaryBlocks({ selectedChannelName, userBuckets, selectedUser type: 'section', text: { type: 'mrkdwn', - text: `:bust_in_silhouette: *${selectedUserLabel}*\n\n:hourglass_flowing_sand: No summary available yet for this user. Click "Generate Summaries for All Members" to create one.` + text: `:bust_in_silhouette: *${selectedUserLabel}*\n\n:hourglass_flowing_sand: No summary available yet for this user.\nSelect "Generate Summary" to create one, or select "Generate Summaries for All Members" to structure summaries for everyone!` } }); } else { @@ -1078,7 +1141,7 @@ function buildUserSummaryBlocks({ selectedChannelName, userBuckets, selectedUser return blocks; } -function buildWeeklySummaryBlocks({ selectedChannelName, dbName, summaries, selectedWeek }) { +function buildWeeklySummaryBlocks({ selectedChannelName, dbName, summaries, selectedWeek, selectedChannelCreatedAtTs }) { if (!selectedChannelName) { return [ { @@ -1091,18 +1154,6 @@ function buildWeeklySummaryBlocks({ selectedChannelName, dbName, summaries, sele ]; } - if (!summaries.length) { - return [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: `No weekly summaries found yet for *#${selectedChannelName}*.` - } - } - ]; - } - const sortedSummaries = summaries .slice() .sort((left, right) => { @@ -1111,7 +1162,7 @@ function buildWeeklySummaryBlocks({ selectedChannelName, dbName, summaries, sele return rightDate - leftDate; }); - const availableWeeks = getAvailableWeeks(sortedSummaries); + const availableWeeks = getAvailableWeeksFromChannelCreation(selectedChannelCreatedAtTs); const selectedWeekKey = typeof selectedWeek === 'string' ? selectedWeek : ''; const resolvedSelectedWeek = availableWeeks.includes(selectedWeekKey) ? selectedWeekKey @@ -1152,8 +1203,40 @@ function buildWeeklySummaryBlocks({ selectedChannelName, dbName, summaries, sele } }); + blocks.push({ + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'Generate Summaries For This Week', + emoji: true + }, + ...(!visibleSummaries.length ? { style: 'primary' } : {}), + action_id: HOME_GENERATE_SELECTED_WEEK_SUMMARY_ACTION_ID, + value: encodeDashboardState({ + channelName: selectedChannelName || '', + selectedWeek: resolvedSelectedWeek, + selectedUser: null + }) + } + ] + }); + blocks.push({ type: 'divider' }); + if (!visibleSummaries.length) { + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: `No weekly summaries found for *${formatWeekRangeForDisplay(resolvedSelectedWeek)}* in *#${selectedChannelName}*.` + } + }); + return blocks; + } + blocks.push({ type: 'context', elements: [ @@ -1161,10 +1244,7 @@ function buildWeeklySummaryBlocks({ selectedChannelName, dbName, summaries, sele type: 'mrkdwn', text: resolvedSelectedWeek == null ? `Showing all ${visibleSummaries.length} daily updates.` - : `Showing ${visibleSummaries.length} daily updates for *${(getWeekInfo({ - week_start_utc: resolvedSelectedWeek.startsWith('ws:') ? resolvedSelectedWeek.slice(3) : null, - week_of: resolvedSelectedWeek.startsWith('wk:') ? resolvedSelectedWeek.slice(3) : null - }).label)}*.` + : `Showing ${visibleSummaries.length} daily updates for *${formatWeekRangeForDisplay(resolvedSelectedWeek)}*.` } ] }); @@ -1202,6 +1282,7 @@ function buildSampleHomeView({ workspaceName, channelOptions, selectedChannelName, + selectedChannelCreatedAtTs, selectedWeek, selectedUser, selectedUserSummary, @@ -1329,7 +1410,8 @@ function buildSampleHomeView({ selectedChannelName, dbName, summaries, - selectedWeek + selectedWeek, + selectedChannelCreatedAtTs })); if (activeChannels > selectableChannels) { @@ -1495,6 +1577,8 @@ async function publishHomeTab({ client, userId, teamId, workspaceName, logger, a console.log(`[publishHomeTab] Loaded channels for Home dropdown: ${channels.length} total | First 3: ${first3Channels}`); const resolvedChannelName = selectedChannelName || (channels[0] && channels[0].name) || ''; + const resolvedChannel = channels.find((channel) => channel.name === resolvedChannelName) || null; + const selectedChannelCreatedAtTs = resolvedChannel ? resolvedChannel.createdAtTs : null; console.log(`[publishHomeTab] Selected channel: ${resolvedChannelName || '(none)'}`); if (logger) { @@ -1544,7 +1628,6 @@ async function publishHomeTab({ client, userId, teamId, workspaceName, logger, a apiStatus, dbName, summaries, - availableWeeks, summaryRecords, messagesSummarized, errorMessage: weeklySummaryErrorMessage @@ -1557,6 +1640,7 @@ async function publishHomeTab({ client, userId, teamId, workspaceName, logger, a errorMessage: userSummaryErrorMessage } = userSummaryResult; const errorMessage = weeklySummaryErrorMessage || userSummaryErrorMessage; + const availableWeeks = getAvailableWeeksFromChannelCreation(selectedChannelCreatedAtTs); if (logger) { logger.info(`[publishHomeTab] Summaries fetched - Records: ${summaryRecords}, Messages: ${messagesSummarized}, Status: ${apiStatus}`); @@ -1569,6 +1653,7 @@ async function publishHomeTab({ client, userId, teamId, workspaceName, logger, a workspaceName: resolvedWorkspaceName, channelOptions, selectedChannelName: resolvedChannelName, + selectedChannelCreatedAtTs, selectedWeek: availableWeeks.includes(selectedWeek) ? selectedWeek : availableWeeks[0], selectedUser: selectedUserId || selectedUser, selectedUserSummary, @@ -1631,6 +1716,7 @@ module.exports = { HOME_CHANNEL_SELECT_ACTION_ID, HOME_REFRESH_ACTION_ID, HOME_SUMMARY_WEEK_SELECT_ACTION_ID, + HOME_GENERATE_SELECTED_WEEK_SUMMARY_ACTION_ID, HOME_USER_SUMMARY_SELECT_ACTION_ID, HOME_GENERATE_USER_SUMMARIES_ACTION_ID, HOME_GENERATE_SINGLE_USER_SUMMARY_ACTION_ID, diff --git a/mongo_storage/models/Message.js b/mongo_storage/models/Message.js index 7cb2dbe3d..5fb31fb65 100644 --- a/mongo_storage/models/Message.js +++ b/mongo_storage/models/Message.js @@ -3,10 +3,10 @@ const { Schema } = require("mongoose"); const msgSchema = new Schema( { - user: String, - type: String, - text: String, - ts: String + user: { type: String, required: true }, + type: { type: String, required: true }, + text: { type: String, required: true }, + ts: { type: String, required: true } }, { strict: false } // Ensures all raw fields are logged ); diff --git a/mongo_storage/routes/messages.js b/mongo_storage/routes/messages.js index fb927f059..42d396541 100644 --- a/mongo_storage/routes/messages.js +++ b/mongo_storage/routes/messages.js @@ -177,6 +177,18 @@ router.post('/api/messages/:channelName', async (req, res) => { }); } + const requiredFields = ['user', 'type', 'text', 'ts']; + const missingFields = requiredFields.filter((field) => { + const value = bodyData[field]; + return value === undefined || value === null || String(value).trim() === ''; + }); + + if (missingFields.length > 0) { + return res.status(400).json({ + error: `Missing required message fields: ${missingFields.join(', ')}`, + }); + } + // If single message, check for duplicate by message identity (user + ts) const existingMessage = await MessageModel.findOne({ user: bodyData.user, ts: bodyData.ts }); @@ -185,12 +197,7 @@ router.post('/api/messages/:channelName', async (req, res) => { return res.status(200).json({ message: 'Message already exists in database', duplicate: true }); } - const newMessage = new MessageModel({ - user: bodyData.user, - type: bodyData.type || 'message', - text: bodyData.text, - ts: bodyData.ts - }); + const newMessage = new MessageModel(bodyData); await newMessage.save(); console.log(`Single message stored to ${channelName} database`);