Add premium My Case MVP
This commit is contained in:
+244
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
require_once __DIR__ . '/includes/FreeTier.php';
|
||||
require_once __DIR__ . '/includes/CaseStore.php';
|
||||
require_once __DIR__ . '/includes/CaseResults.php';
|
||||
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$userId = (int)($_SESSION['dbn_tools_sso_uid'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
header('Location: /dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$result = $id > 0 ? CaseResults::get($userId, $id) : null;
|
||||
|
||||
if (!$result) {
|
||||
http_response_code(404);
|
||||
?><!doctype html><html lang="<?= htmlspecialchars($uiLang) ?>"><head>
|
||||
<meta charset="utf-8"><title>Ikke funnet — Min Sak</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
|
||||
<link rel="stylesheet" href="assets/css/tools.css">
|
||||
</head><body><main style="max-width:720px;margin:4rem auto;padding:0 1.5rem;text-align:center;font-family:'IBM Plex Sans',sans-serif;">
|
||||
<h1 style="font-family:'Crimson Pro',serif;color:#00205B;">Analysen finnes ikke</h1>
|
||||
<p>Den lagrede analysen ble ikke funnet, eller du har ikke tilgang til den.</p>
|
||||
<p><a href="/min-sak.php" style="color:#00205B;font-weight:600;">← Tilbake til Min Sak</a></p>
|
||||
</main></body></html><?php
|
||||
exit;
|
||||
}
|
||||
|
||||
$toolSlug = (string)$result['tool'];
|
||||
$toolLabel = CaseResults::toolLabel($toolSlug);
|
||||
$toolIcon = CaseResults::toolIcon($toolSlug);
|
||||
$title = (string)($result['title'] ?? $toolLabel);
|
||||
$createdAt = (string)$result['created_at'];
|
||||
$usedCase = !empty($result['used_case_context']);
|
||||
$caseDocIds = is_array($result['case_doc_ids'] ?? null) ? $result['case_doc_ids'] : [];
|
||||
$input = is_array($result['input_payload'] ?? null) ? $result['input_payload'] : [];
|
||||
$output = is_array($result['output_payload'] ?? null) ? $result['output_payload'] : [];
|
||||
|
||||
// Look up doc filenames for the chunks that contributed
|
||||
$caseDocs = [];
|
||||
if (!empty($caseDocIds)) {
|
||||
try {
|
||||
$ownerId = CaseStore::caseResolveClientId($userId);
|
||||
$allDocs = CaseStore::listDocs($ownerId);
|
||||
$byId = [];
|
||||
foreach ($allDocs as $d) {
|
||||
$byId[(int)$d['id']] = $d;
|
||||
}
|
||||
foreach ($caseDocIds as $docId) {
|
||||
if (isset($byId[(int)$docId])) {
|
||||
$caseDocs[] = $byId[(int)$docId];
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$caseDocs = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort extraction of the primary human-readable output
|
||||
$primaryOutput = '';
|
||||
foreach (['draft', 'response', 'answer', 'text', 'summary', 'markdown'] as $k) {
|
||||
if (!empty($output[$k]) && is_string($output[$k])) {
|
||||
$primaryOutput = (string)$output[$k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
?><!doctype html>
|
||||
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= htmlspecialchars($title) ?> — Min Sak</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
|
||||
<link rel="stylesheet" href="assets/css/tools.css">
|
||||
<style>
|
||||
.cr-shell { max-width: 980px; margin: 0 auto; padding: 2rem 1.5rem 4rem; font-family: 'IBM Plex Sans', sans-serif; }
|
||||
.cr-header { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 1.25rem; }
|
||||
.cr-icon { font-size: 2.4rem; line-height: 1; }
|
||||
.cr-title-row { flex: 1; min-width: 0; }
|
||||
.cr-title { font-family: 'Crimson Pro', serif; font-size: 1.9rem; margin: 0; color: #00205B; cursor: pointer; }
|
||||
.cr-title[contenteditable="true"]:focus { outline: 2px solid #00205B; outline-offset: 4px; }
|
||||
.cr-meta { color: #6b7280; margin: 0.4rem 0 0; font-size: 0.9rem; }
|
||||
.cr-tag { display: inline-block; background: #dbeafe; color: #1e3a8a; padding: 1px 8px; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
|
||||
.cr-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 1.25rem 0 2rem; }
|
||||
.cr-btn { padding: 8px 14px; border-radius: 6px; font-size: 0.9rem; font-weight: 600; border: 1px solid #00205B; background: #fff; color: #00205B; cursor: pointer; text-decoration: none; display: inline-block; }
|
||||
.cr-btn-primary { background: #00205B; color: #fff; }
|
||||
.cr-btn-danger { border-color: #b91c1c; color: #b91c1c; }
|
||||
.cr-section { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 1.5rem; margin-bottom: 1.25rem; }
|
||||
.cr-section h2 { font-family: 'Crimson Pro', serif; margin: 0 0 0.75rem; font-size: 1.3rem; color: #00205B; }
|
||||
.cr-output { white-space: pre-wrap; line-height: 1.6; color: #1f2937; font-size: 1rem; }
|
||||
.cr-output-empty { color: #6b7280; font-style: italic; }
|
||||
.cr-docs { display: grid; grid-template-columns: 1fr; gap: 0.5rem; }
|
||||
.cr-doc { padding: 0.65rem 0.85rem; background: #f9fafb; border-radius: 6px; display: flex; align-items: center; gap: 0.6rem; }
|
||||
.cr-doc-name { flex: 1; font-weight: 500; color: #1f2937; }
|
||||
details.cr-collapse > summary { cursor: pointer; color: #00205B; font-weight: 600; padding: 0.5rem 0; }
|
||||
details.cr-collapse pre { background: #f3f4f6; padding: 1rem; border-radius: 6px; overflow-x: auto; font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; max-height: 400px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="cr-shell">
|
||||
<p style="margin:0 0 1rem;color:#6b7280;font-size:0.85rem;text-transform:uppercase;letter-spacing:0.06em;">
|
||||
<a href="/min-sak.php" style="color:inherit;">← Min Sak</a>
|
||||
</p>
|
||||
|
||||
<header class="cr-header">
|
||||
<div class="cr-icon"><?= htmlspecialchars($toolIcon) ?></div>
|
||||
<div class="cr-title-row">
|
||||
<h1 class="cr-title" id="crTitle" contenteditable="false" data-id="<?= (int)$result['id'] ?>">
|
||||
<?= htmlspecialchars($title) ?>
|
||||
</h1>
|
||||
<p class="cr-meta">
|
||||
<?= htmlspecialchars($toolLabel) ?>
|
||||
· <?= htmlspecialchars(date('j. F Y, H:i', strtotime($createdAt))) ?>
|
||||
<?php if ($usedCase): ?>
|
||||
· <span class="cr-tag">Bruk min sak</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($result['model'])): ?>
|
||||
· <span style="font-family:'JetBrains Mono',monospace;font-size:0.8rem;color:#6b7280;"><?= htmlspecialchars((string)$result['model']) ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cr-actions">
|
||||
<button type="button" class="cr-btn cr-btn-primary" id="crRerun" data-tool="<?= htmlspecialchars($toolSlug) ?>" data-id="<?= (int)$result['id'] ?>">Kjør på nytt</button>
|
||||
<button type="button" class="cr-btn" id="crRename">Endre navn</button>
|
||||
<a class="cr-btn" href="/api/case/result-export.php?id=<?= (int)$result['id'] ?>" download>Eksporter JSON</a>
|
||||
<button type="button" class="cr-btn cr-btn-danger" id="crDelete" data-id="<?= (int)$result['id'] ?>">Slett</button>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($caseDocs)): ?>
|
||||
<section class="cr-section">
|
||||
<h2>Saksdokumenter brukt</h2>
|
||||
<div class="cr-docs">
|
||||
<?php foreach ($caseDocs as $d): ?>
|
||||
<div class="cr-doc">
|
||||
<div>📄</div>
|
||||
<div class="cr-doc-name"><?= htmlspecialchars((string)$d['filename']) ?></div>
|
||||
<div style="color:#6b7280;font-size:0.85rem;">
|
||||
<?php if (!empty($d['page_count'])): ?><?= (int)$d['page_count'] ?> sider<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="cr-section">
|
||||
<h2>Resultat</h2>
|
||||
<?php if ($primaryOutput !== ''): ?>
|
||||
<div class="cr-output"><?= htmlspecialchars($primaryOutput) ?></div>
|
||||
<?php else: ?>
|
||||
<p class="cr-output-empty">Dette verktøyet returnerer strukturert output — se rådata under.</p>
|
||||
<?php endif; ?>
|
||||
<details class="cr-collapse" style="margin-top:1.25rem;">
|
||||
<summary>Vis rådata (JSON)</summary>
|
||||
<pre><?= htmlspecialchars(json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}') ?></pre>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="cr-section">
|
||||
<h2>Input</h2>
|
||||
<details class="cr-collapse" open>
|
||||
<summary>Vis input som ble sendt</summary>
|
||||
<pre><?= htmlspecialchars(json_encode($input, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}') ?></pre>
|
||||
</details>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const titleEl = document.getElementById('crTitle');
|
||||
const renameBtn = document.getElementById('crRename');
|
||||
const deleteBtn = document.getElementById('crDelete');
|
||||
const rerunBtn = document.getElementById('crRerun');
|
||||
|
||||
renameBtn.addEventListener('click', () => {
|
||||
titleEl.contentEditable = 'true';
|
||||
titleEl.focus();
|
||||
document.execCommand('selectAll', false, null);
|
||||
});
|
||||
|
||||
titleEl.addEventListener('blur', async () => {
|
||||
if (titleEl.contentEditable !== 'true') return;
|
||||
titleEl.contentEditable = 'false';
|
||||
const newTitle = (titleEl.textContent || '').trim();
|
||||
if (newTitle === '') { return; }
|
||||
try {
|
||||
await fetch('/api/case/result-action.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'rename', id: <?= (int)$result['id'] ?>, title: newTitle }),
|
||||
});
|
||||
} catch (e) { /* silent */ }
|
||||
});
|
||||
|
||||
titleEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); }
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!confirm('Slette denne analysen for godt?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/case/result-action.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'delete', id: <?= (int)$result['id'] ?> }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.ok) window.location.href = '/min-sak.php';
|
||||
else alert(json.error?.message || 'Sletting feilet.');
|
||||
} catch (e) { alert('Nettverksfeil: ' + e.message); }
|
||||
});
|
||||
|
||||
// Re-run: just navigate to the tool page with a hash that the tool can pick up if it wants to.
|
||||
// For MVP, we deep-link back to the tool — the user can re-fill from input shown below.
|
||||
rerunBtn.addEventListener('click', () => {
|
||||
const tool = rerunBtn.getAttribute('data-tool');
|
||||
const path = {
|
||||
'korrespond': '/korrespond.php',
|
||||
'advocate': '/advocate.php',
|
||||
'barnevernet': '/barnevernet.php',
|
||||
'deep-research': '/deep-research.php',
|
||||
'discrepancy': '/discrepancy.php',
|
||||
'timeline': '/timeline.php',
|
||||
}[tool] || '/dashboard.php';
|
||||
window.location.href = path + '?rerun=' + <?= (int)$result['id'] ?>;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user