Phase 16R - Word Companion UX Simplification and Workflow Redesign
This commit is contained in:
parent
5ff2713afb
commit
f77d9ccfc7
@ -7,14 +7,18 @@ namespace PlotLine.Controllers;
|
||||
|
||||
[AllowAnonymous]
|
||||
[Route("word-companion")]
|
||||
public sealed class WordCompanionHostController(ICurrentUserService currentUser) : Controller
|
||||
public sealed class WordCompanionHostController(ICurrentUserService currentUser, IAuthorizationService authorization) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
public IActionResult Index()
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var isAdmin = currentUser.IsAuthenticated
|
||||
&& (await authorization.AuthorizeAsync(User, "AdminOnly")).Succeeded;
|
||||
|
||||
return View(new WordCompanionHostViewModel
|
||||
{
|
||||
IsAuthenticated = currentUser.IsAuthenticated,
|
||||
IsAdmin = isAdmin,
|
||||
DisplayName = currentUser.DisplayName,
|
||||
LoginUrl = Url.Action("Login", "Account", new { returnUrl = "/word-companion" }) ?? "/Account/Login?returnUrl=%2Fword-companion"
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ namespace PlotLine.ViewModels;
|
||||
public sealed class WordCompanionHostViewModel
|
||||
{
|
||||
public bool IsAuthenticated { get; init; }
|
||||
public bool IsAdmin { get; init; }
|
||||
public string? DisplayName { get; init; }
|
||||
public string LoginUrl { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="word-companion-shell" data-authenticated="@Model.IsAuthenticated.ToString().ToLowerInvariant()">
|
||||
<main class="word-companion-shell" data-authenticated="@Model.IsAuthenticated.ToString().ToLowerInvariant()" data-admin="@Model.IsAdmin.ToString().ToLowerInvariant()">
|
||||
<header class="word-companion-header">
|
||||
<div>
|
||||
<h1>PlotDirector</h1>
|
||||
@ -89,15 +89,26 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="word-companion-section word-companion-linked-card" data-linked-manuscript-card hidden aria-label="Linked manuscript">
|
||||
<div>
|
||||
<span>Manuscript</span>
|
||||
<strong><span data-linked-project-title>Project</span> > <span data-linked-book-title>Book</span></strong>
|
||||
</div>
|
||||
<p>Linked to PlotDirector</p>
|
||||
</section>
|
||||
|
||||
<nav class="word-companion-tabs" aria-label="Companion sections">
|
||||
<button type="button" class="is-active" data-tab-button="scene" aria-selected="true">Scene</button>
|
||||
<button type="button" data-tab-button="links" aria-selected="false">Links</button>
|
||||
<button type="button" data-tab-button="structure" aria-selected="false">Structure</button>
|
||||
<button type="button" data-tab-button="review" aria-selected="false">Review</button>
|
||||
<button type="button" data-tab-button="maintenance" aria-selected="false">Maintenance</button>
|
||||
@if (Model.IsAdmin)
|
||||
{
|
||||
<button type="button" data-tab-button="diagnostics" aria-selected="false">Diagnostics</button>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<section class="word-companion-tab-panel is-active" data-tab-panel="scene" aria-label="Scene">
|
||||
<section class="word-companion-section word-companion-connection" aria-label="Project and book selection">
|
||||
<section class="word-companion-section word-companion-connection" data-runtime-book-selectors aria-label="Project and book selection">
|
||||
<div>
|
||||
<label for="word-companion-project">Project</label>
|
||||
<select id="word-companion-project" data-project-select disabled>
|
||||
@ -144,13 +155,9 @@
|
||||
<strong data-current-word-count>-</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Actual</span>
|
||||
<span>PlotDirector</span>
|
||||
<strong data-plotdirector-actual-words>-</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Estimated</span>
|
||||
<strong data-plotdirector-estimated-words>-</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Revision</span>
|
||||
<strong data-plotdirector-revision-status>-</strong>
|
||||
@ -159,6 +166,20 @@
|
||||
<span>Last Sync</span>
|
||||
<strong data-runtime-last-sync>-</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="word-companion-section word-companion-brief" data-writing-brief-block hidden aria-label="Writing Brief">
|
||||
<span>Writing Brief</span>
|
||||
<p data-writing-brief></p>
|
||||
</section>
|
||||
|
||||
<details class="word-companion-collapsible word-companion-advanced">
|
||||
<summary>Advanced</summary>
|
||||
<div class="word-companion-metrics">
|
||||
<div>
|
||||
<span>Estimated</span>
|
||||
<strong data-plotdirector-estimated-words>-</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Blocked</span>
|
||||
<strong data-plotdirector-blocked-status>-</strong>
|
||||
@ -167,10 +188,8 @@
|
||||
<span>Blocked Reason</span>
|
||||
<strong data-plotdirector-blocked-reason>-</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" data-attach-plotdirector-ids disabled>Attach PlotDirector IDs</button>
|
||||
<button type="button" class="word-companion-danger-action" data-unlink-word-document hidden>Unlink this Word document</button>
|
||||
</div>
|
||||
</details>
|
||||
<p class="word-companion-message" data-sync-note></p>
|
||||
</section>
|
||||
|
||||
@ -212,46 +231,56 @@
|
||||
<p class="word-companion-message" data-create-link-status></p>
|
||||
</section>
|
||||
|
||||
<section class="word-companion-section word-companion-brief" data-writing-brief-block hidden aria-label="Writing Brief">
|
||||
<span>Writing Brief</span>
|
||||
<p data-writing-brief></p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="word-companion-tab-panel" data-tab-panel="links" aria-label="Links" hidden>
|
||||
<section class="word-companion-tab-panel" data-tab-panel="review" aria-label="Review" hidden>
|
||||
<section class="word-companion-section word-companion-link-groups" data-plotdirector-link-groups aria-label="Scene Links">
|
||||
<div class="word-companion-section-header">
|
||||
<span>Scene Links</span>
|
||||
<span>Review Discoveries</span>
|
||||
<button type="button" data-save-scene-links disabled>Save Scene Links</button>
|
||||
</div>
|
||||
<p class="word-companion-message" data-scene-links-status>Link a PlotDirector scene first.</p>
|
||||
<p class="word-companion-link-context" data-links-editing-scene>Link a PlotDirector scene first.</p>
|
||||
<div>
|
||||
<span>Characters</span>
|
||||
<p class="word-companion-message">Characters, assets and locations detected while writing will appear here.</p>
|
||||
<details class="word-companion-review-group" open>
|
||||
<summary data-character-review-summary>Characters (0)</summary>
|
||||
<div class="word-companion-check-list" data-character-chips></div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Assets</span>
|
||||
</details>
|
||||
<details class="word-companion-review-group">
|
||||
<summary data-asset-review-summary>Assets (0)</summary>
|
||||
<div class="word-companion-check-list" data-asset-chips></div>
|
||||
</div>
|
||||
<div>
|
||||
</details>
|
||||
<details class="word-companion-review-group">
|
||||
<summary data-location-review-summary>Locations (0)</summary>
|
||||
<label for="word-companion-primary-location">Primary Location</label>
|
||||
<select id="word-companion-primary-location" data-primary-location-select disabled>
|
||||
<option value="">None</option>
|
||||
</select>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="word-companion-tab-panel" data-tab-panel="structure" aria-label="Structure" hidden>
|
||||
<section class="word-companion-tab-panel" data-tab-panel="maintenance" aria-label="Maintenance" hidden>
|
||||
<section class="word-companion-section word-companion-maintenance" aria-label="Manuscript maintenance">
|
||||
<div class="word-companion-section-header">
|
||||
<span>Manuscript Maintenance</span>
|
||||
</div>
|
||||
<p class="word-companion-message">Use these tools when manuscript headings, scene boundaries, or document links need attention.</p>
|
||||
<div class="word-companion-actions word-companion-maintenance-actions">
|
||||
<button type="button" data-attach-plotdirector-ids disabled>Attach PlotDirector IDs</button>
|
||||
<button type="button" data-refresh-document>Refresh Local Cache</button>
|
||||
<button type="button" class="word-companion-danger-action" data-unlink-word-document hidden>Unlink this Word document</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="word-companion-section word-companion-structure-preview" aria-label="Structure Sync Preview">
|
||||
<div class="word-companion-section-header">
|
||||
<span>Structure Sync Preview</span>
|
||||
<span>Structure Maintenance</span>
|
||||
<div class="word-companion-header-actions">
|
||||
<button type="button" data-analyse-manuscript-structure disabled>Analyse Manuscript Structure</button>
|
||||
<button type="button" data-apply-structure-sync disabled>Apply Structure Sync</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="word-companion-message">Use these tools if manuscript headings or scene boundaries have changed significantly.</p>
|
||||
<p class="word-companion-message" data-structure-preview-status>Select a project and book to analyse manuscript structure.</p>
|
||||
<div class="word-companion-preview-groups" data-structure-preview-results>
|
||||
<p>No preview generated.</p>
|
||||
@ -266,6 +295,8 @@
|
||||
</details>
|
||||
</section>
|
||||
|
||||
@if (Model.IsAdmin)
|
||||
{
|
||||
<section class="word-companion-tab-panel" data-tab-panel="diagnostics" aria-label="Diagnostics" hidden>
|
||||
<section class="word-companion-section">
|
||||
<div class="word-companion-section-header">
|
||||
@ -392,6 +423,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
}
|
||||
|
||||
<footer class="word-companion-footer">
|
||||
<span data-office-status>Word Companion loading</span>
|
||||
@ -401,7 +433,6 @@
|
||||
<span data-summary-connection></span>
|
||||
<span data-summary-project></span>
|
||||
<span data-summary-book></span>
|
||||
<button type="button" data-refresh-document></button>
|
||||
<button type="button" data-refresh-plotdirector-scene></button>
|
||||
<span data-sync-word-count></span>
|
||||
<section data-plotdirector-scene-details>
|
||||
|
||||
@ -259,7 +259,7 @@ body {
|
||||
|
||||
.word-companion-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
||||
gap: 3px;
|
||||
border: 1px solid var(--companion-line);
|
||||
border-radius: 9px;
|
||||
@ -338,6 +338,29 @@ body {
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.word-companion-linked-card {
|
||||
gap: 4px;
|
||||
padding: 8px 9px;
|
||||
}
|
||||
|
||||
.word-companion-linked-card div {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.word-companion-linked-card strong {
|
||||
color: var(--companion-accent-dark);
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.word-companion-linked-card p {
|
||||
margin: 0;
|
||||
color: #2f6b45;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.word-companion-first-run {
|
||||
gap: 10px;
|
||||
}
|
||||
@ -568,6 +591,16 @@ body {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.word-companion-maintenance .word-companion-danger-action {
|
||||
border-color: #fecaca;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.word-companion-maintenance .word-companion-danger-action:hover:not(:disabled) {
|
||||
border-color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.word-companion-message:empty {
|
||||
display: none;
|
||||
}
|
||||
@ -696,6 +729,30 @@ body {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.word-companion-review-group {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
border: 1px solid rgba(111, 75, 45, 0.16);
|
||||
border-radius: 7px;
|
||||
padding: 8px;
|
||||
background: #fffdf8;
|
||||
}
|
||||
|
||||
.word-companion-review-group summary {
|
||||
cursor: pointer;
|
||||
color: var(--companion-accent-dark);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.word-companion-review-group:not([open]) > :not(summary) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.word-companion-review-group label {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.word-companion-resolve-diagnostics {
|
||||
padding-top: 4px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@ -759,18 +816,18 @@ body {
|
||||
|
||||
.word-companion-check-list {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.word-companion-check-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 29px;
|
||||
gap: 0.75rem;
|
||||
min-height: 42px;
|
||||
border: 1px solid rgba(111, 75, 45, 0.18);
|
||||
border-radius: 6px;
|
||||
padding: 5px 7px;
|
||||
padding: 0.75rem;
|
||||
background: #fffdf8;
|
||||
color: var(--companion-ink);
|
||||
font-size: 0.9rem;
|
||||
@ -800,6 +857,19 @@ body {
|
||||
background: #fff8ec;
|
||||
}
|
||||
|
||||
.word-companion-advanced {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--companion-line);
|
||||
border-radius: 0;
|
||||
padding: 7px 0 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.word-companion-maintenance-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.word-companion-structure-preview {
|
||||
border-color: rgba(182, 138, 70, 0.35);
|
||||
}
|
||||
|
||||
2
PlotLine/wwwroot/css/word-companion.min.css
vendored
2
PlotLine/wwwroot/css/word-companion.min.css
vendored
File diff suppressed because one or more lines are too long
@ -10,12 +10,15 @@
|
||||
projectId: "plotdirector.word.boundProjectId",
|
||||
bindingVersion: "plotdirector.word.bindingVersion"
|
||||
};
|
||||
const validTabNames = new Set(["scene", "links", "structure", "diagnostics"]);
|
||||
const validTabNames = new Set(["scene", "review", "maintenance", "diagnostics"]);
|
||||
|
||||
const shell = document.querySelector(".word-companion-shell");
|
||||
const tabsNav = document.querySelector(".word-companion-tabs");
|
||||
const tabButtons = [...document.querySelectorAll("[data-tab-button]")];
|
||||
const tabPanels = [...document.querySelectorAll("[data-tab-panel]")];
|
||||
const linkedManuscriptCard = document.querySelector("[data-linked-manuscript-card]");
|
||||
const linkedProjectTitle = document.querySelector("[data-linked-project-title]");
|
||||
const linkedBookTitle = document.querySelector("[data-linked-book-title]");
|
||||
const firstRunWizard = document.querySelector("[data-first-run-wizard]");
|
||||
const firstRunProjectSelect = document.querySelector("[data-first-run-project-select]");
|
||||
const firstRunBookSelect = document.querySelector("[data-first-run-book-select]");
|
||||
@ -48,6 +51,7 @@
|
||||
const summaryBook = document.querySelector("[data-summary-book]");
|
||||
const projectSelect = document.querySelector("[data-project-select]");
|
||||
const bookSelect = document.querySelector("[data-book-select]");
|
||||
const runtimeBookSelectors = document.querySelector("[data-runtime-book-selectors]");
|
||||
const projectMessage = document.querySelector("[data-project-message]");
|
||||
const bookMessage = document.querySelector("[data-book-message]");
|
||||
const refreshCurrentSceneButton = document.querySelector("[data-refresh-current-scene]");
|
||||
@ -88,6 +92,9 @@
|
||||
const writingBriefBlock = document.querySelector("[data-writing-brief-block]");
|
||||
const writingBrief = document.querySelector("[data-writing-brief]");
|
||||
const plotDirectorLinkGroups = document.querySelector("[data-plotdirector-link-groups]");
|
||||
const characterReviewSummary = document.querySelector("[data-character-review-summary]");
|
||||
const assetReviewSummary = document.querySelector("[data-asset-review-summary]");
|
||||
const locationReviewSummary = document.querySelector("[data-location-review-summary]");
|
||||
const characterChips = document.querySelector("[data-character-chips]");
|
||||
const assetChips = document.querySelector("[data-asset-chips]");
|
||||
const locationChips = document.querySelector("[data-location-chips]");
|
||||
@ -280,9 +287,20 @@
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeTabName = (tabName) => {
|
||||
if (tabName === "links") {
|
||||
return "review";
|
||||
}
|
||||
if (tabName === "structure") {
|
||||
return "maintenance";
|
||||
}
|
||||
return tabName;
|
||||
};
|
||||
|
||||
const selectTab = (tabName, persist = true) => {
|
||||
const targetTab = validTabNames.has(tabName) && tabButtons.some((button) => button.dataset.tabButton === tabName)
|
||||
? tabName
|
||||
const requestedTab = normalizeTabName(tabName);
|
||||
const targetTab = validTabNames.has(requestedTab) && tabButtons.some((button) => button.dataset.tabButton === requestedTab)
|
||||
? requestedTab
|
||||
: "scene";
|
||||
|
||||
for (const button of tabButtons) {
|
||||
@ -303,7 +321,7 @@
|
||||
};
|
||||
|
||||
const restoreActiveTab = () => {
|
||||
const storedTab = readStoredText(storageKeys.activeTab);
|
||||
const storedTab = normalizeTabName(readStoredText(storageKeys.activeTab));
|
||||
selectTab(validTabNames.has(storedTab) ? storedTab : "scene", false);
|
||||
};
|
||||
|
||||
@ -365,6 +383,7 @@
|
||||
setText(anchorSceneId, Number.isInteger(resolvedSceneId) && resolvedSceneId > 0 ? String(resolvedSceneId) : "-");
|
||||
|
||||
if (attachPlotDirectorIdsButton) {
|
||||
setHidden(attachPlotDirectorIdsButton, attached && !storedAnchorInvalid);
|
||||
attachPlotDirectorIdsButton.disabled = !resolvedSceneId || (attached && !storedAnchorInvalid);
|
||||
attachPlotDirectorIdsButton.textContent = detectedSceneAnchorId && !attached
|
||||
? "Reattach PlotDirector IDs"
|
||||
@ -593,6 +612,7 @@
|
||||
const lastSyncUtc = runtimeBookContext?.lastSyncUtc || runtimeBookContext?.manuscriptDocument?.lastSyncUtc;
|
||||
setText(runtimeLastSync, formatLastSynced(lastSyncUtc));
|
||||
setText(syncLastSynced, formatLastSynced(lastSyncUtc));
|
||||
updateLinkedManuscriptUi();
|
||||
};
|
||||
|
||||
const clearRuntimeCaches = () => {
|
||||
@ -708,6 +728,7 @@
|
||||
renderSceneLinkList(characterChips, [], "character");
|
||||
renderSceneLinkList(assetChips, [], "asset");
|
||||
renderPrimaryLocationOptions([], null, false);
|
||||
updateReviewSummaries();
|
||||
setResolvedScene(null);
|
||||
setText(syncPlotDirectorCount, "-");
|
||||
setText(plotDirectorSceneTitle, "-");
|
||||
@ -830,6 +851,7 @@
|
||||
renderSceneLinkList(characterChips, currentSceneLinks.characters, "character", !!resolvedSceneId && !sceneContextStale);
|
||||
renderSceneLinkList(assetChips, currentSceneLinks.assets, "asset", !!resolvedSceneId && !sceneContextStale);
|
||||
renderPrimaryLocationOptions(currentSceneLinks.locations, currentSceneLinks.primaryLocationId, !!resolvedSceneId && !sceneContextStale);
|
||||
updateReviewSummaries();
|
||||
updateLinksEditingScene();
|
||||
setSceneLinksStatus(sceneContextStale
|
||||
? "Refresh Current Scene before saving."
|
||||
@ -1115,6 +1137,26 @@
|
||||
return subtitle ? `${title}: ${subtitle}` : title;
|
||||
};
|
||||
|
||||
const runtimeProjectTitle = () =>
|
||||
runtimeBookContext?.projectTitle
|
||||
|| selectedProject()?.title
|
||||
|| "Project";
|
||||
|
||||
const runtimeBookTitle = () =>
|
||||
runtimeBookContext?.bookTitle
|
||||
|| bookDisplayTitle(selectedBook())
|
||||
|| "Book";
|
||||
|
||||
const updateLinkedManuscriptUi = () => {
|
||||
const linked = !!firstRunDocumentBinding;
|
||||
setHidden(linkedManuscriptCard, !linked);
|
||||
setHidden(runtimeBookSelectors, linked);
|
||||
if (linked) {
|
||||
setText(linkedProjectTitle, runtimeProjectTitle());
|
||||
setText(linkedBookTitle, runtimeBookTitle());
|
||||
}
|
||||
};
|
||||
|
||||
const selectedProject = () => {
|
||||
const projectId = Number.parseInt(projectSelect?.value || "", 10);
|
||||
return projects.find((project) => project.projectId === projectId) || null;
|
||||
@ -1161,6 +1203,7 @@
|
||||
setHidden(firstRunWizard, !visible);
|
||||
setHidden(tabsNav, visible);
|
||||
updateUnlinkWordDocumentButton();
|
||||
updateLinkedManuscriptUi();
|
||||
for (const panel of tabPanels) {
|
||||
setHidden(panel, visible ? true : panel.dataset.tabPanel !== "scene" && !panel.classList.contains("is-active"));
|
||||
}
|
||||
@ -1438,6 +1481,16 @@
|
||||
syncNote.textContent = book ? "" : "Select a PlotDirector book before refreshing.";
|
||||
}
|
||||
setAnalyseButtonEnabled();
|
||||
updateLinkedManuscriptUi();
|
||||
};
|
||||
|
||||
const updateReviewSummaries = () => {
|
||||
const characterCount = Array.isArray(currentSceneLinks.characters) ? currentSceneLinks.characters.length : 0;
|
||||
const assetCount = Array.isArray(currentSceneLinks.assets) ? currentSceneLinks.assets.length : 0;
|
||||
const locationCount = Array.isArray(currentSceneLinks.locations) ? currentSceneLinks.locations.length : 0;
|
||||
setText(characterReviewSummary, `Characters (${characterCount})`);
|
||||
setText(assetReviewSummary, `Assets (${assetCount})`);
|
||||
setText(locationReviewSummary, `Locations (${locationCount})`);
|
||||
};
|
||||
|
||||
const normalizedStyleName = (paragraph) => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user