diff --git a/src/Loom.Web/Components/Pages/ArtifactInspector.razor b/src/Loom.Web/Components/Pages/ArtifactInspector.razor index 470f436..32ecc79 100644 --- a/src/Loom.Web/Components/Pages/ArtifactInspector.razor +++ b/src/Loom.Web/Components/Pages/ArtifactInspector.razor @@ -80,7 +80,7 @@ else @RenderTestPlan(_artifact.Canonical.ExternalId) break; default: -
@_artifact.Canonical.ExternalId+ @RenderFallback(_artifact.Canonical.ExternalId, _artifact.Kind) break; } @@ -257,11 +257,7 @@ else } else if (description is null) { - // Bad JSON — fall back to raw text so the body isn't lost. - builder.OpenElement(20, "pre"); - builder.AddAttribute(21, "class", "prompt-block"); - builder.AddContent(22, body); - builder.CloseElement(); + builder.AddContent(20, RenderFallback(body, Loom.Domain.Artifacts.ArtifactKind.Wireframe)); } }; @@ -270,15 +266,17 @@ else var items = TryParseCriteria(body); if (items is null || items.Count == 0) { - builder.OpenElement(0, "pre"); - builder.AddAttribute(1, "class", "prompt-block"); - builder.AddContent(2, body); - builder.CloseElement(); + builder.AddContent(0, RenderFallback(body, Loom.Domain.Artifacts.ArtifactKind.Criteria)); return; } - builder.OpenElement(10, "ul"); - builder.AddAttribute(11, "class", "criteria-list"); - var seq = 12; + builder.AddContent(10, RenderCriteriaList(items)); + }; + + private RenderFragment RenderCriteriaList(IReadOnlyList
if neither matches.
@@ -440,10 +551,7 @@ else
}
}
}
- builder.OpenElement(10, "pre");
- builder.AddAttribute(11, "class", "prompt-block");
- builder.AddContent(12, body);
- builder.CloseElement();
+ builder.AddContent(10, RenderFallback(body, Loom.Domain.Artifacts.ArtifactKind.Doc));
};
private static string GetString(System.Text.Json.JsonElement el, string name) =>
@@ -585,10 +693,7 @@ else
try { doc = System.Text.Json.JsonDocument.Parse(body); } catch (System.Text.Json.JsonException) { }
if (doc is null)
{
- builder.OpenElement(0, "pre");
- builder.AddAttribute(1, "class", "prompt-block");
- builder.AddContent(2, body);
- builder.CloseElement();
+ builder.AddContent(0, RenderFallback(body, Loom.Domain.Artifacts.ArtifactKind.TestPlan));
return;
}
using (doc)
@@ -639,6 +744,343 @@ else
builder.CloseElement();
}
+ private RenderFragment RenderDiscovery(DiscoveryView v) => builder =>
+ {
+ var seq = 0;
+ if (!string.IsNullOrWhiteSpace(v.Title))
+ {
+ builder.OpenElement(seq++, "h3"); builder.AddContent(seq++, v.Title); builder.CloseElement();
+ }
+ if (!string.IsNullOrWhiteSpace(v.Intent))
+ {
+ builder.OpenElement(seq++, "p"); builder.AddContent(seq++, v.Intent); builder.CloseElement();
+ }
+
+ if (v.Outcomes.Count > 0)
+ {
+ builder.OpenElement(seq++, "h3"); builder.AddContent(seq++, "Outcomes"); builder.CloseElement();
+ builder.OpenElement(seq++, "ul"); builder.AddAttribute(seq++, "class", "criteria-list");
+ foreach (var o in v.Outcomes)
+ {
+ builder.OpenElement(seq++, "li");
+ builder.OpenElement(seq++, "div");
+ builder.AddContent(seq++, o.Statement);
+ if (o.Measurable)
+ {
+ builder.OpenElement(seq++, "span");
+ builder.AddAttribute(seq++, "class", "badge");
+ builder.AddAttribute(seq++, "style", "margin-left:.5rem;");
+ builder.AddContent(seq++, "measurable");
+ builder.CloseElement();
+ }
+ builder.CloseElement();
+ if (!string.IsNullOrWhiteSpace(o.MetricHint))
+ {
+ builder.OpenElement(seq++, "div");
+ builder.AddAttribute(seq++, "class", "muted");
+ builder.AddContent(seq++, o.MetricHint);
+ builder.CloseElement();
+ }
+ builder.CloseElement();
+ }
+ builder.CloseElement();
+ }
+
+ if (v.Hypotheses.Count > 0)
+ {
+ builder.OpenElement(seq++, "h3"); builder.AddContent(seq++, "Hypotheses"); builder.CloseElement();
+ builder.OpenElement(seq++, "table"); builder.AddAttribute(seq++, "class", "ws-table");
+ builder.OpenElement(seq++, "thead"); builder.OpenElement(seq++, "tr");
+ foreach (var h in new[] { "If", "Then", "Because" })
+ { builder.OpenElement(seq++, "th"); builder.AddContent(seq++, h); builder.CloseElement(); }
+ builder.CloseElement(); builder.CloseElement();
+ builder.OpenElement(seq++, "tbody");
+ foreach (var h in v.Hypotheses)
+ {
+ builder.OpenElement(seq++, "tr");
+ foreach (var c in new[] { h.If, h.Then, h.Because })
+ { builder.OpenElement(seq++, "td"); builder.AddContent(seq++, c); builder.CloseElement(); }
+ builder.CloseElement();
+ }
+ builder.CloseElement(); builder.CloseElement();
+ }
+
+ if (v.Stakeholders.Count > 0)
+ {
+ builder.OpenElement(seq++, "h3"); builder.AddContent(seq++, "Stakeholders"); builder.CloseElement();
+ builder.OpenElement(seq++, "table"); builder.AddAttribute(seq++, "class", "ws-table");
+ builder.OpenElement(seq++, "thead"); builder.OpenElement(seq++, "tr");
+ foreach (var h in new[] { "Name", "Role", "Interest" })
+ { builder.OpenElement(seq++, "th"); builder.AddContent(seq++, h); builder.CloseElement(); }
+ builder.CloseElement(); builder.CloseElement();
+ builder.OpenElement(seq++, "tbody");
+ foreach (var s in v.Stakeholders)
+ {
+ builder.OpenElement(seq++, "tr");
+ foreach (var c in new[] { s.Name, s.Role, s.Interest ?? string.Empty })
+ { builder.OpenElement(seq++, "td"); builder.AddContent(seq++, c); builder.CloseElement(); }
+ builder.CloseElement();
+ }
+ builder.CloseElement(); builder.CloseElement();
+ }
+
+ if (v.OpenQuestions.Count > 0)
+ {
+ builder.OpenElement(seq++, "h3"); builder.AddContent(seq++, "Open questions"); builder.CloseElement();
+ builder.OpenElement(seq++, "ul");
+ foreach (var q in v.OpenQuestions)
+ {
+ builder.OpenElement(seq++, "li"); builder.AddContent(seq++, q); builder.CloseElement();
+ }
+ builder.CloseElement();
+ }
+ };
+
+ private RenderFragment RenderDecomposition(DecompositionView v) => builder =>
+ {
+ var seq = 0;
+ builder.OpenElement(seq++, "table"); builder.AddAttribute(seq++, "class", "ws-table");
+ builder.OpenElement(seq++, "thead"); builder.OpenElement(seq++, "tr");
+ foreach (var h in new[] { "Slug", "Title", "Type", "Parent", "Intent" })
+ { builder.OpenElement(seq++, "th"); builder.AddContent(seq++, h); builder.CloseElement(); }
+ builder.CloseElement(); builder.CloseElement();
+ builder.OpenElement(seq++, "tbody");
+ foreach (var c in v.Children)
+ {
+ builder.OpenElement(seq++, "tr");
+
+ builder.OpenElement(seq++, "td");
+ builder.OpenElement(seq++, "code"); builder.AddContent(seq++, c.Slug); builder.CloseElement();
+ builder.CloseElement();
+
+ builder.OpenElement(seq++, "td"); builder.AddContent(seq++, c.Title); builder.CloseElement();
+
+ builder.OpenElement(seq++, "td");
+ if (!string.IsNullOrWhiteSpace(c.Type))
+ {
+ builder.OpenElement(seq++, "span"); builder.AddAttribute(seq++, "class", "badge");
+ builder.AddContent(seq++, c.Type); builder.CloseElement();
+ }
+ builder.CloseElement();
+
+ builder.OpenElement(seq++, "td");
+ if (!string.IsNullOrWhiteSpace(c.ParentSlug))
+ {
+ builder.OpenElement(seq++, "code"); builder.AddContent(seq++, c.ParentSlug); builder.CloseElement();
+ }
+ builder.CloseElement();
+
+ builder.OpenElement(seq++, "td"); builder.AddContent(seq++, c.Intent ?? string.Empty); builder.CloseElement();
+
+ builder.CloseElement();
+ }
+ builder.CloseElement(); builder.CloseElement();
+ };
+
+ private const int GenericJsonMaxDepth = 5;
+
+ private RenderFragment RenderGenericJson(System.Text.Json.JsonElement root) => builder =>
+ {
+ var seq = 0;
+ RenderJsonValue(builder, ref seq, root, depth: 0);
+ };
+
+ private static void RenderJsonValue(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder,
+ ref int seq, System.Text.Json.JsonElement el, int depth)
+ {
+ if (depth >= GenericJsonMaxDepth)
+ {
+ builder.OpenElement(seq++, "pre"); builder.AddAttribute(seq++, "class", "prompt-block");
+ builder.AddContent(seq++, el.GetRawText());
+ builder.CloseElement();
+ return;
+ }
+
+ switch (el.ValueKind)
+ {
+ case System.Text.Json.JsonValueKind.Object:
+ RenderJsonObject(builder, ref seq, el, depth);
+ break;
+ case System.Text.Json.JsonValueKind.Array:
+ RenderJsonArray(builder, ref seq, el, depth);
+ break;
+ case System.Text.Json.JsonValueKind.String:
+ builder.AddContent(seq++, el.GetString());
+ break;
+ case System.Text.Json.JsonValueKind.Number:
+ case System.Text.Json.JsonValueKind.True:
+ case System.Text.Json.JsonValueKind.False:
+ builder.AddContent(seq++, el.GetRawText());
+ break;
+ case System.Text.Json.JsonValueKind.Null:
+ case System.Text.Json.JsonValueKind.Undefined:
+ builder.OpenElement(seq++, "span"); builder.AddAttribute(seq++, "class", "muted");
+ builder.AddContent(seq++, "—"); builder.CloseElement();
+ break;
+ }
+ }
+
+ private static void RenderJsonObject(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder,
+ ref int seq, System.Text.Json.JsonElement obj, int depth)
+ {
+ foreach (var prop in obj.EnumerateObject())
+ {
+ builder.OpenElement(seq++, "h4"); builder.AddContent(seq++, prop.Name); builder.CloseElement();
+ builder.OpenElement(seq++, "div"); builder.AddAttribute(seq++, "style", "margin-left:.75rem;");
+ RenderJsonValue(builder, ref seq, prop.Value, depth + 1);
+ builder.CloseElement();
+ }
+ }
+
+ private static void RenderJsonArray(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder,
+ ref int seq, System.Text.Json.JsonElement arr, int depth)
+ {
+ if (arr.GetArrayLength() == 0)
+ {
+ builder.OpenElement(seq++, "span"); builder.AddAttribute(seq++, "class", "muted");
+ builder.AddContent(seq++, "(empty)"); builder.CloseElement();
+ return;
+ }
+
+ var allStrings = true;
+ var allObjects = true;
+ foreach (var el in arr.EnumerateArray())
+ {
+ if (el.ValueKind != System.Text.Json.JsonValueKind.String) allStrings = false;
+ if (el.ValueKind != System.Text.Json.JsonValueKind.Object) allObjects = false;
+ if (!allStrings && !allObjects) break;
+ }
+
+ if (allStrings)
+ {
+ builder.OpenElement(seq++, "ul");
+ foreach (var el in arr.EnumerateArray())
+ {
+ builder.OpenElement(seq++, "li"); builder.AddContent(seq++, el.GetString()); builder.CloseElement();
+ }
+ builder.CloseElement();
+ return;
+ }
+
+ if (allObjects)
+ {
+ var columns = new List();
+ var seen = new HashSet(StringComparer.Ordinal);
+ foreach (var el in arr.EnumerateArray())
+ {
+ foreach (var p in el.EnumerateObject())
+ {
+ if (p.Value.ValueKind == System.Text.Json.JsonValueKind.String && seen.Add(p.Name))
+ {
+ columns.Add(p.Name);
+ if (columns.Count >= 6) break;
+ }
+ }
+ if (columns.Count >= 6) break;
+ }
+ if (columns.Count > 0)
+ {
+ builder.OpenElement(seq++, "table"); builder.AddAttribute(seq++, "class", "ws-table");
+ builder.OpenElement(seq++, "thead"); builder.OpenElement(seq++, "tr");
+ foreach (var c in columns)
+ { builder.OpenElement(seq++, "th"); builder.AddContent(seq++, c); builder.CloseElement(); }
+ builder.CloseElement(); builder.CloseElement();
+ builder.OpenElement(seq++, "tbody");
+ foreach (var el in arr.EnumerateArray())
+ {
+ builder.OpenElement(seq++, "tr");
+ foreach (var c in columns)
+ {
+ builder.OpenElement(seq++, "td");
+ if (el.TryGetProperty(c, out var v) && v.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ builder.AddContent(seq++, v.GetString());
+ }
+ builder.CloseElement();
+ }
+ builder.CloseElement();
+ }
+ builder.CloseElement(); builder.CloseElement();
+ return;
+ }
+ }
+
+ builder.OpenElement(seq++, "ul");
+ foreach (var el in arr.EnumerateArray())
+ {
+ builder.OpenElement(seq++, "li");
+ RenderJsonValue(builder, ref seq, el, depth + 1);
+ builder.CloseElement();
+ }
+ builder.CloseElement();
+ }
+
+ private static bool KindHasKnownSchema(Loom.Domain.Artifacts.ArtifactKind kind) => kind switch
+ {
+ Loom.Domain.Artifacts.ArtifactKind.Wireframe => true,
+ Loom.Domain.Artifacts.ArtifactKind.Criteria => true,
+ Loom.Domain.Artifacts.ArtifactKind.Risks => true,
+ Loom.Domain.Artifacts.ArtifactKind.Doc => true,
+ Loom.Domain.Artifacts.ArtifactKind.TestPlan => true,
+ _ => false,
+ };
+
+ private RenderFragment RenderFallback(string body, Loom.Domain.Artifacts.ArtifactKind kind) => builder =>
+ {
+ System.Text.Json.JsonDocument? doc = null;
+ try { doc = System.Text.Json.JsonDocument.Parse(body); } catch (System.Text.Json.JsonException) { }
+
+ if (doc is null)
+ {
+ builder.OpenElement(0, "pre");
+ builder.AddAttribute(1, "class", "prompt-block");
+ builder.AddContent(2, body);
+ builder.CloseElement();
+ return;
+ }
+
+ using (doc)
+ {
+ var root = doc.RootElement;
+ var discovery = TryParseDiscovery(root);
+ var decomposition = discovery is null ? TryParseDecomposition(root) : null;
+ var criteria = discovery is null && decomposition is null && kind != Loom.Domain.Artifacts.ArtifactKind.Criteria
+ ? TryParseCriteria(body) : null;
+ var risks = discovery is null && decomposition is null && criteria is null && kind != Loom.Domain.Artifacts.ArtifactKind.Risks
+ ? TryParseRisks(body) : null;
+
+ if (KindHasKnownSchema(kind))
+ {
+ builder.OpenElement(0, "p");
+ builder.AddAttribute(1, "class", "muted");
+ builder.AddAttribute(2, "style", "font-size:.875rem;");
+ builder.AddContent(3, $"Content didn't match expected schema for {kind} — rendering generically.");
+ builder.CloseElement();
+ }
+
+ if (discovery is not null)
+ {
+ builder.AddContent(10, RenderDiscovery(discovery));
+ }
+ else if (decomposition is not null)
+ {
+ builder.AddContent(10, RenderDecomposition(decomposition));
+ }
+ else if (criteria is { Count: > 0 })
+ {
+ builder.AddContent(10, RenderCriteriaList(criteria));
+ }
+ else if (risks is { Count: > 0 })
+ {
+ builder.AddContent(10, RenderRisksTable(risks));
+ }
+ else
+ {
+ builder.AddContent(10, RenderGenericJson(root));
+ }
+ }
+ };
+
[Parameter] public Guid ArtifactId { get; set; }
private bool _loaded;