469 lines
30 KiB
Svelte
469 lines
30 KiB
Svelte
<script lang="ts">
|
||
import AgentIdentityLink from '$lib/components/AgentIdentityLink.svelte';
|
||
import KpiFilters from '$lib/components/KpiFilters.svelte';
|
||
import MarkdownContent from '$lib/components/MarkdownContent.svelte';
|
||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||
import RegistryPagination from '$lib/components/RegistryPagination.svelte';
|
||
|
||
type AgentSummary = { id: string; displayName: string; avatarUrl: string };
|
||
type CommunityComment = {
|
||
id: string;
|
||
bodyHtml: string;
|
||
status: 'visible' | 'removed';
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
moderationReason: string | null;
|
||
targetType: string;
|
||
targetTitle: string;
|
||
targetHref: string;
|
||
author: AgentSummary;
|
||
};
|
||
type CommunityBio = {
|
||
id: string;
|
||
bodyHtml: string;
|
||
status: 'visible' | 'hidden';
|
||
updatedAt: string;
|
||
moderationReason: string | null;
|
||
agent: AgentSummary;
|
||
};
|
||
type QueueItem = {
|
||
id: string;
|
||
source: 'scan' | 'report';
|
||
contentType: 'comment' | 'profile_bio' | 'guide';
|
||
contentId: string;
|
||
bodyHtml: string;
|
||
queuedAt: string;
|
||
scanStatus: string | null;
|
||
riskLevel: string;
|
||
reason: string;
|
||
category: string | null;
|
||
provider: string | null;
|
||
model: string | null;
|
||
providerRequestId: string | null;
|
||
reporterName: string | null;
|
||
scores: Array<{ category: string; score: number }>;
|
||
flags: string[];
|
||
ruleMatches: Array<{ label: string; excerpt: string }>;
|
||
targetType: string;
|
||
targetTitle: string;
|
||
targetHref: string;
|
||
author: AgentSummary;
|
||
};
|
||
type CommunityItem = CommunityComment | CommunityBio;
|
||
type ModerationAction = 'removeComment' | 'restoreComment' | 'hideBio' | 'restoreBio';
|
||
type PageData = {
|
||
tab: 'queue' | 'comments' | 'bios';
|
||
status: string;
|
||
search: string;
|
||
summary: {
|
||
queue: { total: number; automated: number; reported: number; priority: number };
|
||
comments: { visible: number; removed: number };
|
||
bios: { visible: number; hidden: number };
|
||
};
|
||
items: Array<CommunityItem | QueueItem>;
|
||
pagination: any;
|
||
permissions: { comments: boolean; profiles: boolean };
|
||
};
|
||
|
||
let { data, form }: { data: PageData; form?: Record<string, any> | null } = $props();
|
||
let reviewing = $state<CommunityItem | null>(null);
|
||
let moderationAction = $state<ModerationAction | null>(null);
|
||
let moderationReason = $state('');
|
||
let confirming = $state(false);
|
||
let queueReviewing = $state<QueueItem | null>(null);
|
||
let queueDecision = $state<'approve' | 'reject'>('approve');
|
||
let queueReason = $state('');
|
||
let queueConfirming = $state(false);
|
||
let handledResultToken = $state('');
|
||
|
||
const canModerateComments = $derived(data.permissions.comments);
|
||
const canModerateProfiles = $derived(data.permissions.profiles);
|
||
const isQueueItem = (item: CommunityItem | QueueItem): item is QueueItem => 'source' in item;
|
||
const isComment = (item: CommunityItem): item is CommunityComment => 'author' in item;
|
||
const itemAgent = (item: CommunityItem) => isComment(item) ? item.author : item.agent;
|
||
const itemTimestamp = (item: CommunityItem) => isComment(item) ? item.createdAt : item.updatedAt;
|
||
const date = (value: string) => {
|
||
const parsed = new Date(value);
|
||
return Number.isNaN(parsed.valueOf())
|
||
? 'DATE UNKNOWN'
|
||
: new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(parsed);
|
||
};
|
||
const actionRoute = $derived(
|
||
moderationAction === 'removeComment' ? '?/removeComment'
|
||
: moderationAction === 'restoreComment' ? '?/restoreComment'
|
||
: moderationAction === 'hideBio' ? '?/hideBio'
|
||
: '?/restoreBio'
|
||
);
|
||
const actionLabel = $derived(
|
||
moderationAction === 'removeComment' ? 'REMOVE COMMENT'
|
||
: moderationAction === 'restoreComment' ? 'RESTORE COMMENT'
|
||
: moderationAction === 'hideBio' ? 'HIDE PROFILE BIO'
|
||
: 'RESTORE PROFILE BIO'
|
||
);
|
||
const actionConsequence = $derived(
|
||
moderationAction === 'removeComment'
|
||
? 'The comment will no longer appear on its public target or the author’s public activity.'
|
||
: moderationAction === 'restoreComment'
|
||
? 'The comment will return to its public target and the author’s public activity.'
|
||
: moderationAction === 'hideBio'
|
||
? 'The current bio will be hidden from the Agent’s public profile.'
|
||
: 'The bio will return to the Agent’s public profile.'
|
||
);
|
||
const statusItems = $derived(
|
||
data.tab === 'queue'
|
||
? [
|
||
{ value: 'all', label: 'PENDING', count: data.summary.queue.total, description: 'All items requiring human review' },
|
||
{ value: 'priority', label: 'PRIORITY', count: data.summary.queue.priority, description: 'High-risk or failed automated scans' },
|
||
{ value: 'automated', label: 'AUTOMATED', count: data.summary.queue.automated, description: 'Items routed here by moderation scans' },
|
||
{ value: 'reported', label: 'USER REPORTS', count: data.summary.queue.reported, description: 'Content flagged by community members' }
|
||
]
|
||
: data.tab === 'comments'
|
||
? [
|
||
{ value: 'all', label: 'ALL', count: data.summary.comments.visible + data.summary.comments.removed, description: 'All public and moderated comments' },
|
||
{ value: 'visible', label: 'VISIBLE', count: data.summary.comments.visible, description: 'Comments currently visible to the community' },
|
||
{ value: 'removed', label: 'REMOVED', count: data.summary.comments.removed, description: 'Comments removed by Oversight' }
|
||
]
|
||
: [
|
||
{ value: 'all', label: 'ALL', count: data.summary.bios.visible + data.summary.bios.hidden, description: 'All public and moderated bios' },
|
||
{ value: 'visible', label: 'VISIBLE', count: data.summary.bios.visible, description: 'Bios currently visible on Agent profiles' },
|
||
{ value: 'hidden', label: 'HIDDEN', count: data.summary.bios.hidden, description: 'Bios hidden by Oversight' }
|
||
]
|
||
);
|
||
const queueActionLabel = $derived(
|
||
queueDecision === 'approve'
|
||
? queueReviewing?.source === 'report' ? 'KEEP CONTENT' : queueReviewing?.contentType === 'comment' ? 'APPROVE & PUBLISH' : 'APPROVE REVISION'
|
||
: queueReviewing?.source === 'report'
|
||
? queueReviewing?.contentType === 'comment' ? 'REMOVE COMMENT' : `HIDE ${queueReviewing?.contentType === 'profile_bio' ? 'BIO' : 'GUIDE'}`
|
||
: queueReviewing?.contentType === 'comment' ? 'REJECT & REMOVE' : 'REJECT REVISION'
|
||
);
|
||
const queueConsequence = $derived(
|
||
queueDecision === 'approve'
|
||
? queueReviewing?.source === 'report'
|
||
? 'This report will be dismissed. Other reports for the same content will remain pending.'
|
||
: 'This scan will be approved and the queued content will become public.'
|
||
: queueReviewing?.source === 'report'
|
||
? 'The content will be hidden or removed, and all pending reports for it will be resolved.'
|
||
: 'This scan will be rejected. The queued revision will not be published.'
|
||
);
|
||
|
||
function openModeration(item: CommunityItem) {
|
||
reviewing = item;
|
||
moderationAction = isComment(item)
|
||
? item.status === 'removed' ? 'restoreComment' : 'removeComment'
|
||
: item.status === 'hidden' ? 'restoreBio' : 'hideBio';
|
||
moderationReason = '';
|
||
confirming = false;
|
||
}
|
||
|
||
function closeModeration() {
|
||
reviewing = null;
|
||
moderationAction = null;
|
||
moderationReason = '';
|
||
confirming = false;
|
||
}
|
||
|
||
function openQueueReview(item: QueueItem) {
|
||
queueReviewing = item;
|
||
queueDecision = 'approve';
|
||
queueReason = '';
|
||
queueConfirming = false;
|
||
}
|
||
|
||
function closeQueueReview() {
|
||
queueReviewing = null;
|
||
queueReason = '';
|
||
queueConfirming = false;
|
||
}
|
||
|
||
$effect(() => {
|
||
const token = String(form?.communityResultToken ?? '');
|
||
if (!token || token === handledResultToken) return;
|
||
handledResultToken = token;
|
||
if (form?.communitySaved) {
|
||
closeModeration();
|
||
closeQueueReview();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<svelte:head><title>Community Oversight — ISAC</title></svelte:head>
|
||
<svelte:window onkeydown={(event) => {
|
||
if (event.key === 'Escape' && reviewing) closeModeration();
|
||
if (event.key === 'Escape' && queueReviewing) closeQueueReview();
|
||
}}/>
|
||
|
||
<div class="module-toolbar moderation-toolbar">
|
||
<div>
|
||
<p class="eyebrow">COMMUNITY OVERSIGHT / PUBLIC CONTENT CONTROL</p>
|
||
<h2>COMMUNITY OVERSIGHT</h2>
|
||
<p>Resolve automated safety flags and community reports, then inspect the retained comment and biography history when needed.</p>
|
||
</div>
|
||
<div class="queue-readout">
|
||
<span>{data.tab === 'queue' ? 'PENDING REVIEW' : data.tab === 'comments' ? 'VISIBLE COMMENTS' : 'VISIBLE BIOS'}</span>
|
||
<strong>{data.tab === 'queue' ? data.summary.queue.total : data.tab === 'comments' ? data.summary.comments.visible : data.summary.bios.visible}</strong>
|
||
<small>{data.tab === 'queue' ? 'EXCEPTIONS ONLY' : 'PUBLIC RECORDS'}</small>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="community-tabs" aria-label="Community oversight channels">
|
||
<a class:active={data.tab === 'queue'} aria-current={data.tab === 'queue' ? 'page' : undefined} href="/isac/community?tab=queue&status=all">
|
||
<span>REVIEW QUEUE</span><b>{data.summary.queue.total}</b>
|
||
</a>
|
||
{#if canModerateComments}
|
||
<a class:active={data.tab === 'comments'} aria-current={data.tab === 'comments' ? 'page' : undefined} href="/isac/community?tab=comments&status=all">
|
||
<span>COMMENTS</span><b>{data.summary.comments.visible + data.summary.comments.removed}</b>
|
||
</a>
|
||
{/if}
|
||
{#if canModerateProfiles}
|
||
<a class:active={data.tab === 'bios'} aria-current={data.tab === 'bios' ? 'page' : undefined} href="/isac/community?tab=bios&status=all">
|
||
<span>PROFILE BIOS</span><b>{data.summary.bios.visible + data.summary.bios.hidden}</b>
|
||
</a>
|
||
{/if}
|
||
</nav>
|
||
|
||
<KpiFilters parameter="status" active={data.status} items={statusItems}/>
|
||
|
||
<form class="audit-filter community-filter">
|
||
<input type="hidden" name="tab" value={data.tab}/>
|
||
<input type="hidden" name="status" value={data.status}/>
|
||
<input name="search" type="search" value={data.search} placeholder={data.tab === 'queue' ? 'AUTHOR, TARGET, CATEGORY, OR CONTENT' : data.tab === 'comments' ? 'AUTHOR, TARGET, OR COMMENT TEXT' : 'AGENT OR BIO TEXT'}/>
|
||
<button class="secondary">{data.tab === 'queue' ? 'SEARCH QUEUE →' : 'SEARCH COMMUNITY →'}</button>
|
||
</form>
|
||
|
||
<RegistryPagination pagination={data.pagination}/>
|
||
|
||
<div class="community-registry">
|
||
{#each data.items as item}
|
||
{#if isQueueItem(item)}
|
||
<article class="queue-item" class:priority={item.riskLevel === 'high' || item.scanStatus === 'error'}>
|
||
<header>
|
||
<AgentIdentityLink agent={item.author} meta="CONTENT AUTHOR" />
|
||
<div class="community-state">
|
||
<span>{date(item.queuedAt)}</span>
|
||
<b class:visible={item.source === 'report'}>{item.source === 'scan' ? 'AUTOMATED SCAN' : 'USER REPORT'}</b>
|
||
</div>
|
||
</header>
|
||
<div class="community-target">
|
||
<span>{item.contentType.replace('_', ' ').toUpperCase()}</span>
|
||
<a href={item.targetHref}>{item.targetTitle} →</a>
|
||
</div>
|
||
<div class="queue-evidence">
|
||
<div class="queue-signal">
|
||
<span class:priority={item.riskLevel === 'high'}>{item.riskLevel.toUpperCase()}</span>
|
||
<strong>{item.category ? item.category.replace('_', ' ').toUpperCase() : item.scanStatus?.replace('_', ' ').toUpperCase()}</strong>
|
||
</div>
|
||
<p>{item.reason}</p>
|
||
{#if item.reporterName}<small>REPORTED BY {item.reporterName}</small>{/if}
|
||
{#if item.provider}<small>{item.provider.toUpperCase()} / {item.model ?? 'MODEL UNKNOWN'}</small>{/if}
|
||
</div>
|
||
<div class="community-markdown-preview">
|
||
<MarkdownContent html={item.bodyHtml} compact emptyMessage="The queued content is no longer available."/>
|
||
</div>
|
||
<footer>
|
||
<span>{item.source === 'scan' ? `SCAN ${item.id.slice(0,8).toUpperCase()}` : `REPORT ${item.id.slice(0,8).toUpperCase()}`}</span>
|
||
<button class="secondary" onclick={() => openQueueReview(item)}>REVIEW EVIDENCE →</button>
|
||
</footer>
|
||
</article>
|
||
{:else}
|
||
<article class:moderated={item.status === 'removed' || item.status === 'hidden'}>
|
||
<header>
|
||
<AgentIdentityLink agent={itemAgent(item)} meta={isComment(item) ? 'COMMENT AUTHOR' : 'PROFILE OWNER'} />
|
||
<div class="community-state">
|
||
<span>{date(itemTimestamp(item))}</span>
|
||
<b class:visible={item.status === 'visible'}>{item.status.toUpperCase()}</b>
|
||
</div>
|
||
</header>
|
||
{#if isComment(item)}
|
||
<div class="community-target">
|
||
<span>{item.targetType.toUpperCase()}</span>
|
||
<a href={item.targetHref}>{item.targetTitle} →</a>
|
||
</div>
|
||
{:else}
|
||
<div class="community-target"><span>PUBLIC PROFILE BIO</span><a href={`/agents/${encodeURIComponent(item.agent.id)}`}>VIEW AGENT PROFILE →</a></div>
|
||
{/if}
|
||
<div class="community-markdown-preview">
|
||
<MarkdownContent html={item.bodyHtml} compact emptyMessage={isComment(item) ? 'Comment body unavailable.' : 'No bio content.'}/>
|
||
</div>
|
||
{#if item.moderationReason}
|
||
<div class="moderation-evidence"><span>LAST OVERSIGHT REASON</span><p>{item.moderationReason}</p></div>
|
||
{/if}
|
||
<footer>
|
||
<span>{isComment(item) ? `COMMENT ${item.id.slice(0,8).toUpperCase()}` : `AGENT ${item.id.slice(0,8).toUpperCase()}`}</span>
|
||
{#if (isComment(item) && canModerateComments) || (!isComment(item) && canModerateProfiles)}
|
||
<button class={item.status === 'visible' ? 'danger' : 'secondary'} onclick={() => openModeration(item)}>
|
||
{item.status === 'visible' ? isComment(item) ? 'REMOVE COMMENT →' : 'HIDE BIO →' : isComment(item) ? 'RESTORE COMMENT →' : 'RESTORE BIO →'}
|
||
</button>
|
||
{/if}
|
||
</footer>
|
||
</article>
|
||
{/if}
|
||
{:else}
|
||
<div class="empty-dashboard">
|
||
<strong>NO MATCHING {data.tab === 'queue' ? 'REVIEW EXCEPTIONS' : data.tab === 'comments' ? 'COMMENTS' : 'PROFILE BIOS'}</strong>
|
||
<span>{data.tab === 'queue' ? 'Automated checks have not routed any matching content to human Oversight.' : 'Adjust the status or search filters to inspect another part of the community record.'}</span>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
|
||
{#if queueReviewing}
|
||
<div class="isac-modal-backdrop" role="presentation">
|
||
<div class="isac-confirm-modal community-review-modal" role="dialog" aria-modal="true" aria-labelledby="queue-review-title">
|
||
<div class="editor-title">
|
||
<div>
|
||
<p class="eyebrow">{queueReviewing.source === 'scan' ? 'AUTOMATED SAFETY FLAG' : 'COMMUNITY REPORT'} / {queueReviewing.contentType.replace('_', ' ').toUpperCase()}</p>
|
||
<h3 id="queue-review-title">REVIEW CONTENT EXCEPTION</h3>
|
||
</div>
|
||
<button aria-label="Close content review" onclick={closeQueueReview}>×</button>
|
||
</div>
|
||
<div class="community-review-identity">
|
||
<AgentIdentityLink agent={queueReviewing.author} meta="CONTENT AUTHOR" />
|
||
<span>{date(queueReviewing.queuedAt)}</span>
|
||
</div>
|
||
<div class="community-target review-target">
|
||
<span>{queueReviewing.contentType.replace('_', ' ').toUpperCase()}</span>
|
||
<a href={queueReviewing.targetHref}>{queueReviewing.targetTitle} →</a>
|
||
</div>
|
||
<div class="queue-review-evidence">
|
||
<div>
|
||
<span>SOURCE</span><strong>{queueReviewing.source === 'scan' ? 'AUTOMATED SCAN' : 'USER REPORT'}</strong>
|
||
</div>
|
||
<div>
|
||
<span>RISK / CATEGORY</span><strong>{queueReviewing.riskLevel.toUpperCase()} · {(queueReviewing.category ?? queueReviewing.scanStatus ?? 'REVIEW').replace('_', ' ').toUpperCase()}</strong>
|
||
</div>
|
||
{#if queueReviewing.provider}
|
||
<div><span>PROVIDER / MODEL</span><strong>{queueReviewing.provider.toUpperCase()} · {queueReviewing.model ?? 'UNKNOWN'}</strong></div>
|
||
{/if}
|
||
{#if queueReviewing.providerRequestId}
|
||
<div><span>REQUEST ID</span><strong>{queueReviewing.providerRequestId}</strong></div>
|
||
{/if}
|
||
{#if queueReviewing.reporterName}
|
||
<div><span>REPORTER</span><strong>{queueReviewing.reporterName}</strong></div>
|
||
{/if}
|
||
</div>
|
||
<div class="moderation-evidence queue-reason-evidence"><span>ROUTING REASON</span><p>{queueReviewing.reason}</p></div>
|
||
{#if queueReviewing.flags.length || queueReviewing.ruleMatches.length || queueReviewing.scores.length}
|
||
<div class="queue-signals">
|
||
{#if queueReviewing.flags.length}<div><span>FLAGGED CATEGORIES</span><p>{queueReviewing.flags.join(' · ')}</p></div>{/if}
|
||
{#if queueReviewing.ruleMatches.length}<div><span>LOCAL RULES</span>{#each queueReviewing.ruleMatches as match}<p><b>{match.label}</b>{match.excerpt ? ` — ${match.excerpt}` : ''}</p>{/each}</div>{/if}
|
||
{#if queueReviewing.scores.length}<div><span>TOP PROVIDER SCORES</span>{#each queueReviewing.scores as score}<p><b>{score.category}</b> {(score.score * 100).toFixed(1)}%</p>{/each}</div>{/if}
|
||
</div>
|
||
{/if}
|
||
<div class="community-review-content"><MarkdownContent html={queueReviewing.bodyHtml} emptyMessage="The queued content is no longer available."/></div>
|
||
<div class="decision-tabs queue-decision-tabs" role="group" aria-label="Moderation decision">
|
||
<button type="button" class:active={queueDecision === 'approve'} aria-pressed={queueDecision === 'approve'} onclick={() => { queueDecision = 'approve'; queueConfirming = false; }}>
|
||
<b>✓</b><span>{queueReviewing.source === 'report' ? 'KEEP' : 'APPROVE'}<small>{queueReviewing.source === 'report' ? 'DISMISS THIS REPORT' : 'PUBLISH QUEUED CONTENT'}</small></span>
|
||
</button>
|
||
<button type="button" class:active={queueDecision === 'reject'} aria-pressed={queueDecision === 'reject'} onclick={() => { queueDecision = 'reject'; queueConfirming = false; }}>
|
||
<b>×</b><span>{queueReviewing.source === 'report' ? 'REMOVE' : 'REJECT'}<small>{queueReviewing.source === 'report' ? 'RESOLVE ALL REPORTS' : 'KEEP CONTENT HIDDEN'}</small></span>
|
||
</button>
|
||
</div>
|
||
{#if !queueConfirming}
|
||
<label class="field moderation-reason">
|
||
OVERSIGHT REASON — REQUIRED
|
||
<textarea bind:value={queueReason} minlength="3" maxlength="500" required placeholder="Record the evidence and policy basis for this decision…"></textarea>
|
||
</label>
|
||
{#if form?.communityQueueError}<p class="error queue-error">{form.communityQueueError}</p>{/if}
|
||
<div class="moderation-warning" class:rejecting={queueDecision === 'reject'}>
|
||
<b>!</b><span>{queueConsequence} The review decision and reason will be added to the audit history.</span>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="secondary" onclick={closeQueueReview}>CANCEL</button>
|
||
<button type="button" class="primary" disabled={queueReason.trim().length < 3} onclick={() => queueConfirming = true}>REVIEW ACTION →</button>
|
||
</div>
|
||
{:else}
|
||
{@const queueReviewingId = queueReviewing.id}
|
||
{@const queueReviewingSource = queueReviewing.source}
|
||
<div class="community-final-confirm" role="alert">
|
||
<div><span>FINAL CONFIRMATION</span><strong>{queueActionLabel}?</strong><p>{queueConsequence}</p></div>
|
||
<dl><dt>OVERSIGHT REASON</dt><dd>{queueReason}</dd></dl>
|
||
</div>
|
||
<PendingForm action={queueDecision === 'approve' ? '?/approveQueue' : '?/rejectQueue'}>
|
||
{#snippet children(pending)}
|
||
<input type="hidden" name="id" value={queueReviewingId}/>
|
||
<input type="hidden" name="source" value={queueReviewingSource}/>
|
||
<input type="hidden" name="reason" value={queueReason}/>
|
||
{#if form?.communityQueueError}<p class="error">{form.communityQueueError}</p>{/if}
|
||
<div class="form-actions">
|
||
<button type="button" class="secondary" disabled={pending} onclick={() => queueConfirming = false}>BACK</button>
|
||
<button class={queueDecision === 'reject' ? 'danger' : 'primary'} disabled={pending} aria-busy={pending}>
|
||
{pending ? 'PROCESSING…' : `CONFIRM ${queueActionLabel} →`}
|
||
</button>
|
||
</div>
|
||
{/snippet}
|
||
</PendingForm>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if reviewing && moderationAction}
|
||
<div class="isac-modal-backdrop" role="presentation">
|
||
<div class="isac-confirm-modal community-review-modal" role="dialog" aria-modal="true" aria-labelledby="community-review-title">
|
||
<div class="editor-title">
|
||
<div><p class="eyebrow">COMMUNITY CONTROL / {reviewing.status.toUpperCase()}</p><h3 id="community-review-title">{actionLabel}</h3></div>
|
||
<button aria-label="Close community moderation review" onclick={closeModeration}>×</button>
|
||
</div>
|
||
<div class="community-review-identity">
|
||
<AgentIdentityLink agent={itemAgent(reviewing)} meta={isComment(reviewing) ? 'COMMENT AUTHOR' : 'PROFILE OWNER'} />
|
||
<span>{date(itemTimestamp(reviewing))}</span>
|
||
</div>
|
||
{#if isComment(reviewing)}
|
||
<div class="community-target review-target"><span>{reviewing.targetType.toUpperCase()}</span><a href={reviewing.targetHref}>{reviewing.targetTitle} →</a></div>
|
||
{/if}
|
||
<div class="community-review-content"><MarkdownContent html={reviewing.bodyHtml} emptyMessage="No content is available for review."/></div>
|
||
{#if reviewing.moderationReason}<div class="moderation-evidence"><span>PREVIOUS OVERSIGHT REASON</span><p>{reviewing.moderationReason}</p></div>{/if}
|
||
|
||
{#if !confirming}
|
||
<label class="field moderation-reason">
|
||
OVERSIGHT REASON — REQUIRED
|
||
<textarea bind:value={moderationReason} maxlength="500" required placeholder="Record a specific policy, safety, or restoration reason…"></textarea>
|
||
</label>
|
||
{#if form?.communityError}<p class="error">{form.communityError}</p>{/if}
|
||
<div class="moderation-warning" class:rejecting={moderationAction === 'removeComment' || moderationAction === 'hideBio'}>
|
||
<b>!</b><span>{actionConsequence} The original content and this reason remain available to authorized Oversight.</span>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="secondary" onclick={closeModeration}>CANCEL</button>
|
||
<button type="button" class="primary" disabled={!moderationReason.trim()} onclick={() => confirming = true}>REVIEW ACTION →</button>
|
||
</div>
|
||
{:else}
|
||
{@const reviewingId = reviewing.id}
|
||
<div class="community-final-confirm" role="alert">
|
||
<div><span>FINAL CONFIRMATION</span><strong>{actionLabel}?</strong><p>{actionConsequence}</p></div>
|
||
<dl><dt>OVERSIGHT REASON</dt><dd>{moderationReason}</dd></dl>
|
||
</div>
|
||
<PendingForm action={actionRoute}>
|
||
{#snippet children(pending)}
|
||
<input type="hidden" name="id" value={reviewingId}/>
|
||
<input type="hidden" name="reason" value={moderationReason}/>
|
||
{#if form?.communityError}<p class="error">{form.communityError}</p>{/if}
|
||
<div class="form-actions">
|
||
<button type="button" class="secondary" disabled={pending} onclick={() => confirming = false}>BACK</button>
|
||
<button class={moderationAction === 'removeComment' || moderationAction === 'hideBio' ? 'danger' : 'primary'} disabled={pending} aria-busy={pending}>
|
||
{pending ? 'PROCESSING…' : `CONFIRM ${actionLabel} →`}
|
||
</button>
|
||
</div>
|
||
{/snippet}
|
||
</PendingForm>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<style>
|
||
.community-tabs{height:auto;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));margin:0 0 8px;border:1px solid var(--shd-border);background:#0d1011}
|
||
.community-tabs a{min-height:46px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 11px;border-left:1px solid var(--shd-border);border-bottom:2px solid transparent;color:var(--shd-text-muted)}.community-tabs a:first-child{border-left:0}.community-tabs a span,.community-tabs a b{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.community-tabs a b{color:var(--shd-text-dim)}.community-tabs a:hover,.community-tabs a:focus-visible{background:var(--shd-surface-active);color:var(--shd-text)}.community-tabs a.active{border-bottom-color:var(--shd-orange);background:#211a0e;color:var(--shd-orange)}.community-tabs a.active b{color:var(--shd-orange)}
|
||
.community-filter{grid-template-columns:minmax(0,1fr) auto}.community-registry{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:7px}.community-registry>article{min-width:0;display:grid;grid-template-rows:auto auto minmax(100px,1fr) auto;border:1px solid var(--shd-border);border-left:2px solid var(--shd-info);background:var(--shd-surface)}.community-registry>article.moderated{border-left-color:var(--shd-danger)}
|
||
.community-registry article>header{min-height:55px;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 10px;border-bottom:1px solid var(--shd-border);background:var(--shd-surface-raised)}.community-registry article>header :global(.agent-identity){flex:1}.community-state{display:grid;justify-items:end;gap:4px}.community-state span,.community-state b{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.community-state span{color:var(--shd-text-muted)}.community-state b{padding:3px 5px;border:1px solid var(--shd-danger);color:var(--shd-danger)}.community-state b.visible{border-color:var(--shd-success);color:var(--shd-success)}
|
||
.community-target{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 10px;border-bottom:1px solid var(--shd-border-soft);background:#0d1011}.community-target span,.community-target a{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.community-target span{color:var(--shd-text-muted)}.community-target a{overflow:hidden;color:var(--shd-orange);text-overflow:ellipsis;white-space:nowrap}.community-markdown-preview{max-height:150px;overflow:hidden;padding:11px;position:relative}.community-markdown-preview:after{position:absolute;right:0;bottom:0;left:0;height:25px;background:linear-gradient(transparent,var(--shd-surface));content:"";pointer-events:none}
|
||
.community-registry>article.queue-item{grid-template-rows:auto auto auto minmax(100px,1fr) auto;border-left:1px solid var(--shd-info)}.community-registry>article.queue-item.priority{border-color:var(--shd-danger)}
|
||
.queue-evidence{display:grid;gap:5px;padding:9px 10px;border-bottom:1px solid var(--shd-border);background:var(--shd-surface-raised)}.queue-evidence p{margin:0;color:var(--shd-text);line-height:1.4;overflow-wrap:anywhere}.queue-evidence small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px;overflow-wrap:anywhere}.queue-signal{display:flex;align-items:center;gap:7px}.queue-signal span,.queue-signal strong{padding:3px 5px;border:1px solid var(--shd-border-strong);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.queue-signal span.priority{border-color:var(--shd-danger);color:var(--shd-danger)}
|
||
.moderation-evidence{display:grid;gap:4px;margin:0 10px 9px;padding:8px;border:1px solid var(--shd-border);border-left:2px solid var(--shd-danger);background:#1b1111}.moderation-evidence span{color:var(--shd-danger);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.moderation-evidence p{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
|
||
.community-registry article>footer{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-top:1px solid var(--shd-border);background:var(--shd-surface-raised)}.community-registry article>footer>span{color:var(--shd-text-dim);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.community-registry article>footer button{min-height:36px;padding:0 10px}
|
||
.community-review-modal{width:min(900px,calc(100vw - 28px));max-height:calc(100vh - 28px);overflow:auto}.community-review-identity{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px;border-bottom:1px solid var(--shd-border)}.community-review-identity :global(.agent-identity){flex:1}.community-review-identity>span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900}.review-target{border-bottom:1px solid var(--shd-border)}.community-review-content{max-height:330px;overflow:auto;padding:14px;border-bottom:1px solid var(--shd-border);background:#0d1011}.community-review-content :global(.markdown-content){max-width:none}.moderation-reason{margin:10px}.moderation-reason textarea{min-height:90px}.community-final-confirm{display:grid;grid-template-columns:minmax(0,1fr) minmax(220px,.65fr);gap:9px;margin:10px;padding:10px;border:1px solid var(--shd-orange);background:#1b160d}.community-final-confirm>div{display:grid;gap:4px}.community-final-confirm span,.community-final-confirm dt{color:var(--shd-orange);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.community-final-confirm p,.community-final-confirm dd{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}.community-final-confirm dl{display:grid;align-content:start;gap:5px;margin:0;padding:8px;border:1px solid var(--shd-border);background:#0d1011}.community-review-modal>.moderation-warning,.community-review-modal>.form-actions,.community-review-modal :global(form > .form-actions),.community-review-modal :global(form > .error){margin-right:10px;margin-left:10px}
|
||
.queue-review-evidence{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));border-bottom:1px solid var(--shd-border)}.queue-review-evidence>div{min-width:0;display:grid;gap:3px;padding:8px 10px;border-right:1px solid var(--shd-border);border-top:1px solid var(--shd-border-soft)}.queue-review-evidence span,.queue-signals span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.queue-review-evidence strong{overflow-wrap:anywhere}.queue-reason-evidence{margin-top:10px}.queue-signals{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;margin:0 10px 10px;border:1px solid var(--shd-border);background:var(--shd-border)}.queue-signals>div{min-width:0;padding:8px;background:#0d1011}.queue-signals p{margin:4px 0 0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);overflow-wrap:anywhere}.queue-decision-tabs{margin:10px}.queue-error{margin:0 10px}
|
||
@media(max-width:900px){.community-registry{grid-template-columns:1fr}}
|
||
@media(max-width:650px){.community-tabs{grid-template-columns:1fr}.community-tabs a{min-height:52px;border-top:1px solid var(--shd-border);border-left:0}.community-tabs a:first-child{border-top:0}.community-filter{grid-template-columns:1fr}.community-filter button{min-height:44px}.community-registry article>header,.community-registry article>footer,.community-review-identity,.community-target{align-items:stretch;flex-direction:column}.community-state{justify-items:start}.community-target a{white-space:normal}.community-registry article>footer button{width:100%;min-height:44px}.community-final-confirm,.queue-review-evidence,.queue-signals{grid-template-columns:1fr}.community-review-modal>.form-actions,.community-review-modal :global(.form-actions){display:grid;grid-template-columns:1fr 1fr}.community-review-modal button{min-height:44px}}
|
||
@media(prefers-reduced-motion:reduce){.community-tabs a,.community-registry button{transition:none}}
|
||
</style>
|