479 lines
22 KiB
PHP
479 lines
22 KiB
PHP
@extends('layouts.dashboard')
|
|
|
|
@section('content')
|
|
<div class="toolbar py-5 py-lg-15" id="kt_toolbar">
|
|
<div id="kt_toolbar_container" class="container-xxl d-flex flex-stack flex-wrap">
|
|
<div class="page-title d-flex flex-column me-3">
|
|
<h1 class="d-flex text-white fw-bolder my-1 fs-3">Dashboard</h1>
|
|
<ul class="breadcrumb breadcrumb-separatorless fw-bold fs-7 my-1">
|
|
<li class="breadcrumb-item text-white opacity-75">
|
|
<a href="{{ route('dashboard.index', ['id' => 1]) }}" class="text-white text-hover-primary">Home</a>
|
|
</li>
|
|
<li class="breadcrumb-item">
|
|
<span class="bullet bg-white opacity-75 w-5px h-2px"></span>
|
|
</li>
|
|
<li class="breadcrumb-item text-white opacity-75">Dashboard</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="kt_content_container" class="d-flex flex-column-fluid align-items-start container-xxl">
|
|
<div class="content flex-row-fluid" id="kt_content">
|
|
<div class="row g-5 g-xxl-8">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<form action="{{ route('dashboard.entry.upload') }}" class="dropzone" id="journal-dropzone"
|
|
enctype="multipart/form-data">
|
|
@csrf
|
|
<div class="dz-message" data-dz-message><span>Drop journal PDF here or click to upload.</span>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-5">
|
|
<div class="card-body">
|
|
<!-- Keterangan warna node -->
|
|
<div class="mb-3">
|
|
<strong>Keterangan Warna Node:</strong>
|
|
<span
|
|
style="background:#1E90FF; color:#fff; padding:2px 8px; border-radius:4px; margin-right:8px;">Keyword
|
|
</span>
|
|
<span
|
|
style="background:#FF6347; color:#fff; padding:2px 8px; border-radius:4px; margin-right:8px;">Article
|
|
</span>
|
|
<span
|
|
style="background:#32CD32; color:#fff; padding:2px 8px; border-radius:4px; margin-right:8px;">Author
|
|
</span>
|
|
<span
|
|
style="background:#9370DB; color:#fff; padding:2px 8px; border-radius:4px; margin-right:8px;">Affiliation
|
|
</span>
|
|
<span
|
|
style="background:#FFA500; color:#fff; padding:2px 8px; border-radius:4px; margin-right:8px;">Journal
|
|
</span>
|
|
<span
|
|
style="background:#708090; color:#fff; padding:2px 8px; border-radius:4px; margin-right:8px;">DOI
|
|
</span>
|
|
<span
|
|
style="background:#00CED1; color:#fff; padding:2px 8px; border-radius:4px; margin-right:8px;">Open
|
|
</span>
|
|
</div>
|
|
|
|
<div id="cy" style="width: 100%; height: 600px; border: 1px solid #ccc;"></div>
|
|
<div id="article-grid" class="mt-5">
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="titleModal" tabindex="-1" aria-labelledby="titleModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<form id="titleForm">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="titleModalLabel">Isi Judul Artikel</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" class="form-control" id="articleTitle" name="title"
|
|
placeholder="Masukkan judul artikel" required>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="submit" class="btn btn-primary">Kirim Analisis</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
@endsection
|
|
|
|
@push('js')
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
|
|
<!-- jQuery dulu -->
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
|
|
<!-- qTip2 CSS dan JS -->
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/qtip2/3.0.3/jquery.qtip.min.css" rel="stylesheet" />
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qtip2/3.0.3/jquery.qtip.min.js"></script>
|
|
|
|
<!-- Cytoscape -->
|
|
<script src="https://unpkg.com/cytoscape@3.24.0/dist/cytoscape.min.js"></script>
|
|
|
|
<!-- cytoscape-qtip plugin -->
|
|
<script src="https://cdn.jsdelivr.net/npm/cytoscape-qtip@2.7.0/cytoscape-qtip.min.js"></script>
|
|
|
|
|
|
<script>
|
|
let uploadedFile = null;
|
|
|
|
Dropzone.options.journalDropzone = {
|
|
paramName: 'file',
|
|
maxFilesize: 5, // MB
|
|
acceptedFiles: '.pdf',
|
|
dictDefaultMessage: 'Drop journal PDF here or click to upload',
|
|
sending: function (file, xhr, formData) {
|
|
Swal.fire({
|
|
title: 'Uploading...',
|
|
text: 'Mohon tunggu, jurnal sedang diproses.',
|
|
allowOutsideClick: false,
|
|
didOpen: () => {
|
|
Swal.showLoading();
|
|
}
|
|
});
|
|
},
|
|
success: function (file, response) {
|
|
Swal.close();
|
|
if (response.keywords && response.results) {
|
|
renderGraph(response.keywords, response.results);
|
|
uploadedFile = file;
|
|
|
|
let titleModal = new bootstrap.Modal(document.getElementById('titleModal'));
|
|
titleModal.show();
|
|
} else {
|
|
alert('Response tidak mengandung data yang valid!');
|
|
}
|
|
},
|
|
error: function (file, response) {
|
|
Swal.close();
|
|
alert("Upload error: " + (response.error || response));
|
|
}
|
|
};
|
|
|
|
|
|
function normalize(text) {
|
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
}
|
|
|
|
function renderGraph(keywords, results) {
|
|
document.getElementById('cy').innerHTML = '';
|
|
|
|
let nodes = [];
|
|
let edges = [];
|
|
let nodeIds = new Set();
|
|
|
|
function truncateLabel(label) {
|
|
return label && label.length > 30 ? label.substr(0, 27) + '...' : label;
|
|
}
|
|
|
|
function getNodeSizeByText(text, minSize = 40, maxSize = 150, charMultiplier = 6) {
|
|
const length = text.length;
|
|
return Math.min(maxSize, Math.max(minSize, length * charMultiplier));
|
|
}
|
|
|
|
keywords.forEach((kw, i) => {
|
|
let id = 'kw' + i;
|
|
let size = getNodeSizeByText(kw);
|
|
nodes.push({
|
|
data: { id, label: truncateLabel(kw), fullLabel: kw, type: 'keyword' },
|
|
style: { width: size, height: size }
|
|
});
|
|
nodeIds.add(id);
|
|
});
|
|
|
|
results.forEach((article, i) => {
|
|
let artId = 'art' + i;
|
|
let title = article['dc:title'] || article.title || 'Judul tidak diketahui';
|
|
let artSize = getNodeSizeByText(title, 40, 100, 4);
|
|
|
|
if (!nodeIds.has(artId)) {
|
|
nodes.push({
|
|
data: { id: artId, label: truncateLabel(title), fullLabel: title, type: 'article' },
|
|
style: { width: artSize, height: artSize }
|
|
});
|
|
nodeIds.add(artId);
|
|
}
|
|
|
|
keywords.forEach((kw, j) => {
|
|
if (normalize(title).includes(normalize(kw))) {
|
|
edges.push({
|
|
data: { id: `e_kw${j}_art${i}`, source: 'kw' + j, target: artId }
|
|
});
|
|
}
|
|
});
|
|
|
|
if (article['dc:creator']) {
|
|
let authors = Array.isArray(article['dc:creator']) ? article['dc:creator'] : [article['dc:creator']];
|
|
authors.forEach((authorName, idx) => {
|
|
let authId = `auth${i}_${idx}`;
|
|
let authSize = getNodeSizeByText(authorName);
|
|
|
|
if (!nodeIds.has(authId)) {
|
|
nodes.push({
|
|
data: { id: authId, label: truncateLabel(authorName), fullLabel: authorName, type: 'author' },
|
|
style: { width: authSize, height: authSize }
|
|
});
|
|
nodeIds.add(authId);
|
|
}
|
|
|
|
edges.push({
|
|
data: { id: `e_art${i}_auth${i}_${idx}`, source: artId, target: authId, label: 'written by' }
|
|
});
|
|
|
|
if (article.affiliation && article.affiliation.length > 0) {
|
|
article.affiliation.forEach((affil, affilIdx) => {
|
|
let affilName = affil.affilname || 'Unknown Affiliation';
|
|
let affilId = `affil${i}_${affilIdx}`;
|
|
let affilSize = getNodeSizeByText(affilName);
|
|
|
|
if (!nodeIds.has(affilId)) {
|
|
nodes.push({
|
|
data: { id: affilId, label: truncateLabel(affilName), fullLabel: affilName, type: 'affiliation' },
|
|
style: { width: affilSize, height: affilSize }
|
|
});
|
|
nodeIds.add(affilId);
|
|
}
|
|
|
|
edges.push({
|
|
data: { id: `e_auth${i}_${idx}_affil${i}_${affilIdx}`, source: authId, target: affilId, label: 'affiliated with' }
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
if (article['prism:publicationName']) {
|
|
let journalName = article['prism:publicationName'];
|
|
let journalId = `journal${i}`;
|
|
let journalSize = getNodeSizeByText(journalName, 40, 100, 4);
|
|
|
|
if (!nodeIds.has(journalId)) {
|
|
nodes.push({
|
|
data: { id: journalId, label: truncateLabel(journalName), fullLabel: journalName, type: 'journal' },
|
|
style: { width: journalSize, height: journalSize }
|
|
});
|
|
nodeIds.add(journalId);
|
|
}
|
|
|
|
edges.push({
|
|
data: { id: `e_art${i}_journal${i}`, source: artId, target: journalId, label: 'published in' }
|
|
});
|
|
}
|
|
|
|
if (article['prism:doi']) {
|
|
let doi = article['prism:doi'];
|
|
let doiId = `doi${i}`;
|
|
let doiSize = getNodeSizeByText(doi, 40, 80, 5);
|
|
|
|
if (!nodeIds.has(doiId)) {
|
|
nodes.push({
|
|
data: { id: doiId, label: truncateLabel(doi), fullLabel: doi, type: 'doi' },
|
|
style: { width: doiSize, height: 30 }
|
|
});
|
|
nodeIds.add(doiId);
|
|
}
|
|
|
|
edges.push({
|
|
data: { id: `e_art${i}_doi${i}`, source: artId, target: doiId, label: 'has DOI' }
|
|
});
|
|
}
|
|
|
|
if (article.openaccessFlag) {
|
|
let oaId = `oa${i}`;
|
|
if (!nodeIds.has(oaId)) {
|
|
nodes.push({
|
|
data: { id: oaId, label: 'Open Access', fullLabel: 'Open Access', type: 'openaccess' },
|
|
style: { width: 100, height: 30 }
|
|
});
|
|
nodeIds.add(oaId);
|
|
}
|
|
edges.push({
|
|
data: { id: `e_art${i}_oa${i}`, source: artId, target: oaId, label: 'open access' }
|
|
});
|
|
}
|
|
});
|
|
|
|
let cy = cytoscape({
|
|
container: document.getElementById('cy'),
|
|
elements: { nodes, edges },
|
|
style: [
|
|
{
|
|
selector: 'node',
|
|
style: {
|
|
label: 'data(label)',
|
|
color: '#fff',
|
|
'text-valign': 'center',
|
|
'text-halign': 'center',
|
|
'font-size': '10px',
|
|
'text-wrap': 'wrap',
|
|
'text-max-width': '100px'
|
|
}
|
|
},
|
|
{ selector: 'node[type="keyword"]', style: { 'background-color': '#1E90FF', 'shape': 'roundrectangle', 'border-color': '#0a53b6', 'border-width': 2 } },
|
|
{ selector: 'node[type="article"]', style: { 'background-color': '#FF6347', 'shape': 'ellipse' } },
|
|
{ selector: 'node[type="author"]', style: { 'background-color': '#32CD32', 'shape': 'ellipse' } },
|
|
{ selector: 'node[type="affiliation"]', style: { 'background-color': '#9370DB', 'shape': 'roundrectangle' } },
|
|
{ selector: 'node[type="journal"]', style: { 'background-color': '#FFA500', 'shape': 'triangle' } },
|
|
{ selector: 'node[type="doi"]', style: { 'background-color': '#708090', 'shape': 'roundrectangle' } },
|
|
{ selector: 'node[type="openaccess"]', style: { 'background-color': '#00CED1', 'shape': 'rectangle' } },
|
|
{
|
|
selector: 'edge',
|
|
style: {
|
|
'width': 1,
|
|
'line-color': '#bbb',
|
|
'target-arrow-color': '#bbb',
|
|
'target-arrow-shape': 'triangle',
|
|
'curve-style': 'bezier',
|
|
'label': 'data(label)',
|
|
'font-size': '7px',
|
|
'text-rotation': 'autorotate',
|
|
'color': '#555'
|
|
}
|
|
}
|
|
],
|
|
layout: {
|
|
name: 'cose',
|
|
idealEdgeLength: 100,
|
|
nodeOverlap: 20,
|
|
refresh: 20,
|
|
fit: true,
|
|
padding: 30,
|
|
randomize: true,
|
|
componentSpacing: 100,
|
|
nodeRepulsion: 400000,
|
|
edgeElasticity: 100,
|
|
nestingFactor: 5,
|
|
gravity: 80,
|
|
numIter: 1000,
|
|
initialTemp: 200,
|
|
coolingFactor: 0.95,
|
|
minTemp: 1.0
|
|
}
|
|
});
|
|
|
|
cy.nodes().forEach(ele => {
|
|
if (ele.data('type') === 'doi') {
|
|
let doi = ele.data('fullLabel');
|
|
let doiUrl = doi.startsWith('10.') ? 'https://doi.org/' + doi : '#';
|
|
ele.qtip({
|
|
content: `<a href="${doiUrl}" target="_blank" style="color:#00f; text-decoration:underline;">${doi}</a>`,
|
|
position: { my: 'top center', at: 'bottom center' },
|
|
style: { classes: 'qtip-bootstrap', tip: { width: 10, height: 8 } },
|
|
show: { event: 'mouseover' },
|
|
hide: { event: 'mouseout' }
|
|
});
|
|
ele.on('tap', () => window.open(doiUrl, '_blank'));
|
|
} else {
|
|
ele.qtip({
|
|
content: ele.data('fullLabel'),
|
|
position: { my: 'top center', at: 'bottom center' },
|
|
style: { classes: 'qtip-bootstrap', tip: { width: 10, height: 8 } }
|
|
});
|
|
}
|
|
});
|
|
|
|
let gridContainer = document.getElementById('article-grid');
|
|
gridContainer.innerHTML = '';
|
|
|
|
let gridHTML = `
|
|
<h4>Hasil Pencarian Artikel</h4>
|
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
|
`;
|
|
|
|
results.forEach((article, i) => {
|
|
let title = article['dc:title'] || article.title || 'Judul tidak diketahui';
|
|
let authors = Array.isArray(article['dc:creator']) ? article['dc:creator'].join(', ') : article['dc:creator'] || 'Penulis tidak diketahui';
|
|
let journal = article['prism:publicationName'] || 'Jurnal tidak diketahui';
|
|
let doi = article['prism:doi'] ? `<a href="https://doi.org/${article['prism:doi']}" target="_blank">${article['prism:doi']}</a>` : '—';
|
|
|
|
gridHTML += `
|
|
<div class="col">
|
|
<div class="card shadow-sm h-100">
|
|
<div class="card-body">
|
|
<h6 class="card-title fw-bold">${title}</h6>
|
|
<p class="mb-1"><strong>Author:</strong> ${authors}</p>
|
|
<p class="mb-1"><strong>Journal:</strong> ${journal}</p>
|
|
<p class="mb-0"><strong>DOI:</strong> ${doi}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
gridHTML += `</div>`;
|
|
gridContainer.innerHTML = gridHTML;
|
|
|
|
|
|
document.getElementById('titleForm').addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
|
|
const titleInput = document.getElementById('articleTitle').value.trim();
|
|
if (!titleInput) {
|
|
alert('Judul artikel wajib diisi!');
|
|
return;
|
|
}
|
|
|
|
if (!uploadedFile) {
|
|
alert('File tidak ditemukan, silakan upload ulang.');
|
|
return;
|
|
}
|
|
|
|
// Buat FormData baru untuk POST
|
|
let formData = new FormData();
|
|
formData.append('file', uploadedFile);
|
|
formData.append('title', titleInput);
|
|
|
|
// Tampilkan Swal loading sebelum kirim
|
|
Swal.fire({
|
|
title: 'Mengirim data...',
|
|
text: 'Mohon tunggu, sedang memproses analisis jurnal.',
|
|
allowOutsideClick: false,
|
|
didOpen: () => {
|
|
Swal.showLoading();
|
|
}
|
|
});
|
|
|
|
fetch('{{ route("dashboard.journal.store") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
|
},
|
|
body: formData
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) throw new Error('Network response was not ok');
|
|
return res.json();
|
|
})
|
|
.then(data => {
|
|
Swal.close();
|
|
|
|
let modalEl = document.getElementById('titleModal');
|
|
let modal = bootstrap.Modal.getInstance(modalEl);
|
|
modal.hide();
|
|
|
|
Swal.fire({
|
|
title: 'Sukses!',
|
|
text: data.message || 'Analisis jurnal berhasil dikirim.',
|
|
icon: 'success',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Lihat Hasil',
|
|
cancelButtonText: 'Tutup'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
if (data.journal_id) {
|
|
window.open(`{{ url('dashboard/journal') }}/${data.journal_id}`, '_blank');
|
|
}
|
|
}
|
|
});
|
|
})
|
|
|
|
.catch(err => {
|
|
Swal.close();
|
|
Swal.fire('Error!', 'Terjadi kesalahan saat mengirim data.', 'error');
|
|
});
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
@endpush
|