Fix Bedrock advocate synthesis: engine guard, response_format, Claude engine option
- DeepResearchAgent: engine guard now accepts dbn_legal_v3, claude_sonnet, claude_haiku
(previously stripped these to azure_mini, breaking dbn_legal_v3 selection and
preventing Claude engines from reaching the correct synthesis branch)
- DbnBedrockGateway: remove response_format=json_object from chat payload — LiteLLM
converts this to a tool-use constraint for Bedrock, routing output into tool_calls
instead of content (root cause of the {} empty brief)
- advocate.php: add Claude Sonnet 4.6 (AWS Bedrock) engine option
- account, billing, dashboard, nav, min-sak: pending UI/flow changes from prior sessions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1173
-163
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -56,8 +56,9 @@ require_once __DIR__ . '/includes/layout.php';
|
|||||||
<label><input type="radio" name="advEngine" value="azure_full"> Azure gpt-4o <small class="control-hint">(best · ~60-180s)</small></label>
|
<label><input type="radio" name="advEngine" value="azure_full"> Azure gpt-4o <small class="control-hint">(best · ~60-180s)</small></label>
|
||||||
<label><input type="radio" name="advEngine" value="gpu"> GPU (cuttlefish) <small class="control-hint">(local · ~30-90s)</small></label>
|
<label><input type="radio" name="advEngine" value="gpu"> GPU (cuttlefish) <small class="control-hint">(local · ~30-90s)</small></label>
|
||||||
<label><input type="radio" name="advEngine" value="dbn_legal_v3"> 🇳🇴⚔️ DBN Legal Agent ★ <small class="control-hint">(dbn-legal-agent-v3 fine-tune · ~20-60s)</small></label>
|
<label><input type="radio" name="advEngine" value="dbn_legal_v3"> 🇳🇴⚔️ DBN Legal Agent ★ <small class="control-hint">(dbn-legal-agent-v3 fine-tune · ~20-60s)</small></label>
|
||||||
|
<label><input type="radio" name="advEngine" value="claude_sonnet"> ☁️ Claude Sonnet 4.6 (AWS Bedrock) ★★ <small class="control-hint">(best · ~30-90s)</small></label>
|
||||||
</div>
|
</div>
|
||||||
<p class="upload-hint">Azure mini finishes fastest. Azure full produces the most thorough advocate brief. Norwegian specialist v3 is a Qwen2.5 fine-tune trained on barnevernsloven, ECHR, and forvaltningsloven — highest precision for § 4-25, Strand Lobben, and procedural red flags.</p>
|
<p class="upload-hint">Azure mini finishes fastest. Claude Sonnet 4.6 via AWS Bedrock produces the most thorough advocate brief — superior at multi-party legal reasoning, ECHR precedent weighting, and long-form argumentation. Norwegian specialist v3 is a Qwen2.5 fine-tune trained on barnevernsloven, ECHR, and forvaltningsloven — highest precision for § 4-25, Strand Lobben, and procedural red flags.</p>
|
||||||
|
|
||||||
<div class="dr-slice-section">
|
<div class="dr-slice-section">
|
||||||
<p class="control-label">Corpus slices</p>
|
<p class="control-label">Corpus slices</p>
|
||||||
|
|||||||
+5
-201
@@ -1,205 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/includes/bootstrap.php';
|
// Consolidated into /account.php (Plan & credits section).
|
||||||
require_once __DIR__ . '/includes/FreeTier.php';
|
// Preserve external bookmarks, Stripe return URLs and help-doc links via 302.
|
||||||
require_once __DIR__ . '/includes/PricingCatalog.php';
|
|
||||||
|
|
||||||
dbnToolsRequirePageAuth('/billing.php');
|
$paid = (isset($_GET['status']) && $_GET['status'] === 'success') ? '?paid=1' : '';
|
||||||
|
header('Location: /account.php' . $paid . '#plan', true, 302);
|
||||||
$uiLang = dbnToolsCurrentLanguage();
|
exit;
|
||||||
$isSso = dbnToolsIsFreeTier();
|
|
||||||
$userId = $isSso ? (int)$_SESSION['dbn_tools_sso_uid'] : 0;
|
|
||||||
$email = (string)($_SESSION['dbn_tools_user_email'] ?? '');
|
|
||||||
|
|
||||||
$detail = $userId > 0 ? FreeTier::balanceDetail($userId) : [
|
|
||||||
'balance' => 0, 'bonus_balance' => 0, 'tier' => 'caveau',
|
|
||||||
'storage_used_bytes' => 0, 'storage_quota_bytes' => 0,
|
|
||||||
'survey_completed_at' => null, 'subscription_period_end' => null,
|
|
||||||
'trial_active' => false, 'trial_days_remaining' => 0, 'trial_expires_at' => null,
|
|
||||||
];
|
|
||||||
|
|
||||||
$tier = (string)$detail['tier'];
|
|
||||||
$isPaidTier = in_array($tier, ['plus', 'pro'], true);
|
|
||||||
$effective = (int)$detail['balance'] + (int)$detail['bonus_balance'];
|
|
||||||
$storageMb = round($detail['storage_used_bytes'] / 1048576, 1);
|
|
||||||
$quotaMb = $detail['storage_quota_bytes'] > 0 ? round($detail['storage_quota_bytes'] / 1048576, 0) : 0;
|
|
||||||
$storagePct = $quotaMb > 0 ? min(100, round(($storageMb / $quotaMb) * 100)) : 0;
|
|
||||||
|
|
||||||
$tierLabels = array_map(static fn(array $plan): string => (string)$plan['name'], PricingCatalog::plans());
|
|
||||||
$tierLabels['caveau'] = 'CaveauAI';
|
|
||||||
|
|
||||||
// Recent usage
|
|
||||||
$db = dbnmDb();
|
|
||||||
$stmt = $db->prepare(
|
|
||||||
'SELECT tool, credits_used, created_at FROM user_tool_usage_log
|
|
||||||
WHERE user_id = ? ORDER BY created_at DESC LIMIT 25'
|
|
||||||
);
|
|
||||||
$stmt->execute([$userId]);
|
|
||||||
$recent = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$status = (string)($_GET['status'] ?? '');
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Konto & fakturering — tools.dobetternorge.no</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;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
|
|
||||||
<link rel="stylesheet" href="assets/css/tools.css">
|
|
||||||
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.css">
|
|
||||||
<style>
|
|
||||||
.billing-shell { max-width: 1000px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
|
|
||||||
.billing-shell h1 { font-family: 'Crimson Pro', serif; margin: 0 0 0.5rem; }
|
|
||||||
.billing-shell .sub { color: #6b7280; margin-bottom: 2rem; }
|
|
||||||
.billing-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-bottom: 2rem; }
|
|
||||||
@media (max-width: 720px) { .billing-grid { grid-template-columns: 1fr; } }
|
|
||||||
.billing-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 1.5rem; }
|
|
||||||
.billing-card h2 { margin: 0 0 0.5rem; font-size: 1.1rem; color: #374151; }
|
|
||||||
.tier-badge { display: inline-block; padding: 4px 12px; border-radius: 999px; font-size: 0.85rem; font-weight: 600; }
|
|
||||||
.tier-free { background: #f3f4f6; color: #374151; }
|
|
||||||
.tier-plus { background: #ddd6fe; color: #5b21b6; }
|
|
||||||
.tier-pro { background: #bfdbfe; color: #1e40af; }
|
|
||||||
.tier-caveau { background: #d1fae5; color: #065f46; }
|
|
||||||
.balance-big { font-size: 2.6rem; font-weight: 700; color: #00205B; margin: 0.5rem 0; }
|
|
||||||
.balance-break { color: #6b7280; font-size: 0.9rem; }
|
|
||||||
.storage-bar { background: #f3f4f6; border-radius: 999px; height: 10px; overflow: hidden; margin: 0.75rem 0; }
|
|
||||||
.storage-bar > div { background: #00205B; height: 100%; transition: width 0.3s; }
|
|
||||||
.billing-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 2rem 0; }
|
|
||||||
.btn { padding: 0.7rem 1.25rem; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-block; }
|
|
||||||
.btn-primary { background: #00205B; color: #fff; }
|
|
||||||
.btn-primary:hover { background: #001740; }
|
|
||||||
.btn-secondary { background: #f3f4f6; color: #1f2937; }
|
|
||||||
.btn-secondary:hover { background: #e5e7eb; }
|
|
||||||
.usage-table { width: 100%; border-collapse: collapse; }
|
|
||||||
.usage-table th, .usage-table td { padding: 8px 12px; border-bottom: 1px solid #f3f4f6; text-align: left; font-size: 0.9rem; }
|
|
||||||
.usage-table th { color: #6b7280; font-weight: 500; }
|
|
||||||
.usage-table .negative { color: #059669; font-weight: 600; }
|
|
||||||
.usage-table .positive { color: #b91c1c; }
|
|
||||||
.status-pill { display: inline-block; margin-bottom: 1.5rem; padding: 8px 14px; border-radius: 6px; font-size: 0.9rem; }
|
|
||||||
.status-success { background: #d1fae5; color: #065f46; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main class="billing-shell">
|
|
||||||
<h1>Konto & fakturering</h1>
|
|
||||||
<p class="sub"><?= htmlspecialchars($email) ?> · <a href="/dashboard.php">Tilbake til dashbordet</a></p>
|
|
||||||
|
|
||||||
<?php if ($status === 'success'): ?>
|
|
||||||
<p class="status-pill status-success">Betalingen er bekreftet. Hvis du nettopp abonnerte, kan det ta noen sekunder før kontoen oppdateres.</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="billing-grid">
|
|
||||||
<div class="billing-card">
|
|
||||||
<h2>Nåværende plan</h2>
|
|
||||||
<p style="margin:0.5rem 0 1rem;">
|
|
||||||
<span class="tier-badge tier-<?= htmlspecialchars($tier) ?>"><?= htmlspecialchars($tierLabels[$tier] ?? $tier) ?></span>
|
|
||||||
</p>
|
|
||||||
<?php if (!empty($detail['trial_active'])): ?>
|
|
||||||
<p class="balance-break" style="color:#92400e;">Plus prøveperiode: <?= (int)$detail['trial_days_remaining'] ?> dager igjen. Kortet belastes automatisk etter prøveperioden hvis du ikke kansellerer.</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($detail['subscription_period_end'])): ?>
|
|
||||||
<p class="balance-break">Fornyes <?= htmlspecialchars(date('j. F Y', strtotime((string)$detail['subscription_period_end']))) ?></p>
|
|
||||||
<?php elseif ($tier === 'free'): ?>
|
|
||||||
<p class="balance-break">Ingen aktiv abonnement. <a href="/pricing.php">Se planer</a></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="billing-actions" style="margin: 1.25rem 0 0;">
|
|
||||||
<?php if ($isPaidTier): ?>
|
|
||||||
<button type="button" id="portalBtn" class="btn btn-secondary">Administrer abonnement</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a class="btn btn-primary" href="/pricing.php">Se alle planer</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="billing-card">
|
|
||||||
<h2>Tilgjengelige kreditter</h2>
|
|
||||||
<p class="balance-big"><?= number_format($effective, 0, ',', ' ') ?></p>
|
|
||||||
<p class="balance-break">
|
|
||||||
<?= (int)$detail['balance'] ?> månedlige · <?= (int)$detail['bonus_balance'] ?> forhåndsbetalte
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($isPaidTier): ?>
|
|
||||||
<div class="billing-card">
|
|
||||||
<h2>Sak-lagring</h2>
|
|
||||||
<p class="balance-big" style="font-size:1.8rem;"><?= $storageMb ?> MB <span style="color:#6b7280; font-size:1rem;">/ <?= $quotaMb ?> MB</span></p>
|
|
||||||
<div class="storage-bar"><div style="width: <?= $storagePct ?>%;"></div></div>
|
|
||||||
<p class="balance-break"><a href="/min-sak.php">Gå til Min Sak</a></p>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="billing-card">
|
|
||||||
<h2>Min Sak</h2>
|
|
||||||
<p style="color:#6b7280; margin:0 0 1rem;">Last opp dine egne dokumenter — alle verktøyene kan deretter referere til din private sak.</p>
|
|
||||||
<a class="btn btn-primary" href="/pricing.php">Oppgrader for å bygge sak</a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="billing-card">
|
|
||||||
<h2>Bonus-kreditter (undersøkelse)</h2>
|
|
||||||
<?php if (!empty($detail['survey_completed_at'])): ?>
|
|
||||||
<p style="color:#059669;">✓ Du har allerede mottatt 25 bonuskreditter for å fylle ut undersøkelsen.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<p style="color:#374151; margin:0 0 1rem;">Svar på 5 korte spørsmål om dine behov og motta 25 bonus-kreditter — utløper aldri.</p>
|
|
||||||
<a class="btn btn-primary" href="https://dobetternorge.no/survey.php">Ta undersøkelsen</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="billing-card">
|
|
||||||
<h2>Nylig bruk (siste 25)</h2>
|
|
||||||
<?php if (empty($recent)): ?>
|
|
||||||
<p style="color:#6b7280;">Ingen bruk registrert ennå.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<table class="usage-table">
|
|
||||||
<thead><tr><th>Verktøy / hendelse</th><th>Kreditter</th><th>Tidspunkt</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($recent as $r): ?>
|
|
||||||
<?php $credits = (int)$r['credits_used']; ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= htmlspecialchars($r['tool']) ?></td>
|
|
||||||
<td class="<?= $credits < 0 ? 'negative' : 'positive' ?>"><?= $credits < 0 ? '+' . abs($credits) : '-' . $credits ?></td>
|
|
||||||
<td><?= htmlspecialchars((string)$r['created_at']) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
const portalBtn = document.getElementById('portalBtn');
|
|
||||||
if (portalBtn) {
|
|
||||||
portalBtn.addEventListener('click', async () => {
|
|
||||||
portalBtn.disabled = true;
|
|
||||||
const original = portalBtn.textContent;
|
|
||||||
portalBtn.textContent = 'Kobler til...';
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/stripe-portal.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: '{}',
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.ok && data.url) {
|
|
||||||
window.location.href = data.url;
|
|
||||||
} else {
|
|
||||||
alert(data.error?.message || 'Kunne ikke åpne portalen.');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
alert('Nettverksfeil: ' + e.message);
|
|
||||||
} finally {
|
|
||||||
portalBtn.disabled = false;
|
|
||||||
portalBtn.textContent = original;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
+3
-82
@@ -289,7 +289,7 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
|||||||
</div>
|
</div>
|
||||||
<div class="dash-overview-bar__right">
|
<div class="dash-overview-bar__right">
|
||||||
<?php if ($isPaid): ?>
|
<?php if ($isPaid): ?>
|
||||||
<a href="/billing.php" class="dash-overview-bar__action dash-overview-bar__action--manage"><?= htmlspecialchars($dl['manage_plan']) ?></a>
|
<a href="/account.php#plan" class="dash-overview-bar__action dash-overview-bar__action--manage"><?= htmlspecialchars($dl['manage_plan']) ?></a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<a href="/pricing.php" class="dash-overview-bar__action dash-overview-bar__action--upgrade"><?= htmlspecialchars($dl['upgrade_plan']) ?> →</a>
|
<a href="/pricing.php" class="dash-overview-bar__action dash-overview-bar__action--upgrade"><?= htmlspecialchars($dl['upgrade_plan']) ?> →</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -302,10 +302,10 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
|||||||
<div class="status-card" style="background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:1.3rem 1.5rem;">
|
<div class="status-card" style="background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:1.3rem 1.5rem;">
|
||||||
<p style="margin:0; color:#6b7280; font-size:.95rem; text-transform:uppercase; letter-spacing:.06em;"><?= htmlspecialchars(dbnToolsT('credits_available', $uiLang)) ?></p>
|
<p style="margin:0; color:#6b7280; font-size:.95rem; text-transform:uppercase; letter-spacing:.06em;"><?= htmlspecialchars(dbnToolsT('credits_available', $uiLang)) ?></p>
|
||||||
<p style="margin:.35rem 0 0; font-size:2rem; font-weight:700; color:#00205B;"><?= number_format($eff, 0, ',', ' ') ?></p>
|
<p style="margin:.35rem 0 0; font-size:2rem; font-weight:700; color:#00205B;"><?= number_format($eff, 0, ',', ' ') ?></p>
|
||||||
<p style="margin:0; color:#6b7280; font-size:.95rem;"><?= (int)$dashDetail['balance'] ?> <?= htmlspecialchars(dbnToolsT('credits_monthly', $uiLang)) ?> · <?= (int)$dashDetail['bonus_balance'] ?> <?= $uiLang === 'no' ? 'forhåndsbetalte' : 'prepaid' ?> · <a href="/billing.php"><?= htmlspecialchars(dbnToolsT('details_link', $uiLang)) ?></a></p>
|
<p style="margin:0; color:#6b7280; font-size:.95rem;"><?= (int)$dashDetail['balance'] ?> <?= htmlspecialchars(dbnToolsT('credits_monthly', $uiLang)) ?> · <?= (int)$dashDetail['bonus_balance'] ?> <?= $uiLang === 'no' ? 'forhåndsbetalte' : 'prepaid' ?> · <a href="/account.php#plan"><?= htmlspecialchars(dbnToolsT('details_link', $uiLang)) ?></a></p>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($isPaid): ?>
|
<?php if ($isPaid): ?>
|
||||||
<a class="status-card" href="/min-sak.php" style="background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:1.3rem 1.5rem; text-decoration:none; color:inherit;">
|
<a class="status-card" href="/account.php#case" style="background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:1.3rem 1.5rem; text-decoration:none; color:inherit;">
|
||||||
<p style="margin:0; color:#6b7280; font-size:.95rem; text-transform:uppercase; letter-spacing:.06em;"><?= htmlspecialchars(dbnToolsT('my_case', $uiLang)) ?></p>
|
<p style="margin:0; color:#6b7280; font-size:.95rem; text-transform:uppercase; letter-spacing:.06em;"><?= htmlspecialchars(dbnToolsT('my_case', $uiLang)) ?></p>
|
||||||
<p style="margin:.35rem 0 0; font-size:1.55rem; font-weight:700; color:#00205B;"><?= htmlspecialchars(dbnToolsT('build_your_case', $uiLang)) ?> →</p>
|
<p style="margin:.35rem 0 0; font-size:1.55rem; font-weight:700; color:#00205B;"><?= htmlspecialchars(dbnToolsT('build_your_case', $uiLang)) ?> →</p>
|
||||||
<?php
|
<?php
|
||||||
@@ -337,85 +337,6 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="account" class="dashboard-account-grid" aria-label="Account and profile">
|
|
||||||
<article class="dash-account-panel dash-account-panel--profile">
|
|
||||||
<div class="dash-account-panel__head">
|
|
||||||
<div>
|
|
||||||
<p class="dash-section-kicker">Account</p>
|
|
||||||
<h2>Profile details</h2>
|
|
||||||
</div>
|
|
||||||
<span class="dash-account-chip">Optional</span>
|
|
||||||
</div>
|
|
||||||
<form id="profileForm" class="dash-profile-form">
|
|
||||||
<label>
|
|
||||||
<span>Display name</span>
|
|
||||||
<input name="display_name" type="text" maxlength="100" autocomplete="name" value="<?= htmlspecialchars($dashProfileValue('display_name')) ?>">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Phone</span>
|
|
||||||
<input name="phone" type="tel" maxlength="40" autocomplete="tel" value="<?= htmlspecialchars($dashProfileValue('phone')) ?>">
|
|
||||||
</label>
|
|
||||||
<label class="span-2">
|
|
||||||
<span>Address line 1</span>
|
|
||||||
<input name="address_line1" type="text" maxlength="180" autocomplete="address-line1" value="<?= htmlspecialchars($dashProfileValue('address_line1')) ?>">
|
|
||||||
</label>
|
|
||||||
<label class="span-2">
|
|
||||||
<span>Address line 2</span>
|
|
||||||
<input name="address_line2" type="text" maxlength="180" autocomplete="address-line2" value="<?= htmlspecialchars($dashProfileValue('address_line2')) ?>">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Postal code</span>
|
|
||||||
<input name="postal_code" type="text" maxlength="32" autocomplete="postal-code" value="<?= htmlspecialchars($dashProfileValue('postal_code')) ?>">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>City</span>
|
|
||||||
<input name="city" type="text" maxlength="100" autocomplete="address-level2" value="<?= htmlspecialchars($dashProfileValue('city')) ?>">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Region</span>
|
|
||||||
<input name="address_region" type="text" maxlength="100" autocomplete="address-level1" value="<?= htmlspecialchars($dashProfileValue('address_region')) ?>">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Country</span>
|
|
||||||
<input name="country" type="text" maxlength="2" autocomplete="country" placeholder="NO" value="<?= htmlspecialchars($dashProfileValue('country')) ?>">
|
|
||||||
</label>
|
|
||||||
<input type="hidden" name="preferred_language" value="<?= htmlspecialchars($uiLang) ?>">
|
|
||||||
<div class="dash-profile-actions span-2">
|
|
||||||
<button type="submit" class="dash-profile-save">Save profile</button>
|
|
||||||
<p id="profileStatus" class="dash-profile-status" role="status" aria-live="polite"></p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="dash-account-panel">
|
|
||||||
<div class="dash-account-panel__head">
|
|
||||||
<div>
|
|
||||||
<p class="dash-section-kicker">Users</p>
|
|
||||||
<h2>Workspace access</h2>
|
|
||||||
</div>
|
|
||||||
<span class="dash-account-chip"><?= htmlspecialchars($tierLabel[0]) ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="dash-account-list">
|
|
||||||
<div>
|
|
||||||
<span>Email</span>
|
|
||||||
<strong><?= htmlspecialchars((string)($dashAuthUser['email'] ?? '')) ?></strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Seats</span>
|
|
||||||
<strong><?= htmlspecialchars($dashTeamLabel) ?></strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Role</span>
|
|
||||||
<strong>Owner</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Plan storage</span>
|
|
||||||
<strong><?= $isPaid ? htmlspecialchars(round((int)$dashDetail['storage_quota_bytes'] / 1048576) . ' MB') : 'Upgrade for My Case storage' ?></strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="dash-account-note">Team invitations and seat management will be added after this consolidation pass.</p>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="disclaimer" role="note"><?= htmlspecialchars(dbnToolsT('disclaimer', $uiLang)) ?></div>
|
<div class="disclaimer" role="note"><?= htmlspecialchars(dbnToolsT('disclaimer', $uiLang)) ?></div>
|
||||||
|
|||||||
@@ -96,9 +96,10 @@ final class DbnBedrockGateway
|
|||||||
'temperature' => (float)($options['temperature'] ?? 0.2),
|
'temperature' => (float)($options['temperature'] ?? 0.2),
|
||||||
'max_tokens' => $options['max_tokens'] ?? 1200,
|
'max_tokens' => $options['max_tokens'] ?? 1200,
|
||||||
];
|
];
|
||||||
if (!empty($options['json'])) {
|
// response_format is intentionally omitted for Claude via Bedrock.
|
||||||
$payload['response_format'] = ['type' => 'json_object'];
|
// LiteLLM converts json_object to a tool-use constraint, routing output
|
||||||
}
|
// into tool_calls instead of content. Claude follows JSON instructions
|
||||||
|
// in the system prompt without needing response_format.
|
||||||
|
|
||||||
return $this->postJson($this->liteLlmUrl, $payload, (int)($options['timeout'] ?? 90));
|
return $this->postJson($this->liteLlmUrl, $payload, (int)($options['timeout'] ?? 90));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ final class DbnDeepResearchAgent
|
|||||||
): array {
|
): array {
|
||||||
$seedQuery = trim($seedQuery);
|
$seedQuery = trim($seedQuery);
|
||||||
$pastedText = trim($pastedText);
|
$pastedText = trim($pastedText);
|
||||||
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true) ? $engine : 'azure_mini';
|
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu', 'dbn_legal', 'dbn_legal_v3', 'claude_sonnet', 'claude_haiku'], true) ? $engine : 'azure_mini';
|
||||||
$language = dbnToolsNormalizeUiLanguage($language);
|
$language = dbnToolsNormalizeUiLanguage($language);
|
||||||
|
|
||||||
$controls = $this->normalizeControls($controls);
|
$controls = $this->normalizeControls($controls);
|
||||||
@@ -1221,7 +1221,7 @@ PROMPT;
|
|||||||
): array {
|
): array {
|
||||||
$seedQuery = trim($seedQuery);
|
$seedQuery = trim($seedQuery);
|
||||||
$pastedText = trim($pastedText);
|
$pastedText = trim($pastedText);
|
||||||
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true) ? $engine : 'azure_mini';
|
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu', 'dbn_legal', 'dbn_legal_v3', 'claude_sonnet', 'claude_haiku'], true) ? $engine : 'azure_mini';
|
||||||
$language = dbnToolsNormalizeUiLanguage($language);
|
$language = dbnToolsNormalizeUiLanguage($language);
|
||||||
$controls = $this->normalizeControls($controls);
|
$controls = $this->normalizeControls($controls);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -88,7 +88,7 @@ $_navAssetBase = str_contains($_navScriptPath, '/dashboard/') ? '../assets' : 'a
|
|||||||
<?= htmlspecialchars(dbnToolsT('nav_login', $_navLang)) ?>
|
<?= htmlspecialchars(dbnToolsT('nav_login', $_navLang)) ?>
|
||||||
</a>
|
</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<a class="dbn-nav__account-link" href="/dashboard.php#account" title="<?= htmlspecialchars($_navEmail) ?>">
|
<a class="dbn-nav__account-link" href="/account.php" title="<?= htmlspecialchars($_navEmail) ?>">
|
||||||
<?php if ($_navUser !== ''): ?>
|
<?php if ($_navUser !== ''): ?>
|
||||||
<span class="dbn-nav__username"><?= htmlspecialchars($_navUser) ?></span>
|
<span class="dbn-nav__username"><?= htmlspecialchars($_navUser) ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
+4
-317
@@ -1,321 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/includes/bootstrap.php';
|
// Consolidated into /account.php (My Case section).
|
||||||
require_once __DIR__ . '/includes/FreeTier.php';
|
// Preserve external bookmarks and help-doc links via 302.
|
||||||
require_once __DIR__ . '/includes/CaseStore.php';
|
|
||||||
require_once __DIR__ . '/includes/CaseResults.php';
|
|
||||||
|
|
||||||
dbnToolsRequirePageAuth('/min-sak.php');
|
header('Location: /account.php#case', true, 302);
|
||||||
|
exit;
|
||||||
$uiLang = dbnToolsCurrentLanguage();
|
|
||||||
$userId = (int)($_SESSION['dbn_tools_sso_uid'] ?? 0);
|
|
||||||
if ($userId <= 0) {
|
|
||||||
header('Location: /dashboard.php');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$detail = FreeTier::balanceDetail($userId);
|
|
||||||
$tier = (string)$detail['tier'];
|
|
||||||
|
|
||||||
// Free tier: show upgrade gate
|
|
||||||
if (!FreeTier::isPaidTier($tier)) {
|
|
||||||
require_once __DIR__ . '/includes/footer.php';
|
|
||||||
$upgradeUrl = '/pricing.php';
|
|
||||||
?><!doctype html>
|
|
||||||
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
|
||||||
<head><meta charset="utf-8"><title>Min Sak — Do Better Norge</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">
|
|
||||||
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.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;font-size:2.4rem;margin:0 0 0.5rem;color:#00205B;">Min Sak — bygg din egen sak</h1>
|
|
||||||
<p style="color:#4b5563;font-size:1.1rem;margin-bottom:2rem;">Last opp dokumentene fra saken din én gang, og la <strong>alle</strong> verktøyene jobbe på din private korpus.</p>
|
|
||||||
<ul style="text-align:left;display:inline-block;line-height:1.9;color:#1f2937;">
|
|
||||||
<li>📄 Privat dokumentbank med OCR</li>
|
|
||||||
<li>🔍 Hybrid søk (BM25 + vektor) i din sak</li>
|
|
||||||
<li>🧠 Alle verktøy kan referere til din egen sak</li>
|
|
||||||
<li>🇪🇺 Alt lagres i EU (Tyskland/Finland/Norge)</li>
|
|
||||||
</ul>
|
|
||||||
<p style="margin:2rem 0 0;"><a href="<?= htmlspecialchars($upgradeUrl) ?>" style="background:#00205B;color:#fff;padding:1rem 2rem;border-radius:8px;font-weight:700;text-decoration:none;display:inline-block;">Se planer fra NOK 129/mo</a></p>
|
|
||||||
</main></body></html><?php
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$docs = CaseStore::listDocs($userId);
|
|
||||||
$used = (int)$detail['storage_used_bytes'];
|
|
||||||
$quota = (int)$detail['storage_quota_bytes'];
|
|
||||||
$usedMb = round($used / 1048576, 1);
|
|
||||||
$quotaMb = round($quota / 1048576, 0);
|
|
||||||
$pct = $quota > 0 ? min(100, round(($used / $quota) * 100)) : 0;
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Min Sak — tools.dobetternorge.no</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&display=swap">
|
|
||||||
<link rel="stylesheet" href="assets/css/tools.css">
|
|
||||||
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.css">
|
|
||||||
<style>
|
|
||||||
.ms-shell { max-width: 980px; margin: 0 auto; padding: 2rem 1.5rem 4rem; font-family: 'IBM Plex Sans', sans-serif; }
|
|
||||||
.ms-shell h1 { font-family: 'Crimson Pro', serif; margin: 0; }
|
|
||||||
.ms-shell .lede { color: #6b7280; margin: 0.25rem 0 2rem; }
|
|
||||||
.ms-status { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem; }
|
|
||||||
@media (max-width: 640px) { .ms-status { grid-template-columns: 1fr; } }
|
|
||||||
.ms-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 1.25rem 1.5rem; }
|
|
||||||
.ms-card h2 { margin: 0 0 0.4rem; font-size: 1rem; color: #374151; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
||||||
.ms-card .big { font-size: 1.6rem; font-weight: 700; color: #00205B; }
|
|
||||||
.ms-storage-bar { background: #f3f4f6; height: 10px; border-radius: 999px; overflow: hidden; margin: 0.6rem 0; }
|
|
||||||
.ms-storage-bar > div { background: linear-gradient(90deg, #00205B, #003478); height: 100%; }
|
|
||||||
.ms-upload { background: #fff; border: 2px dashed #cbd5e1; border-radius: 12px; padding: 2.5rem 1.5rem; text-align: center; cursor: pointer; transition: all 0.15s; margin-bottom: 2rem; }
|
|
||||||
.ms-upload:hover, .ms-upload.is-drag { border-color: #00205B; background: #f8fafc; }
|
|
||||||
.ms-upload input { display: none; }
|
|
||||||
.ms-upload-icon { font-size: 2.4rem; line-height: 1; margin-bottom: 0.5rem; }
|
|
||||||
.ms-upload p { margin: 0.25rem 0; color: #374151; }
|
|
||||||
.ms-upload .hint { color: #6b7280; font-size: 0.85rem; }
|
|
||||||
.ms-docs { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; }
|
|
||||||
.ms-doc { padding: 1rem 1.25rem; border-bottom: 1px solid #f3f4f6; display: flex; align-items: center; gap: 1rem; }
|
|
||||||
.ms-doc:last-child { border-bottom: none; }
|
|
||||||
.ms-doc-info { flex: 1; min-width: 0; }
|
|
||||||
.ms-doc-name { font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
.ms-doc-meta { font-size: 0.85rem; color: #6b7280; }
|
|
||||||
.ms-status-pill { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 0.78rem; font-weight: 600; }
|
|
||||||
.ms-status-pending { background: #fef3c7; color: #92400e; }
|
|
||||||
.ms-status-running { background: #ddd6fe; color: #5b21b6; }
|
|
||||||
.ms-status-ready { background: #d1fae5; color: #065f46; }
|
|
||||||
.ms-status-failed { background: #fee2e2; color: #991b1b; }
|
|
||||||
.ms-doc-actions button { background: #f3f4f6; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; }
|
|
||||||
.ms-doc-actions button:hover { background: #fee2e2; color: #991b1b; }
|
|
||||||
.ms-empty { padding: 2rem 1.5rem; text-align: center; color: #6b7280; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="dbn-context-bar" role="note">
|
|
||||||
<span class="dbn-context-bar__tag"><?= htmlspecialchars(dbnToolsT('context_bar_tag', $uiLang)) ?></span>
|
|
||||||
<nav class="dbn-context-bar__links" aria-label="About">
|
|
||||||
<a href="/why-ours.php<?= $uiLang !== 'en' ? '?lang=' . urlencode($uiLang) : '' ?>"><?= htmlspecialchars(dbnToolsT('context_bar_why', $uiLang)) ?></a>
|
|
||||||
<a href="/pricing.php<?= $uiLang !== 'en' ? '?lang=' . urlencode($uiLang) : '' ?>"><?= htmlspecialchars(dbnToolsT('context_bar_pricing', $uiLang)) ?></a>
|
|
||||||
<a href="/mcp-tool.php<?= $uiLang !== 'en' ? '?lang=' . urlencode($uiLang) : '' ?>"><?= htmlspecialchars(dbnToolsT('context_bar_mcp', $uiLang)) ?></a>
|
|
||||||
<a href="/privacy.php<?= $uiLang !== 'en' ? '?lang=' . urlencode($uiLang) : '' ?>"><?= htmlspecialchars(dbnToolsT('context_bar_privacy', $uiLang)) ?></a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<main class="ms-shell">
|
|
||||||
<p style="margin:0 0 0.25rem;color:#6b7280;font-size:0.85rem;text-transform:uppercase;letter-spacing:0.06em;">
|
|
||||||
<a href="/dashboard.php" style="color:inherit;">← Dashbord</a> · <a href="/billing.php" style="color:inherit;">Fakturering</a>
|
|
||||||
</p>
|
|
||||||
<h1>Min Sak</h1>
|
|
||||||
<p class="lede">Last opp dokumentene dine én gang. Alle verktøyene kan deretter referere til din private sak når du krysser av "Bruk min sak som kontekst".</p>
|
|
||||||
|
|
||||||
<section class="ms-status">
|
|
||||||
<div class="ms-card">
|
|
||||||
<h2>Lagring</h2>
|
|
||||||
<div class="big"><?= $usedMb ?> MB <span style="color:#6b7280;font-size:1rem;">/ <?= $quotaMb ?> MB</span></div>
|
|
||||||
<div class="ms-storage-bar"><div style="width: <?= $pct ?>%;"></div></div>
|
|
||||||
<p style="margin:0;color:#6b7280;font-size:0.85rem;"><?= count($docs) ?> dokumenter</p>
|
|
||||||
</div>
|
|
||||||
<div class="ms-card">
|
|
||||||
<h2>Plan</h2>
|
|
||||||
<div class="big" style="font-size:1.3rem;">
|
|
||||||
<?= htmlspecialchars(['plus'=>'Plus','pro'=>'Pro Familie'][$tier] ?? ucfirst($tier)) ?>
|
|
||||||
<?php if (!empty($detail['trial_active'])): ?>
|
|
||||||
<span style="display:inline-block;background:#fef3c7;color:#92400e;font-size:0.7rem;padding:2px 8px;border-radius:999px;margin-left:0.5rem;vertical-align:middle;">
|
|
||||||
Prøveperiode · <?= (int)$detail['trial_days_remaining'] ?> dager igjen
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<p style="margin:0;color:#6b7280;font-size:0.85rem;">
|
|
||||||
<a href="/pricing.php">Oppgrader</a> · <a href="/billing.php">Administrer</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<label for="msFileInput" class="ms-upload" id="msUploadZone">
|
|
||||||
<input id="msFileInput" type="file" accept="application/pdf" multiple>
|
|
||||||
<div class="ms-upload-icon">📤</div>
|
|
||||||
<p><strong>Slipp PDF-filer her, eller klikk for å bla</strong></p>
|
|
||||||
<p class="hint">Maks 25 MB per fil · OCR kjøres automatisk · alt lagres kryptert i EU</p>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div id="msFlash" role="status" aria-live="polite" style="margin-bottom:1rem;"></div>
|
|
||||||
|
|
||||||
<section class="ms-docs" aria-label="Dine dokumenter">
|
|
||||||
<?php if (empty($docs)): ?>
|
|
||||||
<p class="ms-empty">Ingen dokumenter ennå. Last opp din første PDF over.</p>
|
|
||||||
<?php else: foreach ($docs as $d): ?>
|
|
||||||
<div class="ms-doc" data-doc-id="<?= (int)$d['id'] ?>">
|
|
||||||
<div style="font-size:1.6rem;line-height:1;">📄</div>
|
|
||||||
<div class="ms-doc-info">
|
|
||||||
<div class="ms-doc-name"><?= htmlspecialchars($d['filename']) ?></div>
|
|
||||||
<div class="ms-doc-meta">
|
|
||||||
<?= round((int)$d['size_bytes'] / 1024, 0) ?> KB
|
|
||||||
<?php if (!empty($d['page_count'])): ?> · <?= (int)$d['page_count'] ?> sider<?php endif; ?>
|
|
||||||
<?php if (!empty($d['doc_type'])): ?> · <?= htmlspecialchars($d['doc_type']) ?><?php endif; ?>
|
|
||||||
· lastet opp <?= htmlspecialchars(date('j. F Y', strtotime((string)$d['uploaded_at']))) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="ms-status-pill ms-status-<?= htmlspecialchars($d['ocr_status']) ?>">
|
|
||||||
<?php
|
|
||||||
echo htmlspecialchars(['pending'=>'I kø','running'=>'OCR pågår','ready'=>'Klar','failed'=>'Feilet'][$d['ocr_status']] ?? $d['ocr_status']);
|
|
||||||
?>
|
|
||||||
</span>
|
|
||||||
<div class="ms-doc-actions">
|
|
||||||
<button type="button" class="ms-delete" data-id="<?= (int)$d['id'] ?>">Slett</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; endif; ?>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php $results = CaseResults::listForUser($userId, 50); ?>
|
|
||||||
<h2 style="font-family:'Crimson Pro',serif;margin:2.5rem 0 0.5rem;font-size:1.6rem;color:#00205B;">Lagrede analyser</h2>
|
|
||||||
<p class="lede" style="margin:0 0 1rem;">Alle resultater fra Korrespondanse, Advokat, BVJ, Dyp analyse, Motstrid og Tidslinje samles her — klar til å gjenåpnes, kjøres på nytt eller eksporteres.</p>
|
|
||||||
<section class="ms-results" aria-label="Lagrede analyser">
|
|
||||||
<?php if (empty($results)): ?>
|
|
||||||
<p class="ms-empty">Ingen lagrede analyser ennå. Kjør et verktøy for å lagre din første.</p>
|
|
||||||
<?php else: foreach ($results as $r): ?>
|
|
||||||
<div class="ms-result" data-result-id="<?= (int)$r['id'] ?>"
|
|
||||||
style="display:flex;align-items:center;gap:1rem;padding:0.95rem 1.25rem;border-bottom:1px solid #f3f4f6;background:#fff;">
|
|
||||||
<div style="font-size:1.4rem;line-height:1;"><?= htmlspecialchars(CaseResults::toolIcon((string)$r['tool'])) ?></div>
|
|
||||||
<div style="flex:1;min-width:0;">
|
|
||||||
<div style="font-weight:600;color:#1f2937;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
|
||||||
<a href="/case-result.php?id=<?= (int)$r['id'] ?>" style="color:inherit;text-decoration:none;">
|
|
||||||
<?= htmlspecialchars((string)($r['title'] ?? CaseResults::toolLabel((string)$r['tool']))) ?>
|
|
||||||
</a>
|
|
||||||
<?php if (!empty($r['pinned'])): ?>
|
|
||||||
<span title="Festet" style="color:#c9a84c;margin-left:0.4rem;">★</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<div style="font-size:0.85rem;color:#6b7280;">
|
|
||||||
<?= htmlspecialchars(CaseResults::toolLabel((string)$r['tool'])) ?>
|
|
||||||
· <?= htmlspecialchars(date('j. M Y H:i', strtotime((string)$r['created_at']))) ?>
|
|
||||||
<?php if (!empty($r['used_case_context'])): ?>
|
|
||||||
· <span style="background:#dbeafe;color:#1e3a8a;padding:1px 8px;border-radius:999px;font-size:0.75rem;font-weight:600;">Bruk min sak</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ms-result-actions" style="display:flex;gap:0.4rem;">
|
|
||||||
<button type="button" class="ms-pin" data-id="<?= (int)$r['id'] ?>"
|
|
||||||
style="background:#f3f4f6;border:none;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:0.85rem;">
|
|
||||||
<?= !empty($r['pinned']) ? 'Løsne' : 'Fest' ?>
|
|
||||||
</button>
|
|
||||||
<a href="/case-result.php?id=<?= (int)$r['id'] ?>"
|
|
||||||
style="background:#00205B;color:#fff;padding:6px 12px;border-radius:6px;text-decoration:none;font-size:0.85rem;font-weight:600;">
|
|
||||||
Åpne
|
|
||||||
</a>
|
|
||||||
<button type="button" class="ms-result-delete" data-id="<?= (int)$r['id'] ?>"
|
|
||||||
style="background:#f3f4f6;border:none;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:0.85rem;">
|
|
||||||
Slett
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; endif; ?>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
const fileInput = document.getElementById('msFileInput');
|
|
||||||
const dropZone = document.getElementById('msUploadZone');
|
|
||||||
const flash = document.getElementById('msFlash');
|
|
||||||
|
|
||||||
function showFlash(msg, isError) {
|
|
||||||
flash.style.cssText = 'padding:0.75rem 1rem;border-radius:6px;font-size:0.9rem;'
|
|
||||||
+ (isError ? 'background:#fee2e2;color:#991b1b;' : 'background:#d1fae5;color:#065f46;');
|
|
||||||
flash.textContent = msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadFile(file) {
|
|
||||||
if (file.size > 25 * 1024 * 1024) {
|
|
||||||
showFlash(file.name + ' er over 25 MB — del opp filen først.', true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = new FormData();
|
|
||||||
data.append('file', file);
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/case/upload.php', { method: 'POST', body: data });
|
|
||||||
const json = await res.json();
|
|
||||||
if (json.ok) {
|
|
||||||
showFlash('Lastet opp: ' + file.name + '. OCR starter automatisk.', false);
|
|
||||||
setTimeout(() => window.location.reload(), 1200);
|
|
||||||
} else {
|
|
||||||
showFlash(file.name + ': ' + (json.error?.message || 'Ukjent feil'), true);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showFlash('Nettverksfeil: ' + e.message, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', () => {
|
|
||||||
for (const f of fileInput.files) uploadFile(f);
|
|
||||||
});
|
|
||||||
|
|
||||||
['dragenter','dragover'].forEach(ev => dropZone.addEventListener(ev, e => {
|
|
||||||
e.preventDefault(); dropZone.classList.add('is-drag');
|
|
||||||
}));
|
|
||||||
['dragleave','drop'].forEach(ev => dropZone.addEventListener(ev, e => {
|
|
||||||
e.preventDefault(); dropZone.classList.remove('is-drag');
|
|
||||||
}));
|
|
||||||
dropZone.addEventListener('drop', e => {
|
|
||||||
for (const f of e.dataTransfer.files) {
|
|
||||||
if (f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')) uploadFile(f);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.ms-delete').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async () => {
|
|
||||||
if (!confirm('Slette dette dokumentet for godt?')) return;
|
|
||||||
const id = btn.getAttribute('data-id');
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/case/delete.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ doc_id: parseInt(id, 10) }),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
if (json.ok) window.location.reload();
|
|
||||||
else alert(json.error?.message || 'Sletting feilet.');
|
|
||||||
} catch (e) { alert('Nettverksfeil: ' + e.message); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Saved analyses: pin / delete
|
|
||||||
document.querySelectorAll('.ms-pin').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async () => {
|
|
||||||
const id = btn.getAttribute('data-id');
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/case/result-action.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ action: 'pin', id: parseInt(id, 10) }),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
if (json.ok) window.location.reload();
|
|
||||||
else alert(json.error?.message || 'Festing feilet.');
|
|
||||||
} catch (e) { alert('Nettverksfeil: ' + e.message); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.ms-result-delete').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async () => {
|
|
||||||
if (!confirm('Slette denne analysen for godt?')) return;
|
|
||||||
const id = btn.getAttribute('data-id');
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/case/result-action.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ action: 'delete', id: parseInt(id, 10) }),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
if (json.ok) window.location.reload();
|
|
||||||
else alert(json.error?.message || 'Sletting feilet.');
|
|
||||||
} catch (e) { alert('Nettverksfeil: ' + e.message); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user