This commit is contained in:
Nick Beckley 2026-07-03 20:40:48 +01:00
commit 0f7b6a8116
14 changed files with 2069 additions and 14 deletions

View File

@ -0,0 +1,332 @@
# Phase 20 -- Story Intelligence Onboarding
## Functional Design Specification
### Overview
The purpose of the Story Intelligence Onboarding Wizard is to transform
a new user's manuscript into a structured PlotDirector project with as
little manual effort as possible.
Rather than expecting the user to manually create chapters, scenes,
characters and summaries, PlotDirector will guide them through an
onboarding process appropriate to the way they write.
The wizard has two distinct objectives:
1. Remove the "blank project" experience that new users currently
encounter.
2. Demonstrate PlotDirector's capabilities immediately by building a
rich story model from the user's existing manuscript.
The wizard is website-led throughout. The PlotDirector Word Companion
acts only as a document scanner and synchronisation agent where
Microsoft Word is used.
------------------------------------------------------------------------
# Design Principles
- The onboarding should feel like a conversation rather than a
configuration screen.
- The user should never be presented with options that are irrelevant
to the software they use.
- AI is entirely optional.
- The user always remains in control.
- No manuscript is analysed using AI without explicit consent.
- The Word Companion remains the preferred workflow but is never
mandatory.
------------------------------------------------------------------------
# Wizard Flow
## Step 1 -- Welcome
Display a friendly welcome explaining that PlotDirector will tailor the
setup to the author's workflow.
------------------------------------------------------------------------
## Step 2 -- Where are you in your writing journey?
- I'm planning a brand new story
- I've already started writing
- My manuscript is largely complete
------------------------------------------------------------------------
## Step 3 -- What software do you write in?
- Microsoft Word (Recommended)
- Google Docs
- Scrivener
- LibreOffice / OpenOffice
- Markdown / Obsidian
- Other
Store this preference against the user profile.
------------------------------------------------------------------------
## Step 4 -- Create your first project
Collect:
- Project Name
- Optional Description
------------------------------------------------------------------------
## Step 5 -- Create your first book
Collect:
- Book Title
- Subtitle
- Genre
- Estimated Word Count (optional)
- Cover Image (optional)
------------------------------------------------------------------------
# Path A -- Microsoft Word
## A1 -- Install the Word Companion
Provide:
- Download button
- Installation guide
- Short demonstration video
- Troubleshooting link
------------------------------------------------------------------------
## A2 -- Open the manuscript
The user opens the manuscript in Word, starts the Companion, signs in
and links the document to the selected Project and Book.
------------------------------------------------------------------------
## A3 -- Companion Detection
The Companion silently registers itself with PlotDirector, including:
- User
- Companion Version
- Current Document
- Linked Project
- Linked Book
- SignalR Session
The website updates immediately to show **Word Companion Connected**.
Until this point the Companion status is hidden from the application
header.
------------------------------------------------------------------------
## A4 -- Scan Manuscript
The website instructs the Companion (via SignalR) to scan the document.
The Companion detects:
- Chapters
- Scenes
- Word Counts
- Character Candidates
- Hidden Document IDs
- Document Metadata
Progress is streamed continuously back to PlotDirector.
------------------------------------------------------------------------
## A5 -- Review Scan
Display the detected:
- Chapters
- Scenes
- Character Suggestions
Allow editing before import.
------------------------------------------------------------------------
## A6 -- Import Structure
Create:
- Chapters
- Scenes
- Characters
- Initial Scene Metadata
Return created IDs to the Companion so hidden document markers can be
updated.
------------------------------------------------------------------------
## A7 -- Story Intelligence (Optional)
Explain that PlotDirector can analyse the manuscript using AI.
Explain:
- Entirely optional.
- Manuscript is securely processed using the OpenAI API.
- API content is not used to train AI models.
- Show estimated time.
- Show cost or included credits.
Buttons:
- Analyse Now
- Skip
------------------------------------------------------------------------
# Path B -- Other Writing Software
## B1 -- Export
Guide the user to export their manuscript as **.docx**.
## B2 -- Upload
Upload the document.
Use the same scanning engine as the Word Companion.
## B3 -- Review
Display detected structure.
## B4 -- Import
Create the project structure.
Explain that future updates can be imported by uploading a fresh Word
export.
## B5 -- Story Intelligence
Offer the same optional AI analysis.
------------------------------------------------------------------------
# Story Intelligence Pipeline
The pipeline should progressively build a structured Story Model.
1. Scene analysis
2. Character profiles
3. Relationship summaries
4. Location summaries
5. Asset summaries
6. Plot thread suggestions
7. Timeline observations
8. Continuity and warning generation
Each stage should consume structured data from earlier stages wherever
possible rather than repeatedly analysing the manuscript.
------------------------------------------------------------------------
# Progress Experience
Progress should be streamed live using SignalR.
Example messages:
- Preparing manuscript
- Reading Chapter 12 of 38
- Summarising Scene 54
- Identifying recurring characters
- Building relationship map
- Finding important locations
- Tracking story assets
- Constructing timeline
- Generating character biographies
- Building your Story Model
- Finalising project
Display live counters for:
- Chapters
- Scenes
- Characters
- Locations
- Assets
- Relationships
- Plot Threads
------------------------------------------------------------------------
# Completion Screen
Present a dedicated completion page.
Example summary:
- 42 Chapters
- 186 Scenes
- 38 Characters
- 24 Locations
- 18 Assets
- 31 Relationships
- 11 Plot Threads
- 315 AI Suggestions
Buttons:
- Review AI Suggestions
- Open Project Overview
- Open Writer Workspace
- Take a Quick Tour
------------------------------------------------------------------------
# Dashboard Behaviour
If Microsoft Word was selected but the Companion has never connected:
Display a dashboard reminder.
Once connected:
- Hide installation prompts.
- Show Companion status in the application header.
- Future connections occur silently.
------------------------------------------------------------------------
# Future Enhancements
- Automatic manuscript update detection.
- Incremental re-analysis.
- Selective AI regeneration.
- AI confidence scores.
- Author approval queue.
- Background processing.
- Analysis history.
- Version comparison.
- Additional writing platform support.
------------------------------------------------------------------------
# Guiding Philosophy
The onboarding wizard is not simply importing a manuscript.
It is constructing a structured **Story Model** that powers
PlotDirector's timelines, continuity analysis, relationships, locations,
assets, plot threads, reports and writing tools.
The end result should leave authors feeling that PlotDirector has
genuinely understood their story rather than simply copied their
document into a database.

View File

@ -16,6 +16,7 @@ public sealed class WordCompanionBookDto
public int BookId { get; set; }
public string Title { get; set; } = string.Empty;
public string? Subtitle { get; set; }
public string DisplayTitle => BookTitleFormatter.DisplayTitle(Title, Subtitle);
public int SortOrder { get; set; }
public bool UsesExplicitScenes { get; set; }
}

View File

@ -0,0 +1,244 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE dbo.StoryAsset_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
CREATE TABLE #ProjectLocationPaths
(
LocationID int NOT NULL PRIMARY KEY,
LocationName nvarchar(200) NOT NULL,
LocationPath nvarchar(max) NOT NULL
);
;WITH ProjectLocationTree AS
(
SELECT LocationID, LocationName, CAST(LocationName AS nvarchar(max)) AS LocationPath
FROM dbo.Locations
WHERE ProjectID = @ProjectID AND ParentLocationID IS NULL AND IsArchived = 0
UNION ALL
SELECT child.LocationID, child.LocationName, CAST(parent.LocationPath + N' > ' + child.LocationName AS nvarchar(max)) AS LocationPath
FROM dbo.Locations child
INNER JOIN ProjectLocationTree parent ON parent.LocationID = child.ParentLocationID
WHERE child.ProjectID = @ProjectID AND child.IsArchived = 0
)
INSERT #ProjectLocationPaths (LocationID, LocationName, LocationPath)
SELECT LocationID, LocationName, LocationPath
FROM ProjectLocationTree;
SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance,
sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID,
location.LocationName AS CurrentLocationName, location.LocationPath AS CurrentLocationPath,
sa.IsResolved, sa.ShowInQuickAddBar, sa.ImagePath, sa.ThumbnailPath, sa.CreatedDate, sa.UpdatedDate, sa.IsArchived
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
LEFT JOIN #ProjectLocationPaths location ON location.LocationID = sa.CurrentLocationID
WHERE sa.ProjectID = @ProjectID
AND sa.IsArchived = 0
ORDER BY sa.AssetName, sa.StoryAssetID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetTimeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance,
sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID, sa.IsResolved,
sa.ShowInQuickAddBar, sa.CreatedDate, sa.UpdatedDate, sa.IsArchived
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0 AND (sa.Importance >= 5 OR sa.IsResolved = 0)
ORDER BY sa.AssetName, sa.StoryAssetID;
SELECT ae.AssetEventID, ae.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName,
ae.SceneID, ae.AssetEventTypeID, aet.TypeName AS AssetEventTypeName, aet.MarkerText,
ae.FromStateID, fs.StateName AS FromStateName, ae.ToStateID, ts.StateName AS ToStateName,
ae.EventTitle, ae.EventDescription, b.BookTitle, b.Subtitle AS BookSubtitle,
c.ChapterNumber, s.SceneNumber, s.SceneTitle, ae.CreatedDate, ae.UpdatedDate
FROM dbo.AssetEvents ae
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = ae.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
LEFT JOIN dbo.AssetStates fs ON fs.AssetStateID = ae.FromStateID
LEFT JOIN dbo.AssetStates ts ON ts.AssetStateID = ae.ToStateID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, b.BookNumber, c.SortOrder, c.ChapterNumber, s.SortOrder, s.SceneNumber, sa.AssetName, ae.AssetEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Location_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT lp.LocationID, lp.ProjectID, lp.ParentLocationID, lp.LocationName, lp.LocationTypeID, lt.TypeName AS LocationTypeName,
lp.Description, lp.ShowInQuickAddBar, lp.ExcludeFromCompanionDetection, lp.DetectionPriority,
lp.LocationPath, lp.Depth, l.CreatedDate, l.UpdatedDate, l.IsArchived
FROM dbo.LocationPaths lp
INNER JOIN dbo.Locations l ON l.LocationID = lp.LocationID
LEFT JOIN dbo.LocationTypes lt ON lt.LocationTypeID = lp.LocationTypeID
WHERE lp.ProjectID = @ProjectID
ORDER BY lp.LocationPath, lp.LocationID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotThread_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT pt.PlotThreadID, pt.PlotLineID, pl.ProjectID, pl.PlotLineName, pt.ThreadTitle,
pt.ThreadTypeID, tt.TypeName AS ThreadTypeName, pt.ThreadStatusID, ts.StatusName AS ThreadStatusName,
pt.Importance, pt.Summary, pt.IntroducedSceneID, pt.PlannedResolutionSceneID, pt.ActualResolutionSceneID,
pt.CreatedDate, pt.UpdatedDate, pt.IsArchived
FROM dbo.PlotThreads pt
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadTypes tt ON tt.ThreadTypeID = pt.ThreadTypeID
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
WHERE pl.ProjectID = @ProjectID AND pl.IsArchived = 0 AND pt.IsArchived = 0
ORDER BY pt.ThreadTitle, pl.PlotLineName, pt.PlotThreadID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotThread_ListByPlotLine
@PlotLineID int
AS
BEGIN
SET NOCOUNT ON;
SELECT pt.PlotThreadID, pt.PlotLineID, pl.ProjectID, pl.PlotLineName, pt.ThreadTitle,
pt.ThreadTypeID, tt.TypeName AS ThreadTypeName, pt.ThreadStatusID, ts.StatusName AS ThreadStatusName,
pt.Importance, pt.Summary, pt.IntroducedSceneID, pt.PlannedResolutionSceneID, pt.ActualResolutionSceneID,
pt.CreatedDate, pt.UpdatedDate, pt.IsArchived
FROM dbo.PlotThreads pt
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadTypes tt ON tt.ThreadTypeID = pt.ThreadTypeID
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
WHERE pt.PlotLineID = @PlotLineID AND pt.IsArchived = 0
ORDER BY pt.ThreadTitle, pt.PlotThreadID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Book_ListByProject
@ProjectID int,
@UserID int
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS
(
SELECT 1
FROM dbo.ProjectUserAccess pua
INNER JOIN dbo.Projects p ON p.ProjectID = pua.ProjectID
WHERE pua.ProjectID = @ProjectID
AND pua.UserID = @UserID
AND pua.IsActive = 1
AND p.IsArchived = 0
)
RETURN;
SELECT b.BookID AS BookId,
b.BookTitle AS Title,
b.Subtitle,
b.SortOrder,
b.UsesExplicitScenes
FROM dbo.Books b
WHERE b.ProjectID = @ProjectID
AND b.IsArchived = 0
ORDER BY b.SortOrder, b.BookNumber, b.BookTitle, b.Subtitle, b.BookID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Book_Structure
@BookID int,
@UserID int
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS
(
SELECT 1
FROM dbo.Books b
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
WHERE b.BookID = @BookID
AND b.IsArchived = 0
AND p.IsArchived = 0
AND pua.UserID = @UserID
AND pua.IsActive = 1
)
RETURN;
SELECT @BookID AS BookId;
SELECT c.ChapterID AS ChapterId, c.ChapterTitle AS Title, c.SortOrder
FROM dbo.Chapters c
WHERE c.BookID = @BookID
AND c.IsArchived = 0
ORDER BY c.SortOrder, c.ChapterNumber, c.ChapterTitle, c.ChapterID;
SELECT s.ChapterID AS ChapterId, s.SceneID AS SceneId, s.SceneTitle AS Title, s.SortOrder,
rs.StatusName AS RevisionStatus,
sw.EstimatedWordCount AS EstimatedWords,
sw.ActualWordCount AS ActualWords
FROM dbo.Scenes s
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
WHERE c.BookID = @BookID
AND c.IsArchived = 0
AND s.IsArchived = 0
ORDER BY c.SortOrder, c.ChapterNumber, s.SortOrder, s.SceneNumber, s.SceneTitle, s.SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Chapter_List
@BookID int,
@UserID int
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS
(
SELECT 1
FROM dbo.Books b
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
WHERE b.BookID = @BookID
AND b.IsArchived = 0
AND p.IsArchived = 0
AND pua.UserID = @UserID
AND pua.IsActive = 1
)
RETURN;
SELECT c.ChapterID AS ChapterId,
c.ChapterTitle AS Title,
c.SortOrder
FROM dbo.Chapters c
WHERE c.BookID = @BookID
AND c.IsArchived = 0
ORDER BY c.SortOrder, c.ChapterNumber, c.ChapterTitle, c.ChapterID;
END;
GO

View File

@ -0,0 +1,21 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE dbo.Location_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT lp.LocationID, lp.ProjectID, lp.ParentLocationID, lp.LocationName, lp.LocationTypeID, lt.TypeName AS LocationTypeName,
lp.Description, lp.ShowInQuickAddBar, lp.ExcludeFromCompanionDetection, lp.DetectionPriority,
lp.LocationPath, lp.Depth, l.CreatedDate, l.UpdatedDate, l.IsArchived
FROM dbo.LocationPaths lp
INNER JOIN dbo.Locations l ON l.LocationID = lp.LocationID
LEFT JOIN dbo.LocationTypes lt ON lt.LocationTypeID = lp.LocationTypeID
WHERE lp.ProjectID = @ProjectID
ORDER BY lp.LocationName, lp.LocationPath, lp.LocationID;
END;
GO

View File

@ -3,7 +3,7 @@
ViewData["ShellClass"] = "marketing-shell";
}
<article class="marketing-home">
<article class="marketing-home marketing-home-page">
<section class="marketing-hero reveal-section">
<div class="marketing-container marketing-hero__grid">
<div class="marketing-hero__copy">

View File

@ -71,10 +71,10 @@
}
<section class="mb-3" aria-label="Series summary">
<div class="row g-2">
<div class="dashboard-summary-grid">
@foreach (var card in summaryCards)
{
<div class="col-6 col-md-4 col-xl-2">
<div>
<div class="card h-100 dashboard-summary-card @card.Item3">
<div class="card-body dashboard-summary-body">
<p class="eyebrow dashboard-summary-label">@card.Item1</p>

View File

@ -4,7 +4,7 @@
ViewData["ShellClass"] = "marketing-shell";
}
<article class="marketing-home public-info-page">
<article class="marketing-home public-info-page public-about-page">
<section class="public-hero">
<div class="marketing-container public-hero__inner">
<p class="marketing-eyebrow">About PlotDirector</p>

View File

@ -4,7 +4,7 @@
ViewData["ShellClass"] = "marketing-shell";
}
<article class="marketing-home public-info-page">
<article class="marketing-home public-info-page public-contact-page">
<section class="public-hero">
<div class="marketing-container public-hero__inner">
<p class="marketing-eyebrow">Contact</p>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -470,20 +470,27 @@ td.text-end {
border-color: var(--plotline-line);
}
.dashboard-summary-grid {
display: grid;
grid-template-columns: repeat(9, minmax(0, 1fr));
gap: 8px;
}
.dashboard-summary-body {
padding: 10px 12px;
padding: 8px 9px;
}
.dashboard-summary-label {
margin-bottom: 4px;
font-size: 0.7rem;
margin-bottom: 3px;
font-size: 0.64rem;
line-height: 1.15;
}
.dashboard-summary-value {
display: block;
font-size: 1.45rem;
font-size: 1.25rem;
line-height: 1;
overflow-wrap: anywhere;
}
.dashboard-summary-calm {
@ -501,6 +508,24 @@ td.text-end {
background: rgba(252, 241, 246, 0.7);
}
@media (max-width: 1199.98px) {
.dashboard-summary-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 767.98px) {
.dashboard-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 420px) {
.dashboard-summary-grid {
grid-template-columns: 1fr;
}
}
.dashboard-progress-card {
border-color: rgba(47, 111, 99, 0.18);
background: linear-gradient(180deg, #ffffff 0%, rgba(238, 245, 242, 0.64) 100%);

File diff suppressed because one or more lines are too long

View File

@ -1132,6 +1132,11 @@
};
const bookDisplayTitle = (book) => {
const displayTitle = String(book?.displayTitle || "").trim();
if (displayTitle) {
return displayTitle;
}
const title = String(book?.title || "").trim();
const subtitle = String(book?.subtitle || "").trim();
return subtitle ? `${title}: ${subtitle}` : title;

File diff suppressed because one or more lines are too long