diff --git a/account.php b/account.php
index 00ce7cd..9f6f95a 100644
--- a/account.php
+++ b/account.php
@@ -935,6 +935,14 @@ window.DBN_TOOLS_LANG = = json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
· = htmlspecialchars($l['used_case_badge']) ?>
+
+ ·
+ >Draft
+ >Sent
+ >Reply received
+ >Resolved
+
+
@@ -1164,6 +1172,16 @@ window.ACCT_L = = json_encode($l, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLAS
});
});
+ document.querySelectorAll('.korr-status-select').forEach(function (sel) {
+ sel.addEventListener('change', function () {
+ fetch('/api/case/result-action.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action: 'set_status', id: parseInt(sel.getAttribute('data-id'), 10), status: sel.value })
+ }).catch(function () {});
+ });
+ });
+
document.querySelectorAll('.acct-result-delete').forEach(function (btn) {
btn.addEventListener('click', function () {
if (!confirm(window.ACCT_L.confirm_delete_analysis)) return;
diff --git a/api/case/result-action.php b/api/case/result-action.php
index bd9e85b..de8495e 100644
--- a/api/case/result-action.php
+++ b/api/case/result-action.php
@@ -48,6 +48,14 @@ switch ($action) {
dbnToolsRespond(['ok' => true]);
break;
+ case 'set_status':
+ $status = (string)($input['status'] ?? '');
+ if (!CaseResults::updateStatus($userId, $id, $status)) {
+ dbnToolsError('Could not update status — invalid value or result not found.', 422, 'status_failed');
+ }
+ dbnToolsRespond(['ok' => true, 'status' => $status]);
+ break;
+
default:
dbnToolsError('Unknown action.', 422, 'unknown_action');
}
diff --git a/api/korrespond.php b/api/korrespond.php
index e3ff488..4a8069b 100644
--- a/api/korrespond.php
+++ b/api/korrespond.php
@@ -165,8 +165,12 @@ try {
}
// ── Deduct credit now (Pass 2 starts) ───────────────────────────────────────
- $ftUid = dbnToolsFreeTierCheck('korrespond');
+ $ftUid = dbnToolsFreeTierCheck('korrespond');
$engine = ToolModels::engineForUser($ftUid, 'azure_mini');
+ $inputEngine = (string)($input['engine'] ?? '');
+ if (in_array($inputEngine, ['azure_mini', 'claude_sonnet'], true)) {
+ $engine = $inputEngine;
+ }
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond');
$creditDeducted = true;
@@ -184,12 +188,28 @@ try {
'ok' => true,
'latency_ms' => $result['latency_ms'],
'source_count' => is_array($result['cited_law'] ?? null) ? count($result['cited_law']) : 0,
- 'deployment' => ($engine === 'azure_full') ? 'gpt-4o' : 'gpt-4o-mini',
+ 'deployment' => $engine,
]);
$emit('final', ['result' => $result]);
+ // Auto-save for paid users — non-critical, silent on failure
+ try {
+ require_once __DIR__ . '/../includes/CaseResults.php';
+ require_once __DIR__ . '/../includes/CaseStore.php';
+ $authUser = dbnToolsAuthenticatedUser();
+ $uid = (int)($authUser['user_id'] ?? 0);
+ $oid = CaseStore::caseResolveClientId($uid);
+ CaseResults::save($uid, $oid, 'korrespond', $intake, $result, [
+ 'used_case_context' => !empty($intake['use_my_case']) ? 1 : 0,
+ 'case_doc_ids' => $GLOBALS['dbn_last_case_doc_ids'] ?? [],
+ 'model' => $engine,
+ 'latency_ms' => $result['latency_ms'],
+ 'credits_charged' => 1,
+ ]);
+ } catch (Throwable) { /* non-critical */ }
+
} catch (DbnToolsHttpException $e) {
$latency = (int)round((microtime(true) - $startTime) * 1000);
dbnToolsLogMetadata([
diff --git a/assets/js/korrespond.js b/assets/js/korrespond.js
index 16b8800..e706aaf 100644
--- a/assets/js/korrespond.js
+++ b/assets/js/korrespond.js
@@ -276,6 +276,7 @@
clarifications: pendingClarifications,
force_draft: !!forceDraft,
use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false,
+ engine: (document.querySelector('[name="korrEngine"]:checked')?.value ?? 'azure_mini'),
};
if (korrDocIds.length) payload.doc_ids = korrDocIds;
return payload;
diff --git a/includes/CaseResults.php b/includes/CaseResults.php
index 5434dfc..b0db033 100644
--- a/includes/CaseResults.php
+++ b/includes/CaseResults.php
@@ -124,7 +124,7 @@ final class CaseResults
$db = dbnmDb();
$stmt = $db->prepare(
'SELECT id, user_id, owner_user_id, tool, title, used_case_context,
- model, latency_ms, credits_charged, pinned, created_at
+ model, latency_ms, credits_charged, pinned, korr_status, created_at
FROM case_tool_results
WHERE owner_user_id = ? AND deleted_at IS NULL
ORDER BY pinned DESC, created_at DESC
@@ -226,6 +226,27 @@ final class CaseResults
return $stmt->rowCount() > 0;
}
+ /** Update the correspondence status (korrespond tool only). */
+ public static function updateStatus(int $userId, int $id, string $status): bool
+ {
+ $allowed = ['draft', 'sent', 'replied', 'resolved'];
+ if (!in_array($status, $allowed, true)) {
+ return false;
+ }
+ if (!self::tableReady()) {
+ return false;
+ }
+ $ownerId = CaseStore::caseResolveClientId($userId);
+ $db = dbnmDb();
+ $stmt = $db->prepare(
+ "UPDATE case_tool_results
+ SET korr_status = ?
+ WHERE id = ? AND owner_user_id = ? AND tool = 'korrespond' AND deleted_at IS NULL"
+ );
+ $stmt->execute([$status, $id, $ownerId]);
+ return $stmt->rowCount() > 0;
+ }
+
/** Human-readable Norwegian title hint per tool. */
public static function toolLabel(string $tool): string
{
diff --git a/includes/KorrespondAgent.php b/includes/KorrespondAgent.php
index c4729d6..34a5efd 100644
--- a/includes/KorrespondAgent.php
+++ b/includes/KorrespondAgent.php
@@ -27,6 +27,14 @@ final class DbnKorrespondAgent
private const MAX_CONTEXT_CHARS = 24000;
private const MAX_DRAFT_TOKENS = 2000;
+ private function resolveDeployment(string $azureDeployment, bool $heavy = false): string
+ {
+ if (!($this->azure instanceof DbnBedrockGateway)) {
+ return $azureDeployment;
+ }
+ return $heavy ? DbnBedrockModelRouter::LITELLM_SONNET : DbnBedrockModelRouter::LITELLM_HAIKU;
+ }
+
/** Recipient-body presets → default slice toggles for law retrieval. */
private const BODY_PRESETS = [
'barnehage' => ['family_core', 'bufdir_guidance'],
@@ -215,7 +223,7 @@ PROMPT;
];
try {
- $raw = $this->azure->withDeployment(self::CLASSIFY_DEPLOYMENT)->chatText([
+ $raw = $this->azure->withDeployment($this->resolveDeployment(self::CLASSIFY_DEPLOYMENT))->chatText([
['role' => 'system', 'content' => 'You return valid JSON only. No markdown fences.'],
['role' => 'user', 'content' => $prompt],
], ['json' => true, 'temperature' => 0.1, 'max_tokens' => 800, 'timeout' => 30]);
@@ -237,7 +245,11 @@ PROMPT;
*/
public function generate(array $intake, array $classify, ?callable $emit = null, string $engine = 'azure_mini'): array
{
- $draftDeployment = ($engine === 'azure_full') ? 'gpt-4o' : 'gpt-4o-mini';
+ $draftDeployment = ($this->azure instanceof DbnBedrockGateway)
+ ? (($engine === 'claude_sonnet' || $engine === 'azure_full')
+ ? DbnBedrockModelRouter::LITELLM_SONNET
+ : DbnBedrockModelRouter::LITELLM_HAIKU)
+ : (($engine === 'azure_full') ? self::DRAFT_DEPLOYMENT : 'gpt-4o-mini');
$body = $intake['recipient_body'] ?? 'other';
$outputType = $intake['output_type'] ?? 'email';
$tone = $intake['tone'] ?? 'neutral';
@@ -667,7 +679,7 @@ Norwegian source:
PROMPT;
try {
- return $this->azure->withDeployment(self::SELFCHECK_DEPLOYMENT)->chatText([
+ return $this->azure->withDeployment($this->resolveDeployment(self::SELFCHECK_DEPLOYMENT))->chatText([
['role' => 'system', 'content' => 'You are a precise legal translator.'],
['role' => 'user', 'content' => $prompt],
], [
diff --git a/korrespond.php b/korrespond.php
index 41bcd69..66907cf 100644
--- a/korrespond.php
+++ b/korrespond.php
@@ -70,6 +70,14 @@ require_once __DIR__ . '/includes/layout.php';
Conciliatory-warm
+
+
+ Engine
+ ☁️ Quick (~40-70s)
+ ☁️ Thorough ★★ (~70-120s)
+
+ Quick uses Claude Haiku 4.5 for drafting — fast and solid for standard correspondence. Thorough uses Claude Sonnet 4.6 — better at multi-statute cases, complex appeal grounds, and ECHR framing.
+