From af8cc56e79ec5313337fd3e3a37baff1cd50148d Mon Sep 17 00:00:00 2001 From: Cameron Redmore Date: Sat, 26 Apr 2025 10:53:07 +0100 Subject: [PATCH] Mantis summary updates. --- quasar.config.js | 4 +- src-server/routes/mantis.js | 41 +++++++++- src-server/services/mantisSummarizer.js | 2 +- src-server/utils/gemini.js | 11 +++ src/components/MantisTicketDialog.vue | 13 ---- src/css/app.scss | 14 ++++ src/pages/MantisPage.vue | 99 +++++++++++++++++++++++++ 7 files changed, 168 insertions(+), 16 deletions(-) diff --git a/quasar.config.js b/quasar.config.js index ec3f55e..ecb88ad 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -111,7 +111,9 @@ export default defineConfig((/* ctx */) => plugins: [ 'Dark', 'Notify', - 'Dialog' + 'Dialog', + 'Loading', + 'LoadingBar' ] }, diff --git a/src-server/routes/mantis.js b/src-server/routes/mantis.js index 479a0b3..04eafdf 100644 --- a/src-server/routes/mantis.js +++ b/src-server/routes/mantis.js @@ -3,7 +3,8 @@ import { PrismaClient } from '@prisma/client'; // Import Prisma Client import { getMantisSettings, saveTicketToDatabase } from '../services/mantisDownloader.js'; import axios from 'axios'; import reader from '@kenjiuno/msgreader'; -import SuperJSON from 'superjson'; +import { askGemini } from '../utils/gemini.js'; +import { usernameMap } from '../services/mantisSummarizer.js'; const MsgReader = reader.default; const prisma = new PrismaClient(); // Instantiate Prisma Client @@ -356,4 +357,42 @@ router.get('/stats/comments', async(req, res) => } }); +router.get('/summary/:ticketId', async(req, res) => +{ + const { ticketId } = req.params; + + try + { + const ticket = await prisma.mantisIssue.findUnique({ + where: { id: parseInt(ticketId, 10) }, + include: { + comments: true, + }, + }); + + if (!ticket) + { + return res.status(404).json({ error: 'Mantis ticket not found' }); + } + + //We need to change the usernames using the usernameMap + ticket.reporterUsername = usernameMap[ticket.reporterUsername] || ticket.reporterUsername; + ticket.comments = ticket.comments.map((comment) => + { + comment.senderUsername = usernameMap[comment.senderUsername] || comment.senderUsername; + return comment; + }); + + //Ask Gemini to summarize the ticket + const summary = await askGemini(`Please summarize the following Mantis ticket in the form of a markdown list of bullet points formatted as "[Date] Point" (ensure a newline between each point, format the date as DD/MM/YYY and surround it with square brackets "[]"). Then after your summary, list any outstanding actions as a markdown list in the format "[Name] Action" (again surrounding the name with square brackets). + Output a heading 6 "Summary:", a newline, the summary, then two newlines, a heading 6 "Actions:" then the actions. Do not wrap the output in a code block.\n\n### Ticket Data ###\n\n` + JSON.stringify(ticket, null, 2)); + res.status(200).json({ summary }); + } + catch (error) + { + console.error('Error fetching Mantis summary:', error.message); + res.status(500).json({ error: 'Failed to fetch Mantis summary' }); + } +}); + export default router; \ No newline at end of file diff --git a/src-server/services/mantisSummarizer.js b/src-server/services/mantisSummarizer.js index f1857ac..5217cd3 100644 --- a/src-server/services/mantisSummarizer.js +++ b/src-server/services/mantisSummarizer.js @@ -4,7 +4,7 @@ import prisma from '../database.js'; // Import Prisma client import { getSetting } from '../utils/settings.js'; import { askGemini } from '../utils/gemini.js'; -const usernameMap = { +export const usernameMap = { credmore: 'Cameron Redmore', dgibson: 'Dane Gibson', egzibovskis: 'Ed Gzibovskis', diff --git a/src-server/utils/gemini.js b/src-server/utils/gemini.js index a7ad61c..f0f75dd 100644 --- a/src-server/utils/gemini.js +++ b/src-server/utils/gemini.js @@ -5,9 +5,17 @@ import { getSetting } from './settings.js'; const model = 'gemini-2.0-flash'; +const geminiResponseCache = new Map(); + export async function askGemini(content) { + // Check if the content is already cached + if (geminiResponseCache.has(content)) + { + return geminiResponseCache.get(content); + } + const GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY'); if (!GOOGLE_API_KEY) @@ -34,6 +42,9 @@ export async function askGemini(content) } }); + // Cache the response + geminiResponseCache.set(content, response.text); + return response.text; } catch (error) diff --git a/src/components/MantisTicketDialog.vue b/src/components/MantisTicketDialog.vue index da3ca85..a08c53c 100644 --- a/src/components/MantisTicketDialog.vue +++ b/src/components/MantisTicketDialog.vue @@ -595,17 +595,4 @@ const openImageFullscreen = (src, filename) => max-height: 300px; /* Limit height if descriptions are long */ overflow-y: auto; } - -a, .comment-text:deep(a) { - color: $primary; - text-decoration: none; -} - -a:hover, .comment-text:deep(a:hover) { - text-decoration: underline; -} - -a:visited, .comment-text:deep(a:visited) { - color: $blue-5; -} diff --git a/src/css/app.scss b/src/css/app.scss index ed0bd1b..57313cb 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -25,4 +25,18 @@ body { .drop-shadow { filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5)); +} + + +a { + color: $primary; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +a:visited { + color: $blue-5; } \ No newline at end of file diff --git a/src/pages/MantisPage.vue b/src/pages/MantisPage.vue index 9fff864..43d715c 100644 --- a/src/pages/MantisPage.vue +++ b/src/pages/MantisPage.vue @@ -61,6 +61,23 @@ /> + @@ -69,6 +86,34 @@ :ticket-id="selectedTicketId" @close="closeDialog" /> + + + + + +
+ Summary for Ticket #{{ summaryTicketId }} +
+ + +
+ + + +
+ + + @@ -79,6 +124,8 @@ import axios from 'axios'; import { useQuasar } from 'quasar'; import MantisTicketDialog from 'src/components/MantisTicketDialog.vue'; +import { marked } from 'marked'; + const props = defineProps({ ticketId: { type: [String, Number], @@ -92,6 +139,9 @@ const loading = ref(false); const searchTerm = ref(''); const showDialog = ref(false); const selectedTicketId = ref(null); +const showSummaryDialog = ref(false); // New state for summary dialog +const summaryContent = ref(''); // New state for summary content +const summaryTicketId = ref(null); // New state for summary ticket ID const router = useRouter(); @@ -111,6 +161,7 @@ const columns = [ { name: 'severity', label: 'Severity', field: 'severity', align: 'center', sortable: true }, { name: 'reporterUsername', label: 'Reporter', field: 'reporterUsername', align: 'left', sortable: true }, { name: 'updatedAt', label: 'Last Updated', field: 'updatedAt', align: 'left', sortable: true, format: (val) => new Date(val).toLocaleString() }, + { name: 'actions', label: 'Actions', field: 'id', align: 'center' }, ]; const fetchTickets = async(page = pagination.value.page) => @@ -241,9 +292,57 @@ const getSeverityColor = (severity) => return 'primary'; }; +// Function to fetch and display ticket summary +const showTicketSummary = async(ticketId) => +{ + $q.loading.show({ + message: 'Generating Ticket Summary...', + // spinner: QSpinnerGears, + spinnerColor: 'white' + }); + if (!ticketId) + { + $q.loading.hide(); // Hide loading if no ticketId + return; + } + try + { + const response = await axios.get(`/api/mantis/summary/${ticketId}`); + + // Store summary data and show the dialog + summaryContent.value = marked(response.data?.summary || 'No summary available'); + //Turn anything surrounded by `[]` into a span with a class of "label" + summaryContent.value = summaryContent.value.replace(/\[(.*?)\]/g, '[$1]'); + summaryTicketId.value = ticketId; + showSummaryDialog.value = true; + } + catch (error) + { + console.error('Error fetching ticket summary:', error); + $q.notify({ + type: 'negative', + message: `Failed to fetch summary for ticket #${ticketId}: ${error.response?.data?.message || error.message}` + }); + } + finally + { + $q.loading.hide(); + } +};