681 lines
28 KiB
C#
681 lines
28 KiB
C#
using System.Data;
|
|
using Dapper;
|
|
using PlotLine.Models;
|
|
|
|
namespace PlotLine.Data;
|
|
|
|
public interface IStoryIntelligenceResultRepository
|
|
{
|
|
Task<int> SaveAsync(StoryIntelligenceRunSaveRequest request);
|
|
Task<int> QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request);
|
|
Task<StoryIntelligenceQueuedRun?> ClaimNextPendingAsync();
|
|
Task UpdateProgressAsync(
|
|
int runId,
|
|
string? status = null,
|
|
string? currentStage = null,
|
|
string? currentMessage = null,
|
|
int? totalDetectedScenes = null,
|
|
int? completedScenes = null,
|
|
int? failedScenes = null,
|
|
int? totalInputTokens = null,
|
|
int? totalOutputTokens = null,
|
|
int? totalTokens = null,
|
|
long? totalDurationMs = null,
|
|
decimal? estimatedCostGBP = null,
|
|
decimal? estimatedCostUSD = null);
|
|
Task CompleteRunAsync(int runId, string status, int? totalInputTokens, int? totalOutputTokens, int? totalTokens, long? totalDurationMs, decimal? estimatedCostGBP, decimal? estimatedCostUSD);
|
|
Task FailRunAsync(int runId, string failureStage, string errorMessage, string? errorDetail, long? totalDurationMs);
|
|
Task RequestCancelAsync(int runId);
|
|
Task CancelRunAsync(int runId, long? totalDurationMs);
|
|
Task<int> SaveChapterResultAsync(int runId, StoryIntelligenceChapterResultSaveRequest chapter);
|
|
Task<int> SaveSceneResultAsync(int runId, int? chapterResultId, StoryIntelligenceSceneResultSaveRequest scene);
|
|
Task<IReadOnlyList<StoryIntelligenceSavedRunListItem>> ListRunsAsync();
|
|
Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId);
|
|
Task<StoryIntelligenceSavedChapterResult?> GetChapterResultAsync(int runId);
|
|
Task<IReadOnlyList<StoryIntelligenceSavedSceneResult>> ListSceneResultsAsync(int runId);
|
|
Task<StoryIntelligenceImportCommit?> GetImportCommitAsync(int runId);
|
|
Task<StoryIntelligenceChapterImportReadiness> GetChapterImportReadinessAsync(int chapterId);
|
|
Task<StoryIntelligenceImportCommitResult> CommitImportAsync(StoryIntelligenceImportCommitRequest request);
|
|
}
|
|
|
|
public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceResultRepository
|
|
{
|
|
public async Task<int> SaveAsync(StoryIntelligenceRunSaveRequest request)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
connection.Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
|
|
try
|
|
{
|
|
var runId = await connection.QuerySingleAsync<int>(
|
|
"dbo.StoryIntelligenceRun_Save",
|
|
new
|
|
{
|
|
request.UserID,
|
|
request.ProjectID,
|
|
request.BookID,
|
|
request.Status,
|
|
request.SourceType,
|
|
request.SourceFileName,
|
|
request.SourceFileSizeBytes,
|
|
request.SourceWordCount,
|
|
request.SourceCharacterCount,
|
|
request.SourceChapterCount,
|
|
request.SourceDetectedImagesCount,
|
|
request.SourceDetectedTablesCount,
|
|
request.SourceDetectedFootnotesCount,
|
|
request.SourceDetectedCommentsCount,
|
|
request.PromptVersion,
|
|
request.Model,
|
|
request.StartedUtc,
|
|
request.CompletedUtc,
|
|
request.FailureStage,
|
|
request.TotalInputTokens,
|
|
request.TotalOutputTokens,
|
|
request.TotalTokens,
|
|
request.TotalDurationMs,
|
|
request.EstimatedCostGBP,
|
|
request.EstimatedCostUSD,
|
|
request.PromptVersionsSummary,
|
|
request.ErrorMessage,
|
|
request.ErrorDetail
|
|
},
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure);
|
|
|
|
int? chapterResultId = null;
|
|
if (request.ChapterResult is not null)
|
|
{
|
|
var chapter = request.ChapterResult;
|
|
chapterResultId = await connection.QuerySingleAsync<int>(
|
|
"dbo.StoryIntelligenceChapterResult_Save",
|
|
new
|
|
{
|
|
StoryIntelligenceRunID = runId,
|
|
chapter.ProjectID,
|
|
chapter.BookID,
|
|
chapter.ChapterID,
|
|
chapter.ChapterNumber,
|
|
chapter.SourceLabel,
|
|
chapter.PromptVersion,
|
|
chapter.Model,
|
|
chapter.RawResponseJson,
|
|
chapter.OutputTextJson,
|
|
chapter.ParsedJson,
|
|
chapter.ValidationErrorsCount,
|
|
chapter.ValidationWarningsCount,
|
|
chapter.InputTokens,
|
|
chapter.OutputTokens,
|
|
chapter.TotalTokens,
|
|
chapter.DurationMs
|
|
},
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
foreach (var scene in request.SceneResults)
|
|
{
|
|
await connection.ExecuteAsync(
|
|
"dbo.StoryIntelligenceSceneResult_Save",
|
|
new
|
|
{
|
|
StoryIntelligenceRunID = runId,
|
|
ChapterResultID = chapterResultId,
|
|
scene.ProjectID,
|
|
scene.BookID,
|
|
scene.ChapterID,
|
|
scene.SceneID,
|
|
scene.TemporarySceneNumber,
|
|
scene.StartParagraph,
|
|
scene.EndParagraph,
|
|
scene.SourceLabel,
|
|
scene.PromptVersion,
|
|
scene.Model,
|
|
scene.RawResponseJson,
|
|
scene.OutputTextJson,
|
|
scene.ParsedJson,
|
|
scene.ValidationErrorsCount,
|
|
scene.ValidationWarningsCount,
|
|
scene.InputTokens,
|
|
scene.OutputTokens,
|
|
scene.TotalTokens,
|
|
scene.DurationMs
|
|
},
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
transaction.Commit();
|
|
return runId;
|
|
}
|
|
catch
|
|
{
|
|
transaction.Rollback();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<int> QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
return await connection.QuerySingleAsync<int>(
|
|
"dbo.StoryIntelligenceRun_QueueAdminText",
|
|
new
|
|
{
|
|
request.UserID,
|
|
request.ProjectID,
|
|
request.BookID,
|
|
request.ChapterID,
|
|
request.ChapterNumber,
|
|
request.SourceType,
|
|
request.SourceLabel,
|
|
SourceText = request.ChapterText,
|
|
request.SourceFileName,
|
|
request.SourceFileSizeBytes,
|
|
request.SourceWordCount,
|
|
request.SourceCharacterCount,
|
|
request.SourceParagraphCount,
|
|
request.SourceChapterCount,
|
|
request.Model,
|
|
request.PromptVersionsSummary
|
|
},
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task<StoryIntelligenceQueuedRun?> ClaimNextPendingAsync()
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceQueuedRun>(
|
|
"dbo.StoryIntelligenceRun_ClaimNextPending",
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task UpdateProgressAsync(
|
|
int runId,
|
|
string? status = null,
|
|
string? currentStage = null,
|
|
string? currentMessage = null,
|
|
int? totalDetectedScenes = null,
|
|
int? completedScenes = null,
|
|
int? failedScenes = null,
|
|
int? totalInputTokens = null,
|
|
int? totalOutputTokens = null,
|
|
int? totalTokens = null,
|
|
long? totalDurationMs = null,
|
|
decimal? estimatedCostGBP = null,
|
|
decimal? estimatedCostUSD = null)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
await connection.ExecuteAsync(
|
|
"dbo.StoryIntelligenceRun_UpdateProgress",
|
|
new
|
|
{
|
|
StoryIntelligenceRunID = runId,
|
|
Status = status,
|
|
CurrentStage = currentStage,
|
|
CurrentMessage = currentMessage,
|
|
TotalDetectedScenes = totalDetectedScenes,
|
|
CompletedScenes = completedScenes,
|
|
FailedScenes = failedScenes,
|
|
TotalInputTokens = totalInputTokens,
|
|
TotalOutputTokens = totalOutputTokens,
|
|
TotalTokens = totalTokens,
|
|
TotalDurationMs = totalDurationMs,
|
|
EstimatedCostGBP = estimatedCostGBP,
|
|
EstimatedCostUSD = estimatedCostUSD
|
|
},
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task CompleteRunAsync(int runId, string status, int? totalInputTokens, int? totalOutputTokens, int? totalTokens, long? totalDurationMs, decimal? estimatedCostGBP, decimal? estimatedCostUSD)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
await connection.ExecuteAsync(
|
|
"dbo.StoryIntelligenceRun_Complete",
|
|
new
|
|
{
|
|
StoryIntelligenceRunID = runId,
|
|
Status = status,
|
|
TotalInputTokens = totalInputTokens,
|
|
TotalOutputTokens = totalOutputTokens,
|
|
TotalTokens = totalTokens,
|
|
TotalDurationMs = totalDurationMs,
|
|
EstimatedCostGBP = estimatedCostGBP,
|
|
EstimatedCostUSD = estimatedCostUSD
|
|
},
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task FailRunAsync(int runId, string failureStage, string errorMessage, string? errorDetail, long? totalDurationMs)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
await connection.ExecuteAsync(
|
|
"dbo.StoryIntelligenceRun_Fail",
|
|
new
|
|
{
|
|
StoryIntelligenceRunID = runId,
|
|
FailureStage = failureStage,
|
|
ErrorMessage = errorMessage,
|
|
ErrorDetail = errorDetail,
|
|
TotalDurationMs = totalDurationMs
|
|
},
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task RequestCancelAsync(int runId)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
await connection.ExecuteAsync(
|
|
"dbo.StoryIntelligenceRun_RequestCancel",
|
|
new { StoryIntelligenceRunID = runId },
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task CancelRunAsync(int runId, long? totalDurationMs)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
await connection.ExecuteAsync(
|
|
"dbo.StoryIntelligenceRun_Cancel",
|
|
new { StoryIntelligenceRunID = runId, TotalDurationMs = totalDurationMs },
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task<int> SaveChapterResultAsync(int runId, StoryIntelligenceChapterResultSaveRequest chapter)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
return await connection.QuerySingleAsync<int>(
|
|
"dbo.StoryIntelligenceChapterResult_Save",
|
|
new
|
|
{
|
|
StoryIntelligenceRunID = runId,
|
|
chapter.ProjectID,
|
|
chapter.BookID,
|
|
chapter.ChapterID,
|
|
chapter.ChapterNumber,
|
|
chapter.SourceLabel,
|
|
chapter.PromptVersion,
|
|
chapter.Model,
|
|
chapter.RawResponseJson,
|
|
chapter.OutputTextJson,
|
|
chapter.ParsedJson,
|
|
chapter.ValidationErrorsCount,
|
|
chapter.ValidationWarningsCount,
|
|
chapter.InputTokens,
|
|
chapter.OutputTokens,
|
|
chapter.TotalTokens,
|
|
chapter.DurationMs
|
|
},
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task<int> SaveSceneResultAsync(int runId, int? chapterResultId, StoryIntelligenceSceneResultSaveRequest scene)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
return await connection.QuerySingleAsync<int>(
|
|
"dbo.StoryIntelligenceSceneResult_Save",
|
|
new
|
|
{
|
|
StoryIntelligenceRunID = runId,
|
|
ChapterResultID = chapterResultId,
|
|
scene.ProjectID,
|
|
scene.BookID,
|
|
scene.ChapterID,
|
|
scene.SceneID,
|
|
scene.TemporarySceneNumber,
|
|
scene.StartParagraph,
|
|
scene.EndParagraph,
|
|
scene.SourceLabel,
|
|
scene.PromptVersion,
|
|
scene.Model,
|
|
scene.RawResponseJson,
|
|
scene.OutputTextJson,
|
|
scene.ParsedJson,
|
|
scene.ValidationErrorsCount,
|
|
scene.ValidationWarningsCount,
|
|
scene.InputTokens,
|
|
scene.OutputTokens,
|
|
scene.TotalTokens,
|
|
scene.DurationMs
|
|
},
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<StoryIntelligenceSavedRunListItem>> ListRunsAsync()
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
var rows = await connection.QueryAsync<StoryIntelligenceSavedRunListItem>(
|
|
"dbo.StoryIntelligenceRun_ListAdmin",
|
|
commandType: CommandType.StoredProcedure);
|
|
return rows.ToList();
|
|
}
|
|
|
|
public async Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceSavedRun>(
|
|
"dbo.StoryIntelligenceRun_GetAdmin",
|
|
new { StoryIntelligenceRunID = runId },
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task<StoryIntelligenceSavedChapterResult?> GetChapterResultAsync(int runId)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceSavedChapterResult>(
|
|
"dbo.StoryIntelligenceChapterResult_GetByRun",
|
|
new { StoryIntelligenceRunID = runId },
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<StoryIntelligenceSavedSceneResult>> ListSceneResultsAsync(int runId)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
var rows = await connection.QueryAsync<StoryIntelligenceSavedSceneResult>(
|
|
"dbo.StoryIntelligenceSceneResult_ListByRun",
|
|
new { StoryIntelligenceRunID = runId },
|
|
commandType: CommandType.StoredProcedure);
|
|
return rows.ToList();
|
|
}
|
|
|
|
public async Task<StoryIntelligenceImportCommit?> GetImportCommitAsync(int runId)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceImportCommit>(
|
|
"dbo.StoryIntelligenceImportCommit_GetByRun",
|
|
new { StoryIntelligenceRunID = runId },
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task<StoryIntelligenceChapterImportReadiness> GetChapterImportReadinessAsync(int chapterId)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
return await connection.QuerySingleAsync<StoryIntelligenceChapterImportReadiness>(
|
|
"dbo.StoryIntelligenceImportCommit_GetChapterReadiness",
|
|
new { ChapterID = chapterId },
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
public async Task<StoryIntelligenceImportCommitResult> CommitImportAsync(StoryIntelligenceImportCommitRequest request)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
connection.Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
|
|
var scenesCreated = 0;
|
|
var metricsCreated = 0;
|
|
var commitWarnings = new List<string>();
|
|
try
|
|
{
|
|
var alreadyCommitted = await connection.QuerySingleAsync<bool>(
|
|
"dbo.StoryIntelligenceImportCommit_HasCompleted",
|
|
new { request.StoryIntelligenceRunID },
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure);
|
|
if (alreadyCommitted)
|
|
{
|
|
transaction.Rollback();
|
|
return new StoryIntelligenceImportCommitResult
|
|
{
|
|
Success = false,
|
|
Message = "This Story Intelligence run has already been committed."
|
|
};
|
|
}
|
|
|
|
var readiness = await connection.QuerySingleAsync<StoryIntelligenceChapterImportReadiness>(
|
|
"dbo.StoryIntelligenceImportCommit_GetChapterReadiness",
|
|
new { request.ChapterID },
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure);
|
|
if (!readiness.CanCommit)
|
|
{
|
|
transaction.Rollback();
|
|
return new StoryIntelligenceImportCommitResult
|
|
{
|
|
Success = false,
|
|
Message = readiness.BlockReason ?? "This chapter already contains scenes."
|
|
};
|
|
}
|
|
|
|
var orderedScenes = request.Scenes.OrderBy(scene => scene.TemporarySceneNumber).ToList();
|
|
for (var index = 0; index < orderedScenes.Count; index++)
|
|
{
|
|
var item = orderedScenes[index];
|
|
var operation = "preparing scene";
|
|
|
|
try
|
|
{
|
|
operation = "saving scene";
|
|
var relativeTimeText = TrimOptionalText(
|
|
item.ParsedScene.Setting?.DateOrTimeReference ?? item.ParsedScene.Setting?.TimeOfDay,
|
|
200,
|
|
$"Scene {item.TemporarySceneNumber:N0} relative time",
|
|
commitWarnings);
|
|
|
|
var sceneTitle = TrimOptionalText(item.SceneTitle, 200, $"Scene {item.TemporarySceneNumber:N0} title", commitWarnings)
|
|
?? $"Scene {item.TemporarySceneNumber:N0}";
|
|
|
|
var sceneId = await connection.QuerySingleAsync<int>(
|
|
"dbo.Scene_Save",
|
|
new
|
|
{
|
|
SceneID = (int?)null,
|
|
ChapterID = request.ChapterID,
|
|
SceneNumber = Convert.ToDecimal(item.TemporarySceneNumber),
|
|
SceneTitle = sceneTitle,
|
|
Summary = item.Summary,
|
|
TimeModeID = request.TimeModeID,
|
|
StartDateTime = (DateTime?)null,
|
|
EndDateTime = (DateTime?)null,
|
|
DurationAmount = (decimal?)null,
|
|
DurationUnitID = (int?)null,
|
|
RelativeTimeText = relativeTimeText,
|
|
TimeConfidenceID = request.TimeConfidenceID,
|
|
ScenePurposeNotes = item.PurposeNotes,
|
|
SceneOutcomeNotes = item.OutcomeNotes,
|
|
RevisionStatusID = request.RevisionStatusID,
|
|
PrimaryLocationID = (int?)null,
|
|
FloorPlanID = (int?)null,
|
|
InitialFloorPlanFloorID = (int?)null
|
|
},
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure);
|
|
|
|
operation = "saving source tracking";
|
|
await connection.ExecuteAsync(
|
|
"dbo.StoryIntelligenceSceneSource_Save",
|
|
new
|
|
{
|
|
SceneID = sceneId,
|
|
ImportSource = "Story Intelligence",
|
|
ImportRunID = request.StoryIntelligenceRunID,
|
|
SourceStartParagraph = item.StartParagraph,
|
|
SourceEndParagraph = item.EndParagraph
|
|
},
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure);
|
|
|
|
if (item.PovCharacterID.HasValue)
|
|
{
|
|
operation = "linking POV character";
|
|
await TryOptionalSceneOperationAsync(
|
|
connection,
|
|
transaction,
|
|
item.TemporarySceneNumber,
|
|
operation,
|
|
commitWarnings,
|
|
() => connection.ExecuteAsync(
|
|
"dbo.Scene_UpdatePovCharacter",
|
|
new { SceneID = sceneId, POVCharacterID = item.PovCharacterID },
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure));
|
|
}
|
|
|
|
if (item.PurposeTypeIDs.Count > 0)
|
|
{
|
|
operation = "linking purpose labels";
|
|
await TryOptionalSceneOperationAsync(
|
|
connection,
|
|
transaction,
|
|
item.TemporarySceneNumber,
|
|
operation,
|
|
commitWarnings,
|
|
() => connection.ExecuteAsync(
|
|
"dbo.Scene_SavePurposes",
|
|
new { SceneID = sceneId, PurposeIds = string.Join(',', item.PurposeTypeIDs.Distinct()) },
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure));
|
|
}
|
|
|
|
foreach (var metric in item.Metrics)
|
|
{
|
|
operation = $"saving metric '{metric.MetricName}'";
|
|
var saved = await TryOptionalSceneOperationAsync(
|
|
connection,
|
|
transaction,
|
|
item.TemporarySceneNumber,
|
|
operation,
|
|
commitWarnings,
|
|
() => connection.ExecuteAsync(
|
|
"dbo.SceneMetric_SaveValue",
|
|
new { SceneID = sceneId, metric.MetricTypeID, metric.Value, metric.Notes },
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure));
|
|
if (saved)
|
|
{
|
|
metricsCreated++;
|
|
}
|
|
}
|
|
|
|
if (request.TimelineNoteTypeID.HasValue && !string.IsNullOrWhiteSpace(item.ImportNoteText))
|
|
{
|
|
operation = "saving import note";
|
|
await TryOptionalSceneOperationAsync(
|
|
connection,
|
|
transaction,
|
|
item.TemporarySceneNumber,
|
|
operation,
|
|
commitWarnings,
|
|
() => connection.ExecuteAsync(
|
|
"dbo.SceneNote_Save",
|
|
new
|
|
{
|
|
SceneNoteID = (int?)null,
|
|
SceneID = sceneId,
|
|
SceneNoteTypeID = request.TimelineNoteTypeID.Value,
|
|
NoteTitle = "Story Intelligence import notes",
|
|
NoteText = item.ImportNoteText,
|
|
SortOrder = 10,
|
|
IsPinned = false,
|
|
IsResolved = false
|
|
},
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure));
|
|
}
|
|
|
|
scenesCreated++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Scene {item.TemporarySceneNumber:N0} failed during {operation}: {ex.Message}",
|
|
ex);
|
|
}
|
|
}
|
|
|
|
var notes = new List<string>();
|
|
notes.Add("Committed into empty chapter.");
|
|
notes.AddRange(request.Warnings.Where(warning => !string.IsNullOrWhiteSpace(warning)));
|
|
notes.AddRange(commitWarnings.Where(warning => !string.IsNullOrWhiteSpace(warning)));
|
|
|
|
var commitId = await connection.QuerySingleAsync<int>(
|
|
"dbo.StoryIntelligenceImportCommit_RecordCompleted",
|
|
new
|
|
{
|
|
request.StoryIntelligenceRunID,
|
|
request.ProjectID,
|
|
request.BookID,
|
|
request.ChapterID,
|
|
CommittedByUserID = request.UserID,
|
|
ScenesCreated = scenesCreated,
|
|
MetricsCreated = metricsCreated,
|
|
Notes = string.Join(Environment.NewLine, notes)
|
|
},
|
|
transaction,
|
|
commandType: CommandType.StoredProcedure);
|
|
|
|
transaction.Commit();
|
|
return new StoryIntelligenceImportCommitResult
|
|
{
|
|
Success = true,
|
|
CommitID = commitId,
|
|
ScenesCreated = scenesCreated,
|
|
MetricsCreated = metricsCreated,
|
|
Message = $"Created {scenesCreated:N0} scene(s)."
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
transaction.Rollback();
|
|
await RecordFailedCommitAsync(request, 0, 0, ex);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task RecordFailedCommitAsync(StoryIntelligenceImportCommitRequest request, int scenesCreated, int metricsCreated, Exception ex)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
await connection.ExecuteAsync(
|
|
"dbo.StoryIntelligenceImportCommit_RecordFailed",
|
|
new
|
|
{
|
|
request.StoryIntelligenceRunID,
|
|
request.ProjectID,
|
|
request.BookID,
|
|
request.ChapterID,
|
|
CommittedByUserID = request.UserID,
|
|
ScenesCreated = scenesCreated,
|
|
MetricsCreated = metricsCreated,
|
|
Notes = ex.Message
|
|
},
|
|
commandType: CommandType.StoredProcedure);
|
|
}
|
|
|
|
private static async Task<bool> TryOptionalSceneOperationAsync(
|
|
IDbConnection connection,
|
|
IDbTransaction transaction,
|
|
int sceneNumber,
|
|
string operation,
|
|
ICollection<string> warnings,
|
|
Func<Task<int>> action)
|
|
{
|
|
try
|
|
{
|
|
await action();
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
warnings.Add($"Scene {sceneNumber:N0}: skipped optional {operation} because the database rejected it: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string? TrimOptionalText(string? value, int maxLength, string label, ICollection<string> warnings)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var trimmed = value.Trim();
|
|
if (trimmed.Length <= maxLength)
|
|
{
|
|
return trimmed;
|
|
}
|
|
|
|
warnings.Add($"{label} was truncated to {maxLength:N0} characters for the database field limit.");
|
|
return trimmed[..maxLength].TrimEnd();
|
|
}
|
|
}
|