- Added `transcription_service.py` to handle audio transcription via Whisper API. - Integrated logging for transcription processes and error handling. - Supported audio format checks based on configuration settings. docs: Create Ordre System Implementation Plan - Drafted comprehensive implementation plan for e-conomic order integration. - Outlined business requirements, database changes, backend and frontend implementation details. - Included testing plan and deployment steps for the new order system. feat: Add AI prompts and regex action capabilities - Created `ai_prompts` table for storing custom AI prompts. - Added regex extraction and linking action to email workflow actions. feat: Introduce conversations module for transcribed audio - Created `conversations` table to store transcribed conversations with relevant metadata. - Added indexing for customer, ticket, and user linkage. - Implemented full-text search capabilities for Danish language. fix: Add category column to conversations for classification - Added `category` column to `conversations` table for better conversation classification.
167 lines
7.0 KiB
HTML
167 lines
7.0 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Mine Samtaler - BMC Hub{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<h1><i class="bi bi-mic me-2"></i>Mine Optagede Samtaler</h1>
|
|
<p class="text-muted">Administrer dine telefonsamtaler og lydnotater.</p>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<div class="btn-group" role="group">
|
|
<input type="radio" class="btn-check" name="filterradio" id="btnradio1" autocomplete="off" checked onclick="filterView('all')">
|
|
<label class="btn btn-outline-primary" for="btnradio1">Alle</label>
|
|
|
|
<input type="radio" class="btn-check" name="filterradio" id="btnradio2" autocomplete="off" onclick="filterView('private')">
|
|
<label class="btn btn-outline-primary" for="btnradio2">Kun Private</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="input-group mb-4">
|
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
<input type="text" class="form-control" id="conversationSearch" placeholder="Søg i samtaler..." onkeyup="filterConversations()">
|
|
</div>
|
|
|
|
<div id="conversationsContainer">
|
|
<div class="text-center py-5">
|
|
<div class="spinner-border text-primary"></div>
|
|
<p class="mt-2 text-muted">Henter dine samtaler...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.conversation-item { transition: transform 0.2s; }
|
|
.conversation-item:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
|
|
</style>
|
|
|
|
<script>
|
|
let allConversations = [];
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadMyConversations();
|
|
});
|
|
|
|
async function loadMyConversations() {
|
|
try {
|
|
const response = await fetch('/api/v1/conversations?only_mine=true');
|
|
if (!response.ok) throw new Error('Fejl');
|
|
|
|
allConversations = await response.json();
|
|
renderConversations(allConversations);
|
|
|
|
} catch(e) {
|
|
document.getElementById('conversationsContainer').innerHTML =
|
|
'<div class="alert alert-danger">Kunne ikke hente samtaler</div>';
|
|
}
|
|
}
|
|
|
|
function renderConversations(list) {
|
|
if(list.length === 0) {
|
|
document.getElementById('conversationsContainer').innerHTML =
|
|
'<div class="text-center py-5 text-muted">Ingen samtaler fundet</div>';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('conversationsContainer').innerHTML = list.map(c => `
|
|
<div class="card mb-3 conversation-item ${c.is_private ? 'border-warning' : ''}" data-type="${c.is_private ? 'private' : 'public'}" data-text="${(c.transcript||'').toLowerCase()} ${(c.title||'').toLowerCase()}">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h5 class="card-title fw-bold">
|
|
${c.is_private ? '<i class="bi bi-lock-fill text-warning"></i> ' : ''}
|
|
${c.title}
|
|
</h5>
|
|
<p class="card-text text-muted small mb-2">
|
|
${new Date(c.created_at).toLocaleString()}
|
|
${c.customer_id ? `• Customer #${c.customer_id}` : ''}
|
|
</p>
|
|
|
|
<div class="mb-2" style="max-width: 150px;">
|
|
<select class="form-select form-select-sm py-0" style="font-size: 0.8rem;" onchange="updateCategory(${c.id}, this.value)">
|
|
<option value="General" ${(!c.category || c.category === 'General') ? 'selected' : ''}>Generelt</option>
|
|
<option value="Support" ${c.category === 'Support' ? 'selected' : ''}>Support</option>
|
|
<option value="Sales" ${c.category === 'Sales' ? 'selected' : ''}>Salg</option>
|
|
<option value="Internal" ${c.category === 'Internal' ? 'selected' : ''}>Internt</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="togglePrivacy(${c.id}, ${!c.is_private})">
|
|
${c.is_private ? 'Gør Offentlig' : 'Gør Privat'}
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger ms-2" onclick="deleteConversation(${c.id})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<audio controls class="w-100 my-2 bg-light rounded">
|
|
<source src="/api/v1/conversations/${c.id}/audio" type="audio/mpeg">
|
|
</audio>
|
|
|
|
${c.transcript ? `
|
|
<details>
|
|
<summary class="text-primary" style="cursor:pointer">Vis Transskription</summary>
|
|
<div class="mt-2 p-3 bg-light rounded font-monospace small">${c.transcript}</div>
|
|
</details>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function filterView(type) {
|
|
const items = document.querySelectorAll('.conversation-item');
|
|
items.forEach(item => {
|
|
if (type === 'all') item.style.display = 'block';
|
|
else if (type === 'private') item.style.display = item.dataset.type === 'private' ? 'block' : 'none';
|
|
});
|
|
}
|
|
|
|
function filterConversations() {
|
|
const query = document.getElementById('conversationSearch').value.toLowerCase();
|
|
const items = document.querySelectorAll('.conversation-item');
|
|
items.forEach(item => {
|
|
const text = item.dataset.text;
|
|
item.style.display = text.includes(query) ? 'block' : 'none';
|
|
});
|
|
}
|
|
|
|
async function togglePrivacy(id, makePrivate) {
|
|
await fetch(\`/api/v1/conversations/\${id}\`, {
|
|
method: 'PATCH',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({is_private: makePrivate})
|
|
});
|
|
loadMyConversations();
|
|
}
|
|
|
|
async function deleteConversation(id) {
|
|
if(!confirm('Vil du slette denne samtale?')) return;
|
|
const hard = confirm('Skal dette være en permanent sletning af fil og data? (Kan ikke fortrydes)');
|
|
await fetch(\`/api/v1/conversations/\${id}?hard_delete=\${hard}\`, { method: 'DELETE' });
|
|
loadMyConversations();
|
|
}
|
|
async function updateCategory(id, newCategory) {
|
|
try {
|
|
const response = await fetch(`/api/v1/conversations/${id}`, {
|
|
method: 'PATCH',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({category: newCategory})
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Update failed');
|
|
} catch (e) {
|
|
alert("Kunne ikke opdatere kategori");
|
|
console.error(e);
|
|
loadMyConversations(); // Revert UI on error
|
|
}
|
|
}</script>
|
|
{% endblock %}
|