Timeline: group same-date/actor events, clean badges, Bedrock routing
- renderTimeline(): group consecutive same-date+actor events into one card with a bullet list; single events keep their current layout - Date format: YYYY-MM-DD → "1 Jun 2023" (3-letter month, international) - Time shown in header when available - Remove date_type badge; confidence badge replaced by amber ⚠ flag on low-confidence events only (high/medium border colour still shows) - LegalTools.php: resolve azure_full/azure_mini to Bedrock Sonnet/Haiku when DbnBedrockGateway is active; claude_sonnet/claude_haiku also handled - timeline.php + api/timeline.php: engine labels updated (Claude Haiku/Sonnet); claude_haiku + claude_sonnet added to valid engine list - i18n engine labels updated in all 4 languages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ dbnToolsRequireMethod('POST');
|
|||||||
dbnToolsRequireAuth();
|
dbnToolsRequireAuth();
|
||||||
$input = dbnToolsJsonInput(1500000);
|
$input = dbnToolsJsonInput(1500000);
|
||||||
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
|
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
|
||||||
$_validEngines = ['nova_lite', 'azure_mini', 'azure_full'];
|
$_validEngines = ['nova_lite', 'azure_mini', 'azure_full', 'claude_haiku', 'claude_sonnet'];
|
||||||
$_requestedEngine = in_array((string)($input['engine'] ?? ''), $_validEngines, true)
|
$_requestedEngine = in_array((string)($input['engine'] ?? ''), $_validEngines, true)
|
||||||
? (string)$input['engine'] : 'azure_mini';
|
? (string)$input['engine'] : 'azure_mini';
|
||||||
|
|
||||||
|
|||||||
@@ -849,6 +849,29 @@ p {
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confidence-low-flag {
|
||||||
|
color: #d97706;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
margin-left: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-bullets {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
padding-left: 1.25em;
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-bullet {
|
||||||
|
margin: 4px 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-bullet .timeline-excerpt {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.confidence-badge {
|
.confidence-badge {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
+84
-33
@@ -221,9 +221,9 @@ const REDACT_I18N = {
|
|||||||
const TIMELINE_I18N = {
|
const TIMELINE_I18N = {
|
||||||
en: {
|
en: {
|
||||||
timelineEngine: 'Engine',
|
timelineEngine: 'Engine',
|
||||||
timelineEngineAzureMini: 'Azure gpt-4o-mini',
|
timelineEngineAzureMini: 'Standard',
|
||||||
timelineEngineAzureFull: 'Azure gpt-4o',
|
timelineEngineAzureFull: 'Deep',
|
||||||
timelineEngineHint: 'gpt-4o-mini: 1 credit — fast, handles most timelines well. gpt-4o: 2 credits — higher accuracy for complex multi-actor cases.',
|
timelineEngineHint: 'Standard uses Claude Haiku 4.5 — fast and accurate for most documents (1 credit). Deep uses Claude Sonnet 4.6 — better for complex multi-actor cases (2 credits).',
|
||||||
timelineAdvancedToggle: 'Advanced settings',
|
timelineAdvancedToggle: 'Advanced settings',
|
||||||
timelineFocus: 'Focus',
|
timelineFocus: 'Focus',
|
||||||
timelineFocusAll: 'All events',
|
timelineFocusAll: 'All events',
|
||||||
@@ -262,9 +262,9 @@ const TIMELINE_I18N = {
|
|||||||
},
|
},
|
||||||
no: {
|
no: {
|
||||||
timelineEngine: 'Motor',
|
timelineEngine: 'Motor',
|
||||||
timelineEngineAzureMini: 'Azure gpt-4o-mini',
|
timelineEngineAzureMini: 'Standard',
|
||||||
timelineEngineAzureFull: 'Azure gpt-4o',
|
timelineEngineAzureFull: 'Dyp',
|
||||||
timelineEngineHint: 'gpt-4o-mini: 1 kreditt — rask, håndterer de fleste tidslinjer godt. gpt-4o: 2 kreditter — høyere nøyaktighet for komplekse saker med mange aktører.',
|
timelineEngineHint: 'Standard bruker Claude Haiku 4.5 — rask og nøyaktig for de fleste dokumenter (1 kreditt). Dyp bruker Claude Sonnet 4.6 — bedre for komplekse saker med mange aktører (2 kreditter).',
|
||||||
timelineAdvancedToggle: 'Avanserte innstillinger',
|
timelineAdvancedToggle: 'Avanserte innstillinger',
|
||||||
timelineFocus: 'Fokus',
|
timelineFocus: 'Fokus',
|
||||||
timelineFocusAll: 'Alle hendelser',
|
timelineFocusAll: 'Alle hendelser',
|
||||||
@@ -303,9 +303,9 @@ const TIMELINE_I18N = {
|
|||||||
},
|
},
|
||||||
uk: {
|
uk: {
|
||||||
timelineEngine: 'Рушій',
|
timelineEngine: 'Рушій',
|
||||||
timelineEngineAzureMini: 'Azure gpt-4o-mini',
|
timelineEngineAzureMini: 'Стандарт',
|
||||||
timelineEngineAzureFull: 'Azure gpt-4o',
|
timelineEngineAzureFull: 'Глибокий',
|
||||||
timelineEngineHint: 'gpt-4o-mini: 1 кредит — швидко, добре справляється з більшістю хронологій. gpt-4o: 2 кредити — вища точність для складних справ з багатьма учасниками.',
|
timelineEngineHint: 'Стандарт використовує Claude Haiku 4.5 — швидко і точно для більшості документів (1 кредит). Глибокий використовує Claude Sonnet 4.6 — краще для складних справ (2 кредити).',
|
||||||
timelineAdvancedToggle: 'Розширені налаштування',
|
timelineAdvancedToggle: 'Розширені налаштування',
|
||||||
timelineFocus: 'Фокус',
|
timelineFocus: 'Фокус',
|
||||||
timelineFocusAll: 'Всі події',
|
timelineFocusAll: 'Всі події',
|
||||||
@@ -344,9 +344,9 @@ const TIMELINE_I18N = {
|
|||||||
},
|
},
|
||||||
pl: {
|
pl: {
|
||||||
timelineEngine: 'Silnik',
|
timelineEngine: 'Silnik',
|
||||||
timelineEngineAzureMini: 'Azure gpt-4o-mini',
|
timelineEngineAzureMini: 'Standard',
|
||||||
timelineEngineAzureFull: 'Azure gpt-4o',
|
timelineEngineAzureFull: 'Zaawansowany',
|
||||||
timelineEngineHint: 'gpt-4o-mini: 1 kredyt — szybki, sprawdza się w większości osi czasu. gpt-4o: 2 kredyty — wyższa dokładność dla złożonych spraw z wieloma uczestnikami.',
|
timelineEngineHint: 'Standard używa Claude Haiku 4.5 — szybki i dokładny dla większości dokumentów (1 kredyt). Zaawansowany używa Claude Sonnet 4.6 — lepszy dla złożonych spraw z wieloma uczestnikami (2 kredyty).',
|
||||||
timelineAdvancedToggle: 'Ustawienia zaawansowane',
|
timelineAdvancedToggle: 'Ustawienia zaawansowane',
|
||||||
timelineFocus: 'Fokus',
|
timelineFocus: 'Fokus',
|
||||||
timelineFocusAll: 'Wszystkie zdarzenia',
|
timelineFocusAll: 'Wszystkie zdarzenia',
|
||||||
@@ -1854,38 +1854,89 @@ function renderTimeline(events, grouped = false) {
|
|||||||
}
|
}
|
||||||
return '<p class="timeline-empty">No matching events.</p>';
|
return '<p class="timeline-empty">No matching events.</p>';
|
||||||
}
|
}
|
||||||
const MONTH_NAMES = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||||
|
function fmtDate(d) {
|
||||||
|
if (!d || !/^\d{4}-\d{2}-\d{2}/.test(d)) return d || 'unknown';
|
||||||
|
const [y, m, day] = d.split('-');
|
||||||
|
return `${parseInt(day, 10)} ${MONTHS[parseInt(m, 10) - 1] || m} ${y}`;
|
||||||
|
}
|
||||||
|
// Group consecutive events with the same date + actor into one card
|
||||||
|
const groups = [];
|
||||||
|
for (const ev of events) {
|
||||||
|
const key = `${ev.date || ''}||${ev.actor || ''}`;
|
||||||
|
const last = groups[groups.length - 1];
|
||||||
|
if (last && last.key === key) {
|
||||||
|
last.events.push(ev);
|
||||||
|
} else {
|
||||||
|
groups.push({ key, date: ev.date || '', actor: ev.actor || '', events: [ev] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const CONF_RANK = { high: 0, medium: 1, low: 2 };
|
||||||
let lastGroupKey = null;
|
let lastGroupKey = null;
|
||||||
const items = events.map((ev) => {
|
const items = groups.map((grp) => {
|
||||||
const conf = ev.confidence || 'medium';
|
let monthHeader = '';
|
||||||
let groupHeader = '';
|
if (grouped && /^\d{4}-\d{2}/.test(grp.date)) {
|
||||||
if (grouped && /^\d{4}-\d{2}/.test(ev.date || '')) {
|
const year = grp.date.slice(0, 4);
|
||||||
const year = ev.date.slice(0, 4);
|
const mon = grp.date.slice(5, 7);
|
||||||
const mon = ev.date.slice(5, 7);
|
|
||||||
const key = `${year}-${mon}`;
|
const key = `${year}-${mon}`;
|
||||||
if (key !== lastGroupKey) {
|
if (key !== lastGroupKey) {
|
||||||
const prevYear = lastGroupKey ? lastGroupKey.slice(0, 4) : null;
|
const prevYear = lastGroupKey ? lastGroupKey.slice(0, 4) : null;
|
||||||
const label = (year !== prevYear)
|
const label = year !== prevYear
|
||||||
? year
|
? year
|
||||||
: `${MONTH_NAMES[parseInt(mon, 10) - 1] || mon} ${year}`;
|
: `${MONTHS[parseInt(mon, 10) - 1] || mon} ${year}`;
|
||||||
groupHeader = `<li class="timeline-group-header" role="presentation"><span>${escapeHtml(label)}</span></li>`;
|
monthHeader = `<li class="timeline-group-header" role="presentation"><span>${escapeHtml(label)}</span></li>`;
|
||||||
lastGroupKey = key;
|
lastGroupKey = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const excerptHtml = ev.source_excerpt
|
// Worst confidence in the group (drives border colour)
|
||||||
? `<small class="timeline-excerpt${showSources ? '' : ' is-hidden'}">${escapeHtml(ev.source_excerpt)}</small>`
|
const worstConf = grp.events.reduce(
|
||||||
|
(w, ev) => (CONF_RANK[ev.confidence || 'medium'] ?? 1) > (CONF_RANK[w] ?? 1) ? (ev.confidence || 'medium') : w,
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
const lowFlag = worstConf === 'low'
|
||||||
|
? `<span class="confidence-low-flag" title="Low confidence — date or event may be approximate">⚠</span>`
|
||||||
: '';
|
: '';
|
||||||
const copyText = [ev.date, ev.actor, ev.event].filter(Boolean).join(' · ');
|
// Collect unique times across the group
|
||||||
return `${groupHeader}<li class="timeline-item confidence-${escapeHtml(conf)}">
|
const times = [...new Set(grp.events.map((ev) => ev.time).filter(Boolean))];
|
||||||
|
const timeHtml = times.length ? `<span class="timeline-time">${escapeHtml(times.join(', '))}</span>` : '';
|
||||||
|
const displayDate = fmtDate(grp.date);
|
||||||
|
|
||||||
|
if (grp.events.length === 1) {
|
||||||
|
const ev = grp.events[0];
|
||||||
|
const excerptHtml = ev.source_excerpt
|
||||||
|
? `<small class="timeline-excerpt${showSources ? '' : ' is-hidden'}">${escapeHtml(ev.source_excerpt)}</small>`
|
||||||
|
: '';
|
||||||
|
const copyText = [grp.date, grp.actor, ev.event].filter(Boolean).join(' · ');
|
||||||
|
return `${monthHeader}<li class="timeline-item confidence-${escapeHtml(worstConf)}">
|
||||||
|
<div class="timeline-header">
|
||||||
|
<strong class="timeline-date">${escapeHtml(displayDate)}${timeHtml}</strong>
|
||||||
|
${lowFlag}
|
||||||
|
<button type="button" class="timeline-copy-btn" data-copy="${escapeHtml(copyText)}" title="Copy event" aria-label="Copy event to clipboard">📋</button>
|
||||||
|
</div>
|
||||||
|
<span class="timeline-actor">${escapeHtml(grp.actor || 'unknown actor')}</span>
|
||||||
|
<p class="timeline-event">${escapeHtml(ev.event || '')}</p>
|
||||||
|
${excerptHtml}
|
||||||
|
</li>`;
|
||||||
|
}
|
||||||
|
// Multi-event group: render as bullet list
|
||||||
|
const bullets = grp.events.map((ev) => {
|
||||||
|
const excerptHtml = ev.source_excerpt
|
||||||
|
? `<small class="timeline-excerpt${showSources ? '' : ' is-hidden'}">${escapeHtml(ev.source_excerpt)}</small>`
|
||||||
|
: '';
|
||||||
|
const evLow = ev.confidence === 'low'
|
||||||
|
? `<span class="confidence-low-flag" title="Low confidence">⚠</span>`
|
||||||
|
: '';
|
||||||
|
return `<li class="timeline-bullet">${escapeHtml(ev.event || '')}${evLow}${excerptHtml}</li>`;
|
||||||
|
}).join('');
|
||||||
|
const copyText = [grp.date, grp.actor, ...grp.events.map((ev) => ev.event)].filter(Boolean).join('\n');
|
||||||
|
return `${monthHeader}<li class="timeline-item timeline-item--grouped confidence-${escapeHtml(worstConf)}">
|
||||||
<div class="timeline-header">
|
<div class="timeline-header">
|
||||||
<strong class="timeline-date">${escapeHtml(ev.date || 'unknown')}${ev.time ? `<span class="timeline-time"> ${escapeHtml(ev.time)}</span>` : ''}</strong>
|
<strong class="timeline-date">${escapeHtml(displayDate)}${timeHtml}</strong>
|
||||||
${ev.date_type ? `<span class="date-type-badge">${escapeHtml(ev.date_type)}</span>` : ''}
|
${lowFlag}
|
||||||
<span class="confidence-badge confidence-badge--${escapeHtml(conf)}">${escapeHtml(conf)}</span>
|
<button type="button" class="timeline-copy-btn" data-copy="${escapeHtml(copyText)}" title="Copy events" aria-label="Copy events to clipboard">📋</button>
|
||||||
<button type="button" class="timeline-copy-btn" data-copy="${escapeHtml(copyText)}" title="Copy event" aria-label="Copy event to clipboard">📋</button>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="timeline-actor">${escapeHtml(ev.actor || 'unknown actor')}</span>
|
<span class="timeline-actor">${escapeHtml(grp.actor || 'unknown actor')}</span>
|
||||||
<p class="timeline-event">${escapeHtml(ev.event || '')}</p>
|
<ul class="timeline-bullets">${bullets}</ul>
|
||||||
${excerptHtml}
|
|
||||||
</li>`;
|
</li>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
return `<ol class="timeline-list">${items}</ol>`;
|
return `<ol class="timeline-list">${items}</ol>`;
|
||||||
|
|||||||
+19
-7
@@ -506,18 +506,25 @@ PROMPT;
|
|||||||
['role' => 'system', 'content' => $system],
|
['role' => 'system', 'content' => $system],
|
||||||
['role' => 'user', 'content' => $prompt],
|
['role' => 'user', 'content' => $prompt],
|
||||||
];
|
];
|
||||||
$maxTokens = match ($engine) { 'azure_full' => 8000, 'nova_lite' => 2000, default => 4000 };
|
$isBedrock = $this->azure instanceof DbnBedrockGateway;
|
||||||
|
$maxTokens = match ($engine) { 'azure_full', 'claude_sonnet' => 8000, 'nova_lite' => 2000, default => 4000 };
|
||||||
$chatOptions = ['json' => true, 'temperature' => 0.1, 'max_tokens' => $maxTokens, 'timeout' => 120];
|
$chatOptions = ['json' => true, 'temperature' => 0.1, 'max_tokens' => $maxTokens, 'timeout' => 120];
|
||||||
$deployLabel = match ($engine) { 'azure_full' => 'gpt-4o', 'nova_lite' => 'nova-lite', default => 'gpt-4o-mini' };
|
$deployLabel = match (true) {
|
||||||
|
$engine === 'nova_lite' => 'nova-lite',
|
||||||
|
$engine === 'azure_full' || $engine === 'claude_sonnet' => $isBedrock ? 'claude-sonnet-bedrock' : 'gpt-4o',
|
||||||
|
default => $isBedrock ? 'claude-haiku-bedrock' : 'gpt-4o-mini',
|
||||||
|
};
|
||||||
$onProgress && $onProgress("Calling {$deployLabel}\u{2026}");
|
$onProgress && $onProgress("Calling {$deployLabel}\u{2026}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine === 'nova_lite') {
|
if ($engine === 'nova_lite') {
|
||||||
$response = dbnToolsCallGpuLlm($messages, ['model' => 'nova-lite', 'max_tokens' => $maxTokens, 'temperature' => 0.1, 'timeout' => 120]);
|
$response = dbnToolsCallGpuLlm($messages, ['model' => 'nova-lite', 'max_tokens' => $maxTokens, 'temperature' => 0.1, 'timeout' => 120]);
|
||||||
} elseif ($engine === 'azure_full') {
|
} elseif ($engine === 'azure_full' || $engine === 'claude_sonnet') {
|
||||||
$response = $this->azure->withDeployment('gpt-4o')->chat($messages, $chatOptions);
|
$deploy = $isBedrock ? DbnBedrockModelRouter::LITELLM_SONNET : 'gpt-4o';
|
||||||
|
$response = $this->azure->withDeployment($deploy)->chat($messages, $chatOptions);
|
||||||
} else {
|
} else {
|
||||||
$response = $this->azure->withDeployment('gpt-4o-mini')->chat($messages, $chatOptions);
|
$deploy = $isBedrock ? DbnBedrockModelRouter::LITELLM_HAIKU : 'gpt-4o-mini';
|
||||||
|
$response = $this->azure->withDeployment($deploy)->chat($messages, $chatOptions);
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$msg = $e->getMessage();
|
$msg = $e->getMessage();
|
||||||
@@ -570,7 +577,7 @@ PROMPT;
|
|||||||
$events = array_values(array_filter($events, fn($ev) => ($ev['date_type'] ?? 'absolute') === 'absolute'));
|
$events = array_values(array_filter($events, fn($ev) => ($ev['date_type'] ?? 'absolute') === 'absolute'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$engineLabel = match ($engine) { 'azure_full' => 'gpt-4o', 'nova_lite' => 'nova-lite', default => 'gpt-4o-mini' };
|
$engineLabel = $deployLabel;
|
||||||
|
|
||||||
$focusLabel = match ($focus) {
|
$focusLabel = match ($focus) {
|
||||||
'deadlines' => 'legal deadlines',
|
'deadlines' => 'legal deadlines',
|
||||||
@@ -620,7 +627,12 @@ PROMPT;
|
|||||||
?callable $onProgress,
|
?callable $onProgress,
|
||||||
int $inputDateHintCount
|
int $inputDateHintCount
|
||||||
): array {
|
): array {
|
||||||
$engineLabel = match ($engine) { 'azure_full' => 'gpt-4o', 'nova_lite' => 'nova-lite', default => 'gpt-4o-mini' };
|
$isBedrock = $this->azure instanceof DbnBedrockGateway;
|
||||||
|
$engineLabel = match (true) {
|
||||||
|
$engine === 'nova_lite' => 'nova-lite',
|
||||||
|
$engine === 'azure_full' || $engine === 'claude_sonnet' => $isBedrock ? 'claude-sonnet-bedrock' : 'gpt-4o',
|
||||||
|
default => $isBedrock ? 'claude-haiku-bedrock' : 'gpt-4o-mini',
|
||||||
|
};
|
||||||
$chunkSize = $this->timelineChunkSize($engine);
|
$chunkSize = $this->timelineChunkSize($engine);
|
||||||
$chunks = $this->timelineTextChunks($text, $chunkSize, 900);
|
$chunks = $this->timelineTextChunks($text, $chunkSize, 900);
|
||||||
$chunkCount = count($chunks);
|
$chunkCount = count($chunks);
|
||||||
|
|||||||
+3
-3
@@ -26,10 +26,10 @@ require_once __DIR__ . '/includes/layout.php';
|
|||||||
<div class="control-row" id="timelineEngineControl">
|
<div class="control-row" id="timelineEngineControl">
|
||||||
<span class="control-label" data-i18n="timelineEngine">Mode</span>
|
<span class="control-label" data-i18n="timelineEngine">Mode</span>
|
||||||
<label><input type="radio" name="timelineEngine" value="nova_lite" id="timelineEngineNovaLite"> <span>Quick</span> <small class="control-hint">(nova-lite · 1 credit · fastest)</small></label>
|
<label><input type="radio" name="timelineEngine" value="nova_lite" id="timelineEngineNovaLite"> <span>Quick</span> <small class="control-hint">(nova-lite · 1 credit · fastest)</small></label>
|
||||||
<label><input type="radio" name="timelineEngine" value="azure_mini" checked id="timelineEngineAzureMini"> <span data-i18n="timelineEngineAzureMini">Standard</span> ★ <small class="control-hint">(gpt-4o-mini · 1 credit)</small></label>
|
<label><input type="radio" name="timelineEngine" value="azure_mini" checked id="timelineEngineAzureMini"> <span data-i18n="timelineEngineAzureMini">Standard</span> ★ <small class="control-hint">(Claude Haiku · 1 credit)</small></label>
|
||||||
<label><input type="radio" name="timelineEngine" value="azure_full" id="timelineEngineAzureFull"> <span data-i18n="timelineEngineAzureFull">Deep</span> <small class="control-hint">(gpt-4o · 2 credits · Pro)</small></label>
|
<label><input type="radio" name="timelineEngine" value="azure_full" id="timelineEngineAzureFull"> <span data-i18n="timelineEngineAzureFull">Deep</span> <small class="control-hint">(Claude Sonnet · 2 credits · Pro)</small></label>
|
||||||
</div>
|
</div>
|
||||||
<p class="upload-hint" data-i18n="timelineEngineHint">Quick uses Amazon Bedrock nova-lite (fast, cheap). Standard and Deep use Azure OpenAI.</p>
|
<p class="upload-hint" data-i18n="timelineEngineHint">Standard uses Claude Haiku 4.5 — fast and accurate for most documents. Deep uses Claude Sonnet 4.6 — better for complex multi-actor cases.</p>
|
||||||
|
|
||||||
<details class="advanced-panel" id="timelineAdvanced">
|
<details class="advanced-panel" id="timelineAdvanced">
|
||||||
<summary class="advanced-toggle" data-i18n="timelineAdvancedToggle">Advanced settings</summary>
|
<summary class="advanced-toggle" data-i18n="timelineAdvancedToggle">Advanced settings</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user