Mantis summary updates.
This commit is contained in:
parent
0e0d1bffb3
commit
af8cc56e79
7 changed files with 168 additions and 16 deletions
|
@ -111,7 +111,9 @@ export default defineConfig((/* ctx */) =>
|
||||||
plugins: [
|
plugins: [
|
||||||
'Dark',
|
'Dark',
|
||||||
'Notify',
|
'Notify',
|
||||||
'Dialog'
|
'Dialog',
|
||||||
|
'Loading',
|
||||||
|
'LoadingBar'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { PrismaClient } from '@prisma/client'; // Import Prisma Client
|
||||||
import { getMantisSettings, saveTicketToDatabase } from '../services/mantisDownloader.js';
|
import { getMantisSettings, saveTicketToDatabase } from '../services/mantisDownloader.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import reader from '@kenjiuno/msgreader';
|
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 MsgReader = reader.default;
|
||||||
|
|
||||||
const prisma = new PrismaClient(); // Instantiate Prisma Client
|
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;
|
export default router;
|
|
@ -4,7 +4,7 @@ import prisma from '../database.js'; // Import Prisma client
|
||||||
import { getSetting } from '../utils/settings.js';
|
import { getSetting } from '../utils/settings.js';
|
||||||
import { askGemini } from '../utils/gemini.js';
|
import { askGemini } from '../utils/gemini.js';
|
||||||
|
|
||||||
const usernameMap = {
|
export const usernameMap = {
|
||||||
credmore: 'Cameron Redmore',
|
credmore: 'Cameron Redmore',
|
||||||
dgibson: 'Dane Gibson',
|
dgibson: 'Dane Gibson',
|
||||||
egzibovskis: 'Ed Gzibovskis',
|
egzibovskis: 'Ed Gzibovskis',
|
||||||
|
|
|
@ -5,9 +5,17 @@ import { getSetting } from './settings.js';
|
||||||
|
|
||||||
const model = 'gemini-2.0-flash';
|
const model = 'gemini-2.0-flash';
|
||||||
|
|
||||||
|
const geminiResponseCache = new Map();
|
||||||
|
|
||||||
export async function askGemini(content)
|
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');
|
const GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY');
|
||||||
|
|
||||||
if (!GOOGLE_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;
|
return response.text;
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
|
|
|
@ -595,17 +595,4 @@ const openImageFullscreen = (src, filename) =>
|
||||||
max-height: 300px; /* Limit height if descriptions are long */
|
max-height: 300px; /* Limit height if descriptions are long */
|
||||||
overflow-y: auto;
|
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -26,3 +26,17 @@ body {
|
||||||
.drop-shadow {
|
.drop-shadow {
|
||||||
filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5));
|
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;
|
||||||
|
}
|
|
@ -61,6 +61,23 @@
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
<template #body-cell-actions="actionsProps">
|
||||||
|
<q-td
|
||||||
|
:props="actionsProps"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="info"
|
||||||
|
icon="summarize"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
@click.stop="showTicketSummary(actionsProps.row.id)"
|
||||||
|
:title="'Show summary for ticket ' + actionsProps.row.id"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
|
@ -69,6 +86,34 @@
|
||||||
:ticket-id="selectedTicketId"
|
:ticket-id="selectedTicketId"
|
||||||
@close="closeDialog"
|
@close="closeDialog"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- New Summary Dialog -->
|
||||||
|
<q-dialog
|
||||||
|
v-model="showSummaryDialog"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
style="max-width: max(75%, 500px)"
|
||||||
|
>
|
||||||
|
<q-card-section class="row items-center q-pb-none q-mb-md">
|
||||||
|
<div class="text-h4">
|
||||||
|
Summary for Ticket #{{ summaryTicketId }}
|
||||||
|
</div>
|
||||||
|
<q-space />
|
||||||
|
<q-btn
|
||||||
|
icon="close"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
v-close-popup
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-section class="summary-content">
|
||||||
|
<div v-html="summaryContent" />
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -79,6 +124,8 @@ import axios from 'axios';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import MantisTicketDialog from 'src/components/MantisTicketDialog.vue';
|
import MantisTicketDialog from 'src/components/MantisTicketDialog.vue';
|
||||||
|
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
ticketId: {
|
ticketId: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
|
@ -92,6 +139,9 @@ const loading = ref(false);
|
||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
const showDialog = ref(false);
|
const showDialog = ref(false);
|
||||||
const selectedTicketId = ref(null);
|
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();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -111,6 +161,7 @@ const columns = [
|
||||||
{ name: 'severity', label: 'Severity', field: 'severity', align: 'center', sortable: true },
|
{ name: 'severity', label: 'Severity', field: 'severity', align: 'center', sortable: true },
|
||||||
{ name: 'reporterUsername', label: 'Reporter', field: 'reporterUsername', align: 'left', 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: '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) =>
|
const fetchTickets = async(page = pagination.value.page) =>
|
||||||
|
@ -241,9 +292,57 @@ const getSeverityColor = (severity) =>
|
||||||
return 'primary';
|
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, '<span class="label">[$1]</span>');
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Add any specific styles here */
|
/* Add any specific styles here */
|
||||||
|
.summary-content:deep(h6) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-content:deep(span.label) {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-content:deep(span.label):after {
|
||||||
|
content: ' - ';
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue