Implement portrait prompt API and UI enhancements. Added a new endpoint to fetch portrait prompts, updated the client to handle prompt display, and improved localization for prompt-related text. Enhanced styling for prompt elements in the UI.
ci / server (push) Failing after 3m40s
ci / client (push) Failing after 15s

This commit is contained in:
Leonid Pershin
2026-08-20 05:27:01 +03:00
parent a214378c6e
commit 3fa6ce95df
13 changed files with 401 additions and 34 deletions
+106 -8
View File
@@ -50,6 +50,38 @@ internal sealed class PortraitService(
return outcome.Error;
}
public async Task<PortraitPromptBuildResult> BuildPromptAsync(
int schoolId,
string personId,
PortraitKind kind,
string? promptExtra,
CancellationToken cancellationToken)
{
var resolved = ResolveCustomPromptExtra(schoolId, personId, kind, promptExtra);
if (kind == PortraitKind.Custom && resolved is null)
{
return PortraitPromptBuildResult.InvalidPrompt;
}
var outcome = await LookupPersonAsync(schoolId, personId, cancellationToken);
if (outcome.Error == PersonLookupError.UnknownPerson)
{
return PortraitPromptBuildResult.UnknownPerson;
}
if (outcome.Error != PersonLookupError.None || outcome.Card is null)
{
return PortraitPromptBuildResult.UnknownSchool;
}
var (positive, negative) = PortraitPromptBuilder.Build(outcome.Card, settings, kind, resolved);
return PortraitPromptBuildResult.Succeeded(
kind,
positive,
negative,
kind == PortraitKind.Custom ? resolved : null);
}
public async Task<PortraitGenerationResult> GenerateAsync(
int schoolId,
string personId,
@@ -76,14 +108,7 @@ internal sealed class PortraitService(
}
}
var command = new GameCommand.GetPerson(
schoolId,
personId,
PromptLocale,
NewCompletion<PersonCardResult>());
commands.Enqueue(command);
var outcome = await command.Result.Task.WaitAsync(PersonLookupTimeout, cancellationToken);
var outcome = await LookupPersonAsync(schoolId, personId, cancellationToken);
if (outcome.Error == PersonLookupError.UnknownPerson)
{
return PortraitGenerationResult.UnknownPerson;
@@ -126,10 +151,83 @@ internal sealed class PortraitService(
}
}
private async Task<PersonCardResult> LookupPersonAsync(
int schoolId,
string personId,
CancellationToken cancellationToken)
{
var command = new GameCommand.GetPerson(
schoolId,
personId,
PromptLocale,
NewCompletion<PersonCardResult>());
commands.Enqueue(command);
return await command.Result.Task.WaitAsync(PersonLookupTimeout, cancellationToken);
}
private string? ResolveCustomPromptExtra(int schoolId, string personId, PortraitKind kind, string? promptExtra)
{
if (kind != PortraitKind.Custom)
{
return null;
}
promptExtra = promptExtra?.Trim();
if (string.IsNullOrWhiteSpace(promptExtra))
{
promptExtra = store.TryReadCustomPortraitPrompt(schoolId, personId)?.Trim();
}
if (string.IsNullOrWhiteSpace(promptExtra) || promptExtra.Length > MaxCustomPromptLength)
{
return null;
}
return promptExtra;
}
private static TaskCompletionSource<T> NewCompletion<T>() =>
new(TaskCreationOptions.RunContinuationsAsynchronously);
}
internal enum PortraitPromptBuildOutcome
{
Succeeded,
UnknownSchool,
UnknownPerson,
InvalidPrompt,
}
internal sealed record PortraitPromptBuildResult(
PortraitPromptBuildOutcome Outcome,
PortraitKind Kind,
string Positive,
string Negative,
string? PromptExtra)
{
public static PortraitPromptBuildResult UnknownSchool { get; } =
new(PortraitPromptBuildOutcome.UnknownSchool, default, string.Empty, string.Empty, null);
public static PortraitPromptBuildResult UnknownPerson { get; } =
new(PortraitPromptBuildOutcome.UnknownPerson, default, string.Empty, string.Empty, null);
public static PortraitPromptBuildResult InvalidPrompt { get; } =
new(PortraitPromptBuildOutcome.InvalidPrompt, default, string.Empty, string.Empty, null);
public static PortraitPromptBuildResult Succeeded(
PortraitKind kind,
string positive,
string negative,
string? promptExtra) =>
new(PortraitPromptBuildOutcome.Succeeded, kind, positive, negative, promptExtra);
}
internal sealed record PortraitPromptResponse(
string Kind,
string Positive,
string Negative,
string? PromptExtra);
internal enum PortraitGenerationOutcome
{
Succeeded,