674 lines
28 KiB
Svelte
674 lines
28 KiB
Svelte
<script lang="ts">
|
||
import { page } from '$app/state';
|
||
import PseudoScript from '$lib/components/PseudoScript.svelte';
|
||
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
|
||
import type {
|
||
AiMechanicAuthoringResponse,
|
||
AiMechanicContext,
|
||
AiMechanicTranscriptEntry,
|
||
AiMechanicValueColumn
|
||
} from '$lib/ai/mechanic-authoring';
|
||
import {
|
||
buildMechanicDeveloperPrompt,
|
||
type AiMechanicDeveloperHandoff
|
||
} from '$lib/ai/mechanic-developer-handoff';
|
||
import type { ConditionalMechanicDraftV1 } from '$lib/calculations/mechanic-editor';
|
||
import { onDestroy, tick, untrack } from 'svelte';
|
||
|
||
type ClarificationAnswer = string | number | boolean;
|
||
|
||
let {
|
||
context,
|
||
currentDraft,
|
||
valueColumns,
|
||
idPrefix = 'mechanic',
|
||
allowAssignedAbility = false,
|
||
allowProtocolUpgradeScaling = false,
|
||
disabled = false,
|
||
onapply
|
||
}: {
|
||
context: AiMechanicContext;
|
||
currentDraft: ConditionalMechanicDraftV1;
|
||
valueColumns: AiMechanicValueColumn[];
|
||
idPrefix?: string;
|
||
allowAssignedAbility?: boolean;
|
||
allowProtocolUpgradeScaling?: boolean;
|
||
disabled?: boolean;
|
||
onapply: (draft: ConditionalMechanicDraftV1) => void;
|
||
} = $props();
|
||
|
||
const permissions = $derived((page.data.permissions as string[] | undefined) ?? []);
|
||
const canGenerate = $derived(permissions.includes('*') || permissions.includes('ai.mechanic.generate'));
|
||
const canUseAssistant = $derived(canGenerate && !disabled);
|
||
|
||
let expanded = $state(false);
|
||
let sourceText = $state('');
|
||
let response = $state<AiMechanicAuthoringResponse | null>(null);
|
||
let transcript = $state<AiMechanicTranscriptEntry[]>([]);
|
||
let answers = $state<Record<string, ClarificationAnswer | undefined>>({});
|
||
let pending = $state(false);
|
||
let errorMessage = $state('');
|
||
let activityMessage = $state('');
|
||
let confirmApply = $state(false);
|
||
let gapHandoff = $state<AiMechanicDeveloperHandoff | null>(null);
|
||
let gapReportPending = $state(false);
|
||
let gapReportSubmitted = $state(false);
|
||
let handoffMessage = $state('');
|
||
let handoffMessageKind = $state<'success' | 'error'>('success');
|
||
let resultRegion = $state<HTMLElement>();
|
||
let requestController: AbortController | undefined;
|
||
let trackedContextKey = $state('');
|
||
const contextIdentity = $derived(`${context.sourceKind}:${context.sourceName}`);
|
||
const contextKey = $derived(
|
||
`${idPrefix}-${context.sourceKind}-${context.sourceName}`
|
||
.toLocaleLowerCase()
|
||
.replace(/[^a-z0-9_-]+/g, '-')
|
||
.replace(/^-+|-+$/g, '')
|
||
|| 'mechanic'
|
||
);
|
||
|
||
const questions = $derived(
|
||
response?.status === 'clarification_required' ? response.questions : []
|
||
);
|
||
const allQuestionsAnswered = $derived(
|
||
questions.length > 0 && questions.every((question) => {
|
||
const answer = answers[question.id];
|
||
if (typeof answer === 'string') return answer.trim().length > 0;
|
||
if (typeof answer === 'number') return Number.isFinite(answer);
|
||
return typeof answer === 'boolean';
|
||
})
|
||
);
|
||
const pseudoScriptLines = $derived(
|
||
response?.status === 'draft_ready'
|
||
? response.pseudoScript.split('\n')
|
||
: []
|
||
);
|
||
const draftReadiness = $derived(
|
||
response?.status !== 'draft_ready'
|
||
? undefined
|
||
: response.draft.simulationReadiness === 'ready'
|
||
? 'READY'
|
||
: response.draft.simulationReadiness === 'requires_assumptions'
|
||
? 'REQUIRES ASSUMPTIONS'
|
||
: 'CAPTURED ONLY'
|
||
);
|
||
const currentDraftCounts = $derived(summarizeDraft(currentDraft));
|
||
const proposedDraftCounts = $derived(
|
||
response?.status === 'draft_ready' ? summarizeDraft(response.draft) : null
|
||
);
|
||
|
||
function summarizeDraft(draft: ConditionalMechanicDraftV1) {
|
||
return {
|
||
branches: draft.clauses.length,
|
||
pools: draft.pools.length,
|
||
controls: draft.scenarioInputs.length,
|
||
outcomes: draft.clauses.reduce((total, clause) => total + clause.outcomes.length, 0)
|
||
};
|
||
}
|
||
|
||
function countLabel(value: number, singular: string) {
|
||
return `${value} ${singular}${value === 1 ? '' : 's'}`;
|
||
}
|
||
|
||
function formatDraftCounts(counts: ReturnType<typeof summarizeDraft>) {
|
||
return [
|
||
countLabel(counts.branches, 'branch'),
|
||
countLabel(counts.pools, 'pool'),
|
||
countLabel(counts.controls, 'control'),
|
||
countLabel(counts.outcomes, 'outcome')
|
||
].join(' · ');
|
||
}
|
||
|
||
$effect(() => {
|
||
const nextKey = contextIdentity;
|
||
if (nextKey === trackedContextKey) return;
|
||
trackedContextKey = nextKey;
|
||
untrack(() => resetAssistant(context.sourceDescription ?? ''));
|
||
});
|
||
|
||
onDestroy(() => requestController?.abort());
|
||
|
||
function resetAssistant(description = context.sourceDescription ?? '') {
|
||
requestController?.abort();
|
||
sourceText = description;
|
||
response = null;
|
||
transcript = [];
|
||
answers = {};
|
||
pending = false;
|
||
errorMessage = '';
|
||
activityMessage = '';
|
||
confirmApply = false;
|
||
gapHandoff = null;
|
||
gapReportPending = false;
|
||
gapReportSubmitted = false;
|
||
handoffMessage = '';
|
||
handoffMessageKind = 'success';
|
||
}
|
||
|
||
function setAnswer(questionId: string, answer: ClarificationAnswer | undefined) {
|
||
answers = { ...answers, [questionId]: answer };
|
||
}
|
||
|
||
async function focusResult() {
|
||
await tick();
|
||
resultRegion?.focus();
|
||
}
|
||
|
||
async function requestDraft(nextTranscript = transcript) {
|
||
if (!canUseAssistant || pending) return;
|
||
if (!sourceText.trim()) {
|
||
errorMessage = 'Enter the mechanic wording from the game before requesting a draft.';
|
||
activityMessage = '';
|
||
return;
|
||
}
|
||
|
||
requestController?.abort();
|
||
requestController = new AbortController();
|
||
pending = true;
|
||
errorMessage = '';
|
||
activityMessage = '';
|
||
confirmApply = false;
|
||
gapHandoff = null;
|
||
gapReportSubmitted = false;
|
||
handoffMessage = '';
|
||
|
||
try {
|
||
const authoringRequest = {
|
||
context: { ...context, sourceDescription: sourceText.trim() },
|
||
currentDraft,
|
||
valueColumns,
|
||
allowAssignedAbility,
|
||
allowProtocolUpgradeScaling,
|
||
transcript: nextTranscript
|
||
};
|
||
const request = await fetch('/api/isac/mechanic-assistant', {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
signal: requestController.signal,
|
||
body: JSON.stringify(authoringRequest)
|
||
});
|
||
const contentType = request.headers.get('content-type') ?? '';
|
||
const payload = contentType.includes('application/json')
|
||
? await request.json()
|
||
: null;
|
||
|
||
if (!request.ok) {
|
||
throw new Error(
|
||
typeof payload?.message === 'string'
|
||
? payload.message
|
||
: `The authoring assistant could not complete the request (${request.status}).`
|
||
);
|
||
}
|
||
if (
|
||
!payload
|
||
|| !['clarification_required', 'draft_ready', 'capability_gap'].includes(payload.status)
|
||
) {
|
||
throw new Error('The authoring assistant returned an unrecognized response.');
|
||
}
|
||
|
||
response = payload as AiMechanicAuthoringResponse;
|
||
if (response.status === 'capability_gap') {
|
||
gapHandoff = {
|
||
context: authoringRequest.context,
|
||
valueColumns: authoringRequest.valueColumns,
|
||
allowAssignedAbility: authoringRequest.allowAssignedAbility,
|
||
allowProtocolUpgradeScaling: authoringRequest.allowProtocolUpgradeScaling,
|
||
transcript: authoringRequest.transcript,
|
||
response
|
||
};
|
||
}
|
||
answers = {};
|
||
activityMessage = response.status === 'clarification_required'
|
||
? 'Clarification required before a reliable draft can be created.'
|
||
: response.status === 'draft_ready'
|
||
? 'Draft ready for review. Nothing has been applied or saved.'
|
||
: 'The current mechanic language cannot represent every requested behavior.';
|
||
await focusResult();
|
||
} catch (error) {
|
||
if (error instanceof DOMException && error.name === 'AbortError') return;
|
||
errorMessage = error instanceof Error
|
||
? error.message
|
||
: 'The authoring assistant could not complete the request.';
|
||
await focusResult();
|
||
} finally {
|
||
pending = false;
|
||
}
|
||
}
|
||
|
||
function submitClarifications() {
|
||
if (!allQuestionsAnswered || response?.status !== 'clarification_required') return;
|
||
const entries = response.questions.map((question): AiMechanicTranscriptEntry => ({
|
||
questionId: question.id,
|
||
question: question.question,
|
||
answer: String(answers[question.id])
|
||
}));
|
||
const nextTranscript = [...transcript, ...entries];
|
||
transcript = nextTranscript;
|
||
void requestDraft(nextTranscript);
|
||
}
|
||
|
||
function restartAnalysis() {
|
||
response = null;
|
||
transcript = [];
|
||
answers = {};
|
||
errorMessage = '';
|
||
activityMessage = '';
|
||
confirmApply = false;
|
||
gapHandoff = null;
|
||
gapReportSubmitted = false;
|
||
handoffMessage = '';
|
||
void requestDraft([]);
|
||
}
|
||
|
||
function applyDraft() {
|
||
if (response?.status !== 'draft_ready') return;
|
||
onapply($state.snapshot(response.draft));
|
||
confirmApply = false;
|
||
activityMessage = 'AI draft applied to the manual editor. Review it before saving this item.';
|
||
}
|
||
|
||
async function copyDeveloperPrompt() {
|
||
if (!gapHandoff) return;
|
||
try {
|
||
await navigator.clipboard.writeText(buildMechanicDeveloperPrompt(gapHandoff));
|
||
handoffMessageKind = 'success';
|
||
handoffMessage = 'Developer prompt copied to the clipboard.';
|
||
} catch {
|
||
handoffMessageKind = 'error';
|
||
handoffMessage = 'Clipboard access was blocked. Check this browser’s clipboard permission and try again.';
|
||
}
|
||
}
|
||
|
||
async function submitGapReport() {
|
||
if (!gapHandoff || gapReportPending || gapReportSubmitted) return;
|
||
gapReportPending = true;
|
||
handoffMessage = '';
|
||
try {
|
||
const request = await fetch('/api/isac/mechanic-gap-report', {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify(gapHandoff)
|
||
});
|
||
const payload = request.headers.get('content-type')?.includes('application/json')
|
||
? await request.json()
|
||
: null;
|
||
if (!request.ok) {
|
||
throw new Error(
|
||
typeof payload?.message === 'string'
|
||
? payload.message
|
||
: `The mechanic gap could not be submitted (${request.status}).`
|
||
);
|
||
}
|
||
gapReportSubmitted = true;
|
||
handoffMessageKind = 'success';
|
||
handoffMessage = payload?.alreadySubmitted
|
||
? 'This mechanic gap was already submitted to the admin team.'
|
||
: `Mechanic gap submitted to ${Number(payload?.recipientCount ?? 0)} ${Number(payload?.recipientCount ?? 0) === 1 ? 'administrator' : 'administrators'}.`;
|
||
} catch (error) {
|
||
handoffMessageKind = 'error';
|
||
handoffMessage = error instanceof Error
|
||
? error.message
|
||
: 'The mechanic gap could not be submitted to the admin team.';
|
||
} finally {
|
||
gapReportPending = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<section class="authoring-assistant" class:locked={!canGenerate} aria-labelledby={`${contextKey}-assistant-title`}>
|
||
<header>
|
||
<div>
|
||
<span>ISAC AUTHORING ASSIST</span>
|
||
<strong id={`${contextKey}-assistant-title`}>AI MECHANIC DRAFTING</strong>
|
||
</div>
|
||
<b class:available={canGenerate}>{canGenerate ? 'AUTHORIZED' : 'PERMISSION REQUIRED'}</b>
|
||
<button
|
||
type="button"
|
||
class="assistant-toggle"
|
||
aria-expanded={expanded}
|
||
aria-controls={`${contextKey}-assistant-workspace`}
|
||
disabled={!canGenerate}
|
||
onclick={() => expanded = !expanded}
|
||
>
|
||
{expanded ? 'CLOSE ASSISTANT' : 'OPEN ASSISTANT'}
|
||
</button>
|
||
</header>
|
||
|
||
{#if !canGenerate}
|
||
<p class="permission-note">
|
||
AI assistance is optional. Manual mechanic authoring remains available below.
|
||
Request <b>AI mechanic generation</b> permission from an administrator to use this helper.
|
||
</p>
|
||
{:else if !expanded}
|
||
<p class="assistant-summary">
|
||
Convert exact in-game wording into a reviewable mechanic draft. The assistant cannot save,
|
||
publish, or replace the manual editor.
|
||
</p>
|
||
{/if}
|
||
|
||
{#if expanded}
|
||
<div class="assistant-workspace" id={`${contextKey}-assistant-workspace`}>
|
||
<div class="source-heading">
|
||
<div>
|
||
<span>SOURCE WORDING</span>
|
||
<strong>{context.sourceName || 'UNTITLED MECHANIC'}</strong>
|
||
</div>
|
||
<small>Paste the wording exactly as it appears in the game.</small>
|
||
</div>
|
||
|
||
<label class="source-field">
|
||
<span>MECHANIC DESCRIPTION</span>
|
||
<textarea
|
||
rows="5"
|
||
bind:value={sourceText}
|
||
disabled={!canUseAssistant || pending}
|
||
placeholder="Enter the complete in-game talent or effect wording."
|
||
></textarea>
|
||
</label>
|
||
|
||
<div class="request-actions">
|
||
<p>
|
||
Generated data is a draft. Applying it never saves the parent record and does not
|
||
remove access to the manual controls.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
class="primary request-button"
|
||
disabled={!canUseAssistant || pending || !sourceText.trim()}
|
||
aria-busy={pending}
|
||
onclick={() => response || transcript.length ? restartAnalysis() : requestDraft()}
|
||
>
|
||
{pending ? 'ANALYZING MECHANIC…' : transcript.length ? 'RESTART ANALYSIS' : 'GENERATE DRAFT'}
|
||
</button>
|
||
</div>
|
||
|
||
<div
|
||
class="result-region"
|
||
bind:this={resultRegion}
|
||
tabindex="-1"
|
||
aria-live="polite"
|
||
aria-busy={pending}
|
||
>
|
||
{#if errorMessage}
|
||
<div class="assistant-message error" role="alert">
|
||
<strong>REQUEST FAILED</strong>
|
||
<p>{errorMessage}</p>
|
||
</div>
|
||
{:else if response?.status === 'clarification_required'}
|
||
<div class="result-heading">
|
||
<div>
|
||
<span>CLARIFICATION REQUIRED</span>
|
||
<strong>{response.questions.length} {response.questions.length === 1 ? 'QUESTION' : 'QUESTIONS'}</strong>
|
||
</div>
|
||
<small>Answers are used only to refine this mechanic draft.</small>
|
||
</div>
|
||
|
||
<div class="question-stack">
|
||
{#each response.questions as question, index}
|
||
<section class="question-card">
|
||
<header>
|
||
<span>{String(index + 1).padStart(2, '0')}</span>
|
||
<div>
|
||
<strong>{question.question}</strong>
|
||
<small>{question.reason}</small>
|
||
</div>
|
||
</header>
|
||
|
||
{#if question.answerType === 'boolean'}
|
||
<SegmentedChoice
|
||
legend="ANSWER"
|
||
name={`${contextKey}-${question.id}`}
|
||
options={[
|
||
{ value: 'true', label: 'YES' },
|
||
{ value: 'false', label: 'NO' }
|
||
]}
|
||
value={answers[question.id] === undefined ? '' : String(answers[question.id])}
|
||
onchange={(values) => setAnswer(question.id, values[0] === 'true')}
|
||
/>
|
||
{:else if question.answerType === 'choice' && question.options?.length}
|
||
<SegmentedChoice
|
||
legend="ANSWER"
|
||
name={`${contextKey}-${question.id}`}
|
||
options={question.options.map((option) => ({ value: option, label: option.toUpperCase() }))}
|
||
value={typeof answers[question.id] === 'string' ? answers[question.id] as string : ''}
|
||
columns={Math.min(3, question.options.length)}
|
||
onchange={(values) => setAnswer(question.id, values[0])}
|
||
/>
|
||
{:else}
|
||
<label class="answer-field">
|
||
<span>ANSWER</span>
|
||
<input
|
||
type={question.answerType === 'number' ? 'number' : 'text'}
|
||
step={question.answerType === 'number' ? 'any' : undefined}
|
||
value={answers[question.id] ?? ''}
|
||
oninput={(event) => setAnswer(
|
||
question.id,
|
||
question.answerType === 'number'
|
||
? event.currentTarget.value === ''
|
||
? undefined
|
||
: Number(event.currentTarget.value)
|
||
: event.currentTarget.value
|
||
)}
|
||
/>
|
||
</label>
|
||
{/if}
|
||
</section>
|
||
{/each}
|
||
</div>
|
||
|
||
<div class="result-actions">
|
||
<button type="button" class="secondary" onclick={() => resetAssistant(sourceText)}>
|
||
CANCEL QUESTIONS
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="primary"
|
||
disabled={!allQuestionsAnswered || pending}
|
||
aria-busy={pending}
|
||
onclick={submitClarifications}
|
||
>
|
||
{pending ? 'REFINING DRAFT…' : 'SUBMIT ANSWERS'}
|
||
</button>
|
||
</div>
|
||
{:else if response?.status === 'capability_gap'}
|
||
<div class="result-heading capability">
|
||
<div>
|
||
<span>DEVELOPER HANDOFF</span>
|
||
<strong>MECHANIC LANGUAGE GAP</strong>
|
||
</div>
|
||
<small>The assistant did not invent unsupported calculation behavior.</small>
|
||
</div>
|
||
<p class="gap-summary">{response.summary}</p>
|
||
<div class="gap-stack">
|
||
{#each response.gaps as gap}
|
||
<article class="gap-card">
|
||
<header><span>AREA</span><strong>{gap.area}</strong></header>
|
||
<dl>
|
||
<div><dt>REQUIRED BEHAVIOR</dt><dd>{gap.requiredBehavior}</dd></div>
|
||
<div><dt>WHY UNSUPPORTED</dt><dd>{gap.whyUnsupported}</dd></div>
|
||
<div><dt>DEVELOPER CHANGE</dt><dd>{gap.developerChange}</dd></div>
|
||
{#if gap.workaround}<div><dt>CURRENT WORKAROUND</dt><dd>{gap.workaround}</dd></div>{/if}
|
||
</dl>
|
||
</article>
|
||
{/each}
|
||
</div>
|
||
<div class="result-actions handoff-actions">
|
||
<button type="button" class="secondary" onclick={() => resetAssistant(sourceText)}>
|
||
REVISE SOURCE WORDING
|
||
</button>
|
||
<div>
|
||
<button type="button" class="secondary" disabled={!gapHandoff} onclick={copyDeveloperPrompt}>
|
||
COPY DEVELOPER PROMPT
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="primary"
|
||
disabled={!gapHandoff || gapReportPending || gapReportSubmitted}
|
||
aria-busy={gapReportPending}
|
||
onclick={submitGapReport}
|
||
>
|
||
{gapReportPending
|
||
? 'SUBMITTING GAP…'
|
||
: gapReportSubmitted
|
||
? 'SUBMITTED TO ADMIN TEAM'
|
||
: 'SUBMIT TO ADMIN TEAM'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{#if handoffMessage}
|
||
<p
|
||
class="handoff-message"
|
||
class:error={handoffMessageKind === 'error'}
|
||
role={handoffMessageKind === 'error' ? 'alert' : 'status'}
|
||
>{handoffMessage}</p>
|
||
{/if}
|
||
{:else if response?.status === 'draft_ready'}
|
||
<div class="result-heading ready">
|
||
<div>
|
||
<span>DRAFT READY</span>
|
||
<strong>REVIEW BEFORE APPLICATION</strong>
|
||
</div>
|
||
<small>No changes have been applied or saved.</small>
|
||
</div>
|
||
<p class="draft-summary">{response.summary}</p>
|
||
|
||
{#if response.assumptions.length || response.warnings.length}
|
||
<div class="review-notes">
|
||
{#if response.assumptions.length}
|
||
<section>
|
||
<strong>ASSUMPTIONS</strong>
|
||
<ul>{#each response.assumptions as assumption}<li>{assumption}</li>{/each}</ul>
|
||
</section>
|
||
{/if}
|
||
{#if response.warnings.length}
|
||
<section class="warnings">
|
||
<strong>REVIEW WARNINGS</strong>
|
||
<ul>{#each response.warnings as warning}<li>{warning}</li>{/each}</ul>
|
||
</section>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
|
||
<PseudoScript
|
||
title="PROPOSED MECHANIC PROGRAM"
|
||
lines={pseudoScriptLines}
|
||
readiness={draftReadiness}
|
||
/>
|
||
|
||
{#if confirmApply}
|
||
<div class="apply-confirm" role="alert">
|
||
<div>
|
||
<strong>APPLY THIS DRAFT TO THE MANUAL EDITOR?</strong>
|
||
<p>
|
||
This replaces the current unsaved mechanic fields. It does not save or publish
|
||
the parent record.
|
||
</p>
|
||
{#if proposedDraftCounts}
|
||
<div class="replacement-summary">
|
||
<span><b>CURRENT</b>{formatDraftCounts(currentDraftCounts)}</span>
|
||
<span><b>PROPOSED</b>{formatDraftCounts(proposedDraftCounts)}</span>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
<div>
|
||
<button type="button" class="secondary" onclick={() => confirmApply = false}>
|
||
CANCEL
|
||
</button>
|
||
<button type="button" class="primary" onclick={applyDraft}>
|
||
CONFIRM APPLY
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{:else}
|
||
<div class="result-actions">
|
||
<button type="button" class="secondary" onclick={() => resetAssistant(sourceText)}>
|
||
DISCARD DRAFT
|
||
</button>
|
||
<button type="button" class="primary" onclick={() => confirmApply = true}>
|
||
APPLY TO EDITOR
|
||
</button>
|
||
</div>
|
||
{/if}
|
||
{:else if pending}
|
||
<div class="assistant-message pending">
|
||
<strong>ANALYZING MECHANIC</strong>
|
||
<p>Comparing the source wording with supported events, attributes, and value models.</p>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
|
||
{#if activityMessage}
|
||
<p class="activity-message" role="status">{activityMessage}</p>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</section>
|
||
|
||
<style>
|
||
.authoring-assistant{border:1px solid var(--shd-border);border-left:2px solid var(--shd-info);background:var(--shd-surface)}
|
||
.authoring-assistant>header{min-height:46px;display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:var(--shd-gap-sm);padding:7px 9px;border-bottom:1px solid var(--shd-border);background:var(--shd-surface-raised)}
|
||
.authoring-assistant>header>div,.source-heading>div,.result-heading>div{display:grid;gap:2px}
|
||
.authoring-assistant>header span,.source-heading span,.result-heading span{color:var(--shd-info);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.9px}
|
||
.authoring-assistant>header b{padding:5px 7px;border:1px solid var(--shd-border);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);letter-spacing:.7px}
|
||
.authoring-assistant>header b.available{border-color:color-mix(in srgb,var(--shd-info) 55%,var(--shd-border));color:var(--shd-info)}
|
||
.assistant-toggle{min-height:36px;padding:0 11px;border:1px solid var(--shd-info);background:transparent;color:var(--shd-info);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px;cursor:pointer}
|
||
.assistant-toggle:hover,.assistant-toggle:focus-visible{background:color-mix(in srgb,var(--shd-info) 12%,var(--shd-surface))}
|
||
.assistant-toggle:disabled{cursor:not-allowed;opacity:.5}
|
||
.assistant-toggle:active,.request-button:active,.result-actions button:active,.apply-confirm button:active{transform:translateY(1px);filter:brightness(.84)}
|
||
.permission-note,.assistant-summary{margin:0;padding:9px 11px;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.45}
|
||
.permission-note b{color:var(--shd-text)}
|
||
.locked{border-left-color:var(--shd-border)}
|
||
.assistant-workspace{display:grid;gap:var(--shd-gap-sm);padding:var(--shd-gap-sm)}
|
||
.source-heading,.result-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--shd-gap-sm);padding:7px 9px;border:1px solid var(--shd-border);background:var(--shd-surface-raised)}
|
||
.source-heading small,.result-heading small{color:var(--shd-text-muted);font-size:var(--shd-type-supporting)}
|
||
.source-field,.answer-field{display:grid;gap:5px;color:var(--shd-text);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}
|
||
.source-field textarea,.answer-field input{width:100%;box-sizing:border-box;border:1px solid var(--shd-border);border-radius:0;background:#0d0f10;color:var(--shd-text);font:inherit}
|
||
.source-field textarea{min-height:118px;padding:10px;resize:vertical;font-size:var(--shd-type-body);font-weight:400;letter-spacing:0;line-height:1.5}
|
||
.answer-field input{min-height:42px;padding:0 10px;font-size:var(--shd-type-supporting);font-weight:400;letter-spacing:0}
|
||
.source-field :is(textarea,input):focus-visible,.answer-field input:focus-visible{border-color:var(--shd-orange);outline:1px solid color-mix(in srgb,var(--shd-orange) 55%,transparent);outline-offset:1px}
|
||
.request-actions,.result-actions,.apply-confirm{display:flex;align-items:center;justify-content:space-between;gap:var(--shd-gap-sm)}
|
||
.handoff-actions>div{display:flex;gap:var(--shd-gap-xs)}
|
||
.request-actions p{max-width:70ch;margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
|
||
.request-actions button,.result-actions button,.apply-confirm button{min-height:40px;padding:0 13px;font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px;cursor:pointer}
|
||
.request-actions button:disabled,.result-actions button:disabled{cursor:wait;opacity:.58}
|
||
.result-region{display:grid;gap:var(--shd-gap-sm);outline:none}
|
||
.result-region:focus-visible{outline:1px solid var(--shd-orange);outline-offset:3px}
|
||
.result-heading.capability{border-left:2px solid var(--shd-danger)}.result-heading.capability span{color:var(--shd-danger)}
|
||
.result-heading.ready{border-left:2px solid var(--shd-success)}.result-heading.ready span{color:var(--shd-success)}
|
||
.assistant-message{display:grid;gap:3px;padding:10px;border:1px solid var(--shd-border);border-left:2px solid var(--shd-info);background:var(--shd-surface-raised)}
|
||
.assistant-message p,.gap-summary,.draft-summary,.activity-message{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.45}
|
||
.assistant-message.error{border-left-color:var(--shd-danger)}.assistant-message.error strong{color:var(--shd-danger)}
|
||
.question-stack,.gap-stack{display:grid;gap:var(--shd-gap-xs)}
|
||
.question-card,.gap-card{display:grid;gap:var(--shd-gap-sm);padding:9px;border:1px solid var(--shd-border);background:var(--shd-surface-raised)}
|
||
.question-card>header{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start}
|
||
.question-card>header>span{display:grid;place-items:center;width:28px;height:28px;border:1px solid var(--shd-info);color:var(--shd-info);font-size:var(--shd-type-telemetry);font-weight:900}
|
||
.question-card>header>div{display:grid;gap:3px}.question-card small{color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
|
||
.gap-summary,.draft-summary{padding:0 2px;color:var(--shd-text)}
|
||
.gap-card>header{display:flex;align-items:center;gap:8px}.gap-card>header span,.gap-card dt,.review-notes strong{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}
|
||
.gap-card dl{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));margin:0;border-top:1px solid var(--shd-border-soft);border-left:1px solid var(--shd-border-soft)}
|
||
.gap-card dl>div{display:grid;gap:4px;padding:8px;border-right:1px solid var(--shd-border-soft);border-bottom:1px solid var(--shd-border-soft)}
|
||
.gap-card dd{margin:0;color:var(--shd-text);font-size:var(--shd-type-supporting);line-height:1.45}
|
||
.review-notes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--shd-gap-xs)}
|
||
.review-notes section{padding:9px;border:1px solid var(--shd-border);border-left:2px solid var(--shd-info);background:var(--shd-surface-raised)}
|
||
.review-notes section.warnings{border-left-color:var(--shd-orange)}
|
||
.review-notes ul{display:grid;gap:5px;margin:6px 0 0;padding-left:18px;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
|
||
.apply-confirm{padding:9px;border:1px solid var(--shd-orange);background:color-mix(in srgb,var(--shd-orange) 8%,var(--shd-surface))}
|
||
.apply-confirm>div:first-child{display:grid;gap:3px}.apply-confirm p{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}.apply-confirm>div:last-child{display:flex;gap:7px}
|
||
.replacement-summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px;margin-top:4px}.replacement-summary span{display:grid;gap:2px;padding:5px 7px;border:1px solid var(--shd-border-soft);color:var(--shd-text);font-size:var(--shd-type-supporting)}.replacement-summary b{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);letter-spacing:.7px}
|
||
.activity-message{padding:7px 9px;border-left:2px solid var(--shd-info);background:var(--shd-surface-raised)}
|
||
.handoff-message{margin:0;padding:7px 9px;border-left:2px solid var(--shd-success);background:var(--shd-surface-raised);color:var(--shd-text);font-size:var(--shd-type-supporting);line-height:1.45}
|
||
.handoff-message.error{border-left-color:var(--shd-danger);color:var(--shd-danger)}
|
||
@media(max-width:760px){
|
||
.authoring-assistant>header{grid-template-columns:1fr auto}.authoring-assistant>header b{justify-self:end}.assistant-toggle{grid-column:1/-1;width:100%;min-height:44px}
|
||
.source-heading,.result-heading,.request-actions,.result-actions,.apply-confirm{align-items:stretch;flex-direction:column}
|
||
.request-actions button,.result-actions button,.apply-confirm button{width:100%;min-height:44px}
|
||
.handoff-actions>div{display:grid;grid-template-columns:1fr;width:100%}
|
||
.apply-confirm>div:last-child{display:grid;grid-template-columns:1fr 1fr}
|
||
.replacement-summary{grid-template-columns:1fr}
|
||
.gap-card dl,.review-notes{grid-template-columns:1fr}
|
||
}
|
||
@media(prefers-reduced-motion:reduce){
|
||
.assistant-toggle,.request-button,.result-actions button,.apply-confirm button{transition:none}
|
||
}
|
||
</style>
|