mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 18:32:12 +00:00
Merge 319ca8806e into b9ec13edcf
This commit is contained in:
commit
f08ade315d
@ -339,6 +339,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
|||||||
this.ChatThread = new()
|
this.ChatThread = new()
|
||||||
{
|
{
|
||||||
IncludeDateTime = false,
|
IncludeDateTime = false,
|
||||||
|
AllowChartOutput = true,
|
||||||
SelectedProvider = this.ProviderSettings.Id,
|
SelectedProvider = this.ProviderSettings.Id,
|
||||||
SelectedProfile = this.AllowProfiles ? this.CurrentProfile.Id : Profile.NO_PROFILE.Id,
|
SelectedProfile = this.AllowProfiles ? this.CurrentProfile.Id : Profile.NO_PROFILE.Id,
|
||||||
SystemPrompt = this.SystemPrompt,
|
SystemPrompt = this.SystemPrompt,
|
||||||
@ -355,6 +356,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
|||||||
this.ChatThread = new()
|
this.ChatThread = new()
|
||||||
{
|
{
|
||||||
IncludeDateTime = false,
|
IncludeDateTime = false,
|
||||||
|
AllowChartOutput = true,
|
||||||
SelectedProvider = this.ProviderSettings.Id,
|
SelectedProvider = this.ProviderSettings.Id,
|
||||||
SelectedProfile = this.AllowProfiles ? this.CurrentProfile.Id : Profile.NO_PROFILE.Id,
|
SelectedProfile = this.AllowProfiles ? this.CurrentProfile.Id : Profile.NO_PROFILE.Id,
|
||||||
SystemPrompt = this.SystemPrompt,
|
SystemPrompt = this.SystemPrompt,
|
||||||
|
|||||||
@ -2797,6 +2797,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTE
|
|||||||
-- The model response used an unsupported contract version. Please try again or select another model.
|
-- The model response used an unsupported contract version. Please try again or select another model.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T921285247"] = "The model response used an unsupported contract version. Please try again or select another model."
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T921285247"] = "The model response used an unsupported contract version. Please try again or select another model."
|
||||||
|
|
||||||
|
-- This chart cannot be displayed: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHARTBLOCK::T1070038198"] = "This chart cannot be displayed: {0}"
|
||||||
|
|
||||||
-- System
|
-- System
|
||||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T2402387132"] = "System"
|
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T2402387132"] = "System"
|
||||||
|
|
||||||
|
|||||||
61
app/MindWork AI Studio/Chat/ChartBlock.razor
Normal file
61
app/MindWork AI Studio/Chat/ChartBlock.razor
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
@namespace AIStudio.Chat
|
||||||
|
@using MudBlazor
|
||||||
|
@inherits AIStudio.Components.MSGComponentBase
|
||||||
|
|
||||||
|
@if (this.Result.Chart is { } chart)
|
||||||
|
{
|
||||||
|
<div class="chart-block-shell my-3">
|
||||||
|
<MudPaper Class="chart-block" Outlined="true">
|
||||||
|
<MudText Typo="Typo.h6" Class="chart-block-title">@chart.Title</MudText>
|
||||||
|
@if (chart.Type is ChartDefinitionType.TIME_SERIES)
|
||||||
|
{
|
||||||
|
<div class="chart-block-plot chart-block-plot-axis">
|
||||||
|
<MudTimeSeriesChart ChartSeries="@this.TimeSeriesChartSeries"
|
||||||
|
ChartOptions="@this.ChartOptions"
|
||||||
|
AxisChartOptions="@this.AxisChartOptions"
|
||||||
|
TimeLabelSpacing="@this.TimeLabelSpacing"
|
||||||
|
TimeLabelFormat="@this.TimeLabelFormat"
|
||||||
|
DataMarkerTooltipTimeLabelFormat="dd-MM-yyyy HH:mm:ss 'UTC'"
|
||||||
|
Width="100%"
|
||||||
|
Height="350px" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (chart.Type is ChartDefinitionType.PIE or ChartDefinitionType.DONUT)
|
||||||
|
{
|
||||||
|
<div class="chart-block-plot chart-block-plot-circular">
|
||||||
|
<MudChart ChartType="@this.ChartType"
|
||||||
|
InputData="@chart.Series[0].Values.ToArray()"
|
||||||
|
InputLabels="@chart.Categories.ToArray()"
|
||||||
|
ChartOptions="@this.ChartOptions"
|
||||||
|
Width="100%"
|
||||||
|
Height="200px" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="chart-block-plot chart-block-plot-axis">
|
||||||
|
<MudChart ChartType="@this.ChartType"
|
||||||
|
ChartSeries="@this.ChartSeries"
|
||||||
|
XAxisLabels="@chart.Categories.ToArray()"
|
||||||
|
ChartOptions="@this.CategoryChartOptions"
|
||||||
|
AxisChartOptions="@this.AxisChartOptions"
|
||||||
|
Width="100%"
|
||||||
|
Height="350px" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (chart.Caption is not null)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Align="Align.Center" Class="chart-block-caption d-block">
|
||||||
|
<em>@chart.Caption</em>
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Class="my-3">
|
||||||
|
@string.Format(T("This chart cannot be displayed: {0}"), this.Result.Error)
|
||||||
|
</MudAlert>
|
||||||
|
<pre class="overflow-auto"><code>@this.Result.RawJson</code></pre>
|
||||||
|
}
|
||||||
99
app/MindWork AI Studio/Chat/ChartBlock.razor.cs
Normal file
99
app/MindWork AI Studio/Chat/ChartBlock.razor.cs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
using AIStudio.Components;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
public partial class ChartBlock : MSGComponentBase
|
||||||
|
{
|
||||||
|
private AxisChartOptions AxisChartOptions { get; } = new() { MatchBoundsToSize = true };
|
||||||
|
|
||||||
|
private ChartOptions ChartOptions { get; } = new()
|
||||||
|
{
|
||||||
|
ChartPalette =
|
||||||
|
[
|
||||||
|
"#236A50", "#F2D264", "#79AE90", "#C97857", "#4E7894", "#9B6B8F",
|
||||||
|
"#6A233D", "#6484F2", "#AE7997", "#57A8C9", "#946A4E", "#6B9B77",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
private ChartOptions HeatMapChartOptions { get; } = new()
|
||||||
|
{
|
||||||
|
ChartPalette = ["#236A50", "#79AE90", "#F2D264", "#C97857", "#6A233D"],
|
||||||
|
EnableSmoothGradient = true,
|
||||||
|
YAxisLabelPosition = YAxisLabelPosition.Right,
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public ChartBlockParseResult Result { get; set; } = ChartBlockParseResult.Invalid(string.Empty, string.Empty);
|
||||||
|
|
||||||
|
private ChartType ChartType => this.Result.Chart?.Type switch
|
||||||
|
{
|
||||||
|
ChartDefinitionType.BAR => ChartType.Bar,
|
||||||
|
ChartDefinitionType.STACKED_BAR => ChartType.StackedBar,
|
||||||
|
ChartDefinitionType.LINE => ChartType.Line,
|
||||||
|
ChartDefinitionType.PIE => ChartType.Pie,
|
||||||
|
ChartDefinitionType.DONUT => ChartType.Donut,
|
||||||
|
ChartDefinitionType.HEATMAP => ChartType.HeatMap,
|
||||||
|
_ => ChartType.Bar,
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<ChartSeries> ChartSeries => this.Result.Chart?.Series
|
||||||
|
.Select(series => new ChartSeries { Name = series.Name, Data = series.Values.ToArray() })
|
||||||
|
.ToList() ?? [];
|
||||||
|
|
||||||
|
private ChartOptions CategoryChartOptions => this.Result.Chart?.Type is ChartDefinitionType.HEATMAP
|
||||||
|
? this.HeatMapChartOptions
|
||||||
|
: this.ChartOptions;
|
||||||
|
|
||||||
|
private List<TimeSeriesChartSeries> TimeSeriesChartSeries => this.Result.Chart is not { } chart
|
||||||
|
? []
|
||||||
|
: chart.Series
|
||||||
|
.Select(series => new TimeSeriesChartSeries
|
||||||
|
{
|
||||||
|
Name = series.Name,
|
||||||
|
Data = chart.Categories
|
||||||
|
.Select((category, index) => new TimeSeriesChartSeries.TimeValue(
|
||||||
|
DateTimeOffset.Parse(category, CultureInfo.InvariantCulture).UtcDateTime,
|
||||||
|
series.Values[index]))
|
||||||
|
.ToList(),
|
||||||
|
IsVisible = true,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private TimeSpan TimeLabelSpacing
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var timestamps = this.GetTimeSeriesTimestamps();
|
||||||
|
if (timestamps.Count < 2)
|
||||||
|
return TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
var range = timestamps[^1] - timestamps[0];
|
||||||
|
var intervalCount = Math.Min(timestamps.Count - 1, 8);
|
||||||
|
return TimeSpan.FromTicks(Math.Max(TimeSpan.TicksPerSecond, range.Ticks / intervalCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string TimeLabelFormat
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var timestamps = this.GetTimeSeriesTimestamps();
|
||||||
|
if (timestamps.Count < 2)
|
||||||
|
return "yyyy-MM-dd HH:mm";
|
||||||
|
|
||||||
|
var range = timestamps[^1] - timestamps[0];
|
||||||
|
if (range <= TimeSpan.FromDays(2))
|
||||||
|
return "MM-dd HH:mm";
|
||||||
|
|
||||||
|
return range <= TimeSpan.FromDays(730) ? "yyyy-MM-dd" : "yyyy";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DateTimeOffset> GetTimeSeriesTimestamps() => this.Result.Chart?.Categories
|
||||||
|
.Select(category => DateTimeOffset.Parse(category, CultureInfo.InvariantCulture).ToUniversalTime())
|
||||||
|
.ToList() ?? [];
|
||||||
|
}
|
||||||
55
app/MindWork AI Studio/Chat/ChartBlock.razor.css
Normal file
55
app/MindWork AI Studio/Chat/ChartBlock.razor.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
.chart-block-shell {
|
||||||
|
--chart-pine: #236a50;
|
||||||
|
--chart-sage: #79ae90;
|
||||||
|
--chart-sun: #f2d264;
|
||||||
|
--chart-mist: #eaf1ec;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-block-shell ::deep .chart-block {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: clamp(1rem, 2.5vw, 1.5rem);
|
||||||
|
border-color: color-mix(in srgb, var(--mud-palette-lines-default) 85%, var(--chart-pine));
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
background: var(--mud-palette-surface);
|
||||||
|
background: color-mix(in srgb, var(--mud-palette-surface) 96%, var(--chart-mist));
|
||||||
|
box-shadow: 0 18px 55px rgba(22, 75, 59, .07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-block-shell ::deep .chart-block::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
height: .25rem;
|
||||||
|
background: linear-gradient(90deg, var(--chart-pine), var(--chart-sage), var(--chart-sun));
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-block-shell ::deep .chart-block-title {
|
||||||
|
margin-block-end: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-block-plot {
|
||||||
|
width: 100%;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-block-plot-circular {
|
||||||
|
max-width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-block-plot-axis {
|
||||||
|
max-width: 56rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-block-plot-heatmap {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-block-shell ::deep .chart-block-caption {
|
||||||
|
margin-block-start: .75rem;
|
||||||
|
color: var(--mud-palette-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
315
app/MindWork AI Studio/Chat/ChartBlockParser.cs
Normal file
315
app/MindWork AI Studio/Chat/ChartBlockParser.cs
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses and validates versioned AI Studio chart blocks without executing their content.
|
||||||
|
/// </summary>
|
||||||
|
public static class ChartBlockParser
|
||||||
|
{
|
||||||
|
private const int MAX_JSON_BYTES = 32 * 1024;
|
||||||
|
private const int MAX_CATEGORIES = 50;
|
||||||
|
private const int MAX_SERIES = 10;
|
||||||
|
|
||||||
|
private static readonly HashSet<string> ROOT_PROPERTIES = ["schema_version", "type", "title", "caption", "data"];
|
||||||
|
private static readonly HashSet<string> DATA_PROPERTIES = ["categories", "series"];
|
||||||
|
private static readonly HashSet<string> SERIES_PROPERTIES = ["name", "values"];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a JSON chart definition and applies the complete local version 1 validation contract.
|
||||||
|
/// </summary>
|
||||||
|
public static ChartBlockParseResult Parse(string json)
|
||||||
|
{
|
||||||
|
if (Encoding.UTF8.GetByteCount(json) > MAX_JSON_BYTES)
|
||||||
|
return ChartBlockParseResult.Invalid(json, "The chart JSON exceeds the 32 KB limit.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(json, new JsonDocumentOptions
|
||||||
|
{
|
||||||
|
AllowTrailingCommas = false,
|
||||||
|
CommentHandling = JsonCommentHandling.Disallow,
|
||||||
|
MaxDepth = 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
var root = document.RootElement;
|
||||||
|
if (root.ValueKind is not JsonValueKind.Object)
|
||||||
|
return ChartBlockParseResult.Invalid(json, "The chart definition must be a JSON object.");
|
||||||
|
|
||||||
|
if (!HasOnlyKnownProperties(root, ROOT_PROPERTIES, out var propertyError))
|
||||||
|
return ChartBlockParseResult.Invalid(json, propertyError);
|
||||||
|
|
||||||
|
if (!TryGetRequiredInt(root, "schema_version", out var schemaVersion) || schemaVersion != 1)
|
||||||
|
return ChartBlockParseResult.Invalid(json, "schema_version must be 1.");
|
||||||
|
|
||||||
|
if (!TryGetRequiredString(root, "type", out var typeText)
|
||||||
|
|| !TryParseType(typeText, out var type))
|
||||||
|
return ChartBlockParseResult.Invalid(json, "type must be bar, stacked_bar, line, pie, donut, heatmap, or time_series.");
|
||||||
|
|
||||||
|
if (!TryGetRequiredString(root, "title", out var title) || string.IsNullOrWhiteSpace(title))
|
||||||
|
return ChartBlockParseResult.Invalid(json, "title must be a non-empty string.");
|
||||||
|
|
||||||
|
string? caption = null;
|
||||||
|
if (root.TryGetProperty("caption", out var captionElement))
|
||||||
|
{
|
||||||
|
if (captionElement.ValueKind is not JsonValueKind.String
|
||||||
|
|| string.IsNullOrWhiteSpace(captionElement.GetString()))
|
||||||
|
return ChartBlockParseResult.Invalid(json, "caption must be a non-empty string when provided.");
|
||||||
|
|
||||||
|
caption = captionElement.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.TryGetProperty("data", out var data) || data.ValueKind is not JsonValueKind.Object)
|
||||||
|
return ChartBlockParseResult.Invalid(json, "data must be a JSON object.");
|
||||||
|
|
||||||
|
if (!HasOnlyKnownProperties(data, DATA_PROPERTIES, out propertyError))
|
||||||
|
return ChartBlockParseResult.Invalid(json, propertyError);
|
||||||
|
|
||||||
|
if (!TryReadCategories(data, type, out var categories, out var error))
|
||||||
|
return ChartBlockParseResult.Invalid(json, error);
|
||||||
|
|
||||||
|
if (!TryReadSeries(data, categories.Count, out var series, out error))
|
||||||
|
return ChartBlockParseResult.Invalid(json, error);
|
||||||
|
|
||||||
|
if (type is ChartDefinitionType.PIE or ChartDefinitionType.DONUT)
|
||||||
|
{
|
||||||
|
if (series.Count != 1)
|
||||||
|
return ChartBlockParseResult.Invalid(json, "Pie and donut charts require exactly one series.");
|
||||||
|
|
||||||
|
if (series[0].Values.Any(value => value < 0))
|
||||||
|
return ChartBlockParseResult.Invalid(json, "Pie and donut chart values must not be negative.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChartBlockParseResult.Valid(json, new(schemaVersion, type, title, caption, categories, series));
|
||||||
|
}
|
||||||
|
catch (JsonException exception)
|
||||||
|
{
|
||||||
|
return ChartBlockParseResult.Invalid(json, $"The chart JSON is invalid: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadCategories(
|
||||||
|
JsonElement data,
|
||||||
|
ChartDefinitionType type,
|
||||||
|
out IReadOnlyList<string> categories,
|
||||||
|
out string error)
|
||||||
|
{
|
||||||
|
categories = [];
|
||||||
|
error = string.Empty;
|
||||||
|
if (!data.TryGetProperty("categories", out var element) || element.ValueKind is not JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
error = "data.categories must be an array.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new List<string>();
|
||||||
|
DateTimeOffset? previousTimestamp = null;
|
||||||
|
foreach (var item in element.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind is not JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetString()))
|
||||||
|
{
|
||||||
|
error = "Every category must be a non-empty string.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = item.GetString()!;
|
||||||
|
if (type is ChartDefinitionType.TIME_SERIES)
|
||||||
|
{
|
||||||
|
if (!HasExplicitTimeZone(value) || !item.TryGetDateTimeOffset(out var timestamp))
|
||||||
|
{
|
||||||
|
error = "Time series categories must be ISO 8601 timestamps with Z or an explicit UTC offset.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousTimestamp is not null && timestamp <= previousTimestamp.Value)
|
||||||
|
{
|
||||||
|
error = "Time series categories must be strictly increasing timestamps.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.Add(value);
|
||||||
|
if (values.Count > MAX_CATEGORIES)
|
||||||
|
{
|
||||||
|
error = $"A chart can contain at most {MAX_CATEGORIES} categories.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.Count == 0)
|
||||||
|
{
|
||||||
|
error = "A chart requires at least one category.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type is ChartDefinitionType.TIME_SERIES && values.Count < 2)
|
||||||
|
{
|
||||||
|
error = "A time series chart requires at least two timestamps.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = values;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasExplicitTimeZone(string value)
|
||||||
|
{
|
||||||
|
if (value.EndsWith('Z'))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (value.Length < 6)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var offsetSign = value[^6];
|
||||||
|
return offsetSign is '+' or '-'
|
||||||
|
&& char.IsAsciiDigit(value[^5])
|
||||||
|
&& char.IsAsciiDigit(value[^4])
|
||||||
|
&& value[^3] is ':'
|
||||||
|
&& char.IsAsciiDigit(value[^2])
|
||||||
|
&& char.IsAsciiDigit(value[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadSeries(JsonElement data, int categoryCount, out IReadOnlyList<ChartDefinitionSeries> series, out string error)
|
||||||
|
{
|
||||||
|
series = [];
|
||||||
|
error = string.Empty;
|
||||||
|
if (!data.TryGetProperty("series", out var element) || element.ValueKind is not JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
error = "data.series must be an array.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<ChartDefinitionSeries>();
|
||||||
|
foreach (var item in element.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind is not JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
error = "Every series must be a JSON object.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HasOnlyKnownProperties(item, SERIES_PROPERTIES, out error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!TryGetRequiredString(item, "name", out var name) || string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
error = "Every series requires a non-empty name.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.TryGetProperty("values", out var valuesElement) || valuesElement.ValueKind is not JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
error = "Every series requires a values array.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new List<double>();
|
||||||
|
foreach (var valueElement in valuesElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (valueElement.ValueKind is not JsonValueKind.Number
|
||||||
|
|| !valueElement.TryGetDouble(out var value)
|
||||||
|
|| !double.IsFinite(value))
|
||||||
|
{
|
||||||
|
error = "Series values must be finite JSON numbers.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.Count != categoryCount)
|
||||||
|
{
|
||||||
|
error = "Every series must contain exactly one value per category.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(new(name, values));
|
||||||
|
if (result.Count > MAX_SERIES)
|
||||||
|
{
|
||||||
|
error = $"A chart can contain at most {MAX_SERIES} series.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Count == 0)
|
||||||
|
{
|
||||||
|
error = "A chart requires at least one series.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
series = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasOnlyKnownProperties(JsonElement element, HashSet<string> knownProperties, out string error)
|
||||||
|
{
|
||||||
|
var seenProperties = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (var property in element.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (!knownProperties.Contains(property.Name))
|
||||||
|
{
|
||||||
|
error = $"Unknown chart property: {property.Name}.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!seenProperties.Add(property.Name))
|
||||||
|
{
|
||||||
|
error = $"Duplicate chart property: {property.Name}.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetRequiredInt(JsonElement element, string propertyName, out int value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return element.TryGetProperty(propertyName, out var property)
|
||||||
|
&& property.ValueKind is JsonValueKind.Number
|
||||||
|
&& property.TryGetInt32(out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetRequiredString(JsonElement element, string propertyName, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind is not JsonValueKind.String)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
value = property.GetString() ?? string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseType(string value, out ChartDefinitionType type)
|
||||||
|
{
|
||||||
|
type = value switch
|
||||||
|
{
|
||||||
|
"bar" => ChartDefinitionType.BAR,
|
||||||
|
"stacked_bar" => ChartDefinitionType.STACKED_BAR,
|
||||||
|
"line" => ChartDefinitionType.LINE,
|
||||||
|
"pie" => ChartDefinitionType.PIE,
|
||||||
|
"donut" => ChartDefinitionType.DONUT,
|
||||||
|
"heatmap" => ChartDefinitionType.HEATMAP,
|
||||||
|
"time_series" => ChartDefinitionType.TIME_SERIES,
|
||||||
|
_ => default,
|
||||||
|
};
|
||||||
|
|
||||||
|
return value is "bar" or "stacked_bar" or "line" or "pie" or "donut" or "heatmap" or "time_series";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The safe result of parsing a chart block, including the original JSON for fallback display.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ChartBlockParseResult(string RawJson, ChartDefinition? Chart, string Error)
|
||||||
|
{
|
||||||
|
public bool IsValid => this.Chart is not null;
|
||||||
|
|
||||||
|
public static ChartBlockParseResult Valid(string rawJson, ChartDefinition chart) => new(rawJson, chart, string.Empty);
|
||||||
|
|
||||||
|
public static ChartBlockParseResult Invalid(string rawJson, string error) => new(rawJson, null, error);
|
||||||
|
}
|
||||||
31
app/MindWork AI Studio/Chat/ChartDefinition.cs
Normal file
31
app/MindWork AI Studio/Chat/ChartDefinition.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A renderer-independent, validated chart definition produced by an AI response.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ChartDefinition(
|
||||||
|
int SchemaVersion,
|
||||||
|
ChartDefinitionType Type,
|
||||||
|
string Title,
|
||||||
|
string? Caption,
|
||||||
|
IReadOnlyList<string> Categories,
|
||||||
|
IReadOnlyList<ChartDefinitionSeries> Series);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A named series in a chart definition.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ChartDefinitionSeries(string Name, IReadOnlyList<double> Values);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chart types supported by version 1 of the AI Studio chart contract.
|
||||||
|
/// </summary>
|
||||||
|
public enum ChartDefinitionType
|
||||||
|
{
|
||||||
|
BAR,
|
||||||
|
STACKED_BAR,
|
||||||
|
LINE,
|
||||||
|
PIE,
|
||||||
|
DONUT,
|
||||||
|
HEATMAP,
|
||||||
|
TIME_SERIES,
|
||||||
|
}
|
||||||
@ -14,6 +14,36 @@ public sealed record ChatThread
|
|||||||
{
|
{
|
||||||
private static readonly ILogger<ChatThread> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ChatThread>();
|
private static readonly ILogger<ChatThread> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ChatThread>();
|
||||||
|
|
||||||
|
private const string CHART_OUTPUT_INSTRUCTIONS = """
|
||||||
|
When a chart is useful or explicitly requested, you may include one or more complete chart blocks in your normal response. Use exactly this fenced JSON format:
|
||||||
|
```aistudio-chart
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"type": "bar",
|
||||||
|
"title": "Chart title",
|
||||||
|
"caption": "Contextual interpretation of the chart",
|
||||||
|
"data": {
|
||||||
|
"categories": ["A", "B"],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "Series",
|
||||||
|
"values": [1, 2]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Supported types are `bar`, `stacked_bar`, `line`, `pie`, `donut`, `heatmap`, and `time_series`.
|
||||||
|
- Every series needs exactly one finite numeric value per category.
|
||||||
|
- Pie and donut charts need exactly one series and non-negative values.
|
||||||
|
- For heatmaps, categories are the columns, series names are the rows, and series values are the cells.
|
||||||
|
- Time series categories must contain at least two strictly increasing ISO 8601 timestamps with `Z` or an explicit UTC offset.
|
||||||
|
- Add a concise caption that correctly contextualizes the chart and may explain the chart's main finding.
|
||||||
|
- Use only the fields shown above.
|
||||||
|
- Keep other explanatory text outside the chart block.
|
||||||
|
""";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The unique identifier of the chat thread.
|
/// The unique identifier of the chat thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -56,6 +86,12 @@ public sealed record ChatThread
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IncludeDateTime { get; set; } = false;
|
public bool IncludeDateTime { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the model may emit locally rendered chart blocks.
|
||||||
|
/// False by default so internal structured model requests remain unchanged.
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowChartOutput { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The data source options for this chat thread.
|
/// The data source options for this chat thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -198,23 +234,32 @@ public sealed record ChatThread
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.LogInformation(logMessage);
|
LOGGER.LogInformation(logMessage);
|
||||||
if(!this.IncludeDateTime)
|
if(this.IncludeDateTime)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Prepend the current date and time to the system prompt:
|
||||||
|
//
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
var nowLocal = DateTime.Now;
|
||||||
|
var currentDateTime = string.Create(
|
||||||
|
new CultureInfo("en-US"),
|
||||||
|
$"Today is {nowUtc:dddd, MMMM d, yyyy h:mm tt} (UTC) and {nowLocal:dddd, MMMM d, yyyy h:mm tt} (local time)."
|
||||||
|
);
|
||||||
|
|
||||||
|
systemPromptText = $"""
|
||||||
|
{currentDateTime}
|
||||||
|
|
||||||
|
{systemPromptText}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.AllowChartOutput)
|
||||||
return systemPromptText;
|
return systemPromptText;
|
||||||
|
|
||||||
//
|
|
||||||
// Prepend the current date and time to the system prompt:
|
|
||||||
//
|
|
||||||
var nowUtc = DateTime.UtcNow;
|
|
||||||
var nowLocal = DateTime.Now;
|
|
||||||
var currentDateTime = string.Create(
|
|
||||||
new CultureInfo("en-US"),
|
|
||||||
$"Today is {nowUtc:dddd, MMMM d, yyyy h:mm tt} (UTC) and {nowLocal:dddd, MMMM d, yyyy h:mm tt} (local time)."
|
|
||||||
);
|
|
||||||
|
|
||||||
return $"""
|
return $"""
|
||||||
{currentDateTime}
|
|
||||||
|
|
||||||
{systemPromptText}
|
{systemPromptText}
|
||||||
|
|
||||||
|
{CHART_OUTPUT_INSTRUCTIONS}
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -105,10 +105,14 @@
|
|||||||
{
|
{
|
||||||
<MudMarkdown @key="@segment.RenderKey" Value="@segmentContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.CHAT_MARKDOWN_PIPELINE" />
|
<MudMarkdown @key="@segment.RenderKey" Value="@segmentContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.CHAT_MARKDOWN_PIPELINE" />
|
||||||
}
|
}
|
||||||
else
|
else if (segment.Type is MarkdownRenderSegmentType.MATH_BLOCK)
|
||||||
{
|
{
|
||||||
<MathJaxBlock @key="@segment.RenderKey" Value="@segmentContent" Class="mb-5" />
|
<MathJaxBlock @key="@segment.RenderKey" Value="@segmentContent" Class="mb-5" />
|
||||||
}
|
}
|
||||||
|
else if (segment.ChartResult is not null)
|
||||||
|
{
|
||||||
|
<ChartBlock @key="@segment.RenderKey" Result="@segment.ChartResult" />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@if (textContent.Sources.Count > 0)
|
@if (textContent.Sources.Count > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -17,6 +17,7 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
private const string HTML_SELF_CLOSING_TAG = "/>";
|
private const string HTML_SELF_CLOSING_TAG = "/>";
|
||||||
private const string CODE_FENCE_MARKER_BACKTICK = "```";
|
private const string CODE_FENCE_MARKER_BACKTICK = "```";
|
||||||
private const string CODE_FENCE_MARKER_TILDE = "~~~";
|
private const string CODE_FENCE_MARKER_TILDE = "~~~";
|
||||||
|
private const string CHART_CODE_FENCE_LANGUAGE = "aistudio-chart";
|
||||||
private const string MATH_BLOCK_MARKER_DOLLAR = "$$";
|
private const string MATH_BLOCK_MARKER_DOLLAR = "$$";
|
||||||
private const string MATH_BLOCK_MARKER_BRACKET_OPEN = """\[""";
|
private const string MATH_BLOCK_MARKER_BRACKET_OPEN = """\[""";
|
||||||
private const string MATH_BLOCK_MARKER_BRACKET_CLOSE = """\]""";
|
private const string MATH_BLOCK_MARKER_BRACKET_CLOSE = """\]""";
|
||||||
@ -336,6 +337,20 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
var trimmedLine = TrimWhitespace(normalizedSpan[lineStart..lineEnd]);
|
var trimmedLine = TrimWhitespace(normalizedSpan[lineStart..lineEnd]);
|
||||||
|
if (activeMathBlockFenceType is MathBlockFenceType.NONE
|
||||||
|
&& activeCodeFenceMarker == '\0'
|
||||||
|
&& TryGetChartFenceMarker(trimmedLine, out var chartFenceMarker)
|
||||||
|
&& TryFindClosingCodeFence(normalizedSpan, nextLineStart, chartFenceMarker, out var chartContentEnd, out var afterChartFence))
|
||||||
|
{
|
||||||
|
AddMarkdownSegment(markdownSegmentStart, lineStart);
|
||||||
|
var (start, end) = TrimLineBreaks(normalizedSpan, nextLineStart, chartContentEnd);
|
||||||
|
var chartJson = normalized.Substring(start, end - start);
|
||||||
|
segments.Add(new(MarkdownRenderSegmentType.CHART, start, end - start, ChartBlockParser.Parse(chartJson)));
|
||||||
|
markdownSegmentStart = afterChartFence;
|
||||||
|
lineStart = afterChartFence;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (activeMathBlockFenceType is MathBlockFenceType.NONE && TryUpdateCodeFenceState(trimmedLine, ref activeCodeFenceMarker))
|
if (activeMathBlockFenceType is MathBlockFenceType.NONE && TryUpdateCodeFenceState(trimmedLine, ref activeCodeFenceMarker))
|
||||||
{
|
{
|
||||||
lineStart = nextLineStart;
|
lineStart = nextLineStart;
|
||||||
@ -463,6 +478,48 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetChartFenceMarker(ReadOnlySpan<char> trimmedLine, out char fenceMarker)
|
||||||
|
{
|
||||||
|
fenceMarker = '\0';
|
||||||
|
if (trimmedLine.SequenceEqual($"{CODE_FENCE_MARKER_BACKTICK}{CHART_CODE_FENCE_LANGUAGE}".AsSpan()))
|
||||||
|
fenceMarker = '`';
|
||||||
|
else if (trimmedLine.SequenceEqual($"{CODE_FENCE_MARKER_TILDE}{CHART_CODE_FENCE_LANGUAGE}".AsSpan()))
|
||||||
|
fenceMarker = '~';
|
||||||
|
|
||||||
|
return fenceMarker != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFindClosingCodeFence(ReadOnlySpan<char> text, int searchStart, char fenceMarker, out int contentEnd, out int afterFence)
|
||||||
|
{
|
||||||
|
contentEnd = 0;
|
||||||
|
afterFence = 0;
|
||||||
|
var closingFence = fenceMarker == '`' ? CODE_FENCE_MARKER_BACKTICK : CODE_FENCE_MARKER_TILDE;
|
||||||
|
|
||||||
|
for (var lineStart = searchStart; lineStart < text.Length;)
|
||||||
|
{
|
||||||
|
var lineEnd = lineStart;
|
||||||
|
while (lineEnd < text.Length && text[lineEnd] is not '\r' and not '\n')
|
||||||
|
lineEnd++;
|
||||||
|
|
||||||
|
var nextLineStart = lineEnd;
|
||||||
|
if (nextLineStart < text.Length && text[nextLineStart] == '\r')
|
||||||
|
nextLineStart++;
|
||||||
|
if (nextLineStart < text.Length && text[nextLineStart] == '\n')
|
||||||
|
nextLineStart++;
|
||||||
|
|
||||||
|
if (TrimWhitespace(text[lineStart..lineEnd]).SequenceEqual(closingFence.AsSpan()))
|
||||||
|
{
|
||||||
|
contentEnd = lineStart;
|
||||||
|
afterFence = nextLineStart;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStart = nextLineStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static ReadOnlySpan<char> TrimWhitespace(ReadOnlySpan<char> text)
|
private static ReadOnlySpan<char> TrimWhitespace(ReadOnlySpan<char> text)
|
||||||
{
|
{
|
||||||
var start = 0;
|
var start = 0;
|
||||||
@ -492,6 +549,7 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
{
|
{
|
||||||
MARKDOWN,
|
MARKDOWN,
|
||||||
MATH_BLOCK,
|
MATH_BLOCK,
|
||||||
|
CHART,
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum MathBlockFenceType
|
private enum MathBlockFenceType
|
||||||
@ -506,7 +564,7 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
public static readonly MarkdownRenderPlan EMPTY = new(string.Empty, []);
|
public static readonly MarkdownRenderPlan EMPTY = new(string.Empty, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class MarkdownRenderSegment(MarkdownRenderSegmentType type, int start, int length)
|
private sealed class MarkdownRenderSegment(MarkdownRenderSegmentType type, int start, int length, ChartBlockParseResult? chartResult = null)
|
||||||
{
|
{
|
||||||
private string? cachedContent;
|
private string? cachedContent;
|
||||||
|
|
||||||
@ -516,6 +574,8 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
public int Length { get; } = length;
|
public int Length { get; } = length;
|
||||||
|
|
||||||
|
public ChartBlockParseResult? ChartResult { get; } = chartResult;
|
||||||
|
|
||||||
public int RenderKey { get; } = HashCode.Combine(type, start, length);
|
public int RenderKey { get; } = HashCode.Combine(type, start, length);
|
||||||
|
|
||||||
public string GetContent(string source)
|
public string GetContent(string source)
|
||||||
|
|||||||
@ -817,6 +817,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a user-visible chat request. Internal structured LLM requests keep the default disabled.
|
||||||
|
this.ChatThread.AllowChartOutput = true;
|
||||||
|
|
||||||
var time = DateTimeOffset.Now;
|
var time = DateTimeOffset.Now;
|
||||||
IContent? lastUserPrompt;
|
IContent? lastUserPrompt;
|
||||||
if (!reuseLastUserPrompt)
|
if (!reuseLastUserPrompt)
|
||||||
|
|||||||
@ -87,6 +87,40 @@ Each assistant plugin lives in its own directory under the assistants plugin roo
|
|||||||
- `LaunchBehavior = "OPEN_WORKSPACE_CHAT_BY_NAME"`
|
- `LaunchBehavior = "OPEN_WORKSPACE_CHAT_BY_NAME"`
|
||||||
- `WorkspaceName = "<target workspace name>"`
|
- `WorkspaceName = "<target workspace name>"`
|
||||||
- `UI.Type` is always `"FORM"` and `UI.Children` is a list of component tables.
|
- `UI.Type` is always `"FORM"` and `UI.Children` is a list of component tables.
|
||||||
|
|
||||||
|
### Chart output
|
||||||
|
|
||||||
|
When an assistant is expected to return a chart, include the following instructions in `ASSISTANT.SystemPrompt`. AI Studio validates and renders the block locally; Lua does not generate or render the chart itself.
|
||||||
|
|
||||||
|
````text
|
||||||
|
When a chart is useful or explicitly requested, you may include one or more complete chart blocks in your normal response. Use exactly this fenced JSON format:
|
||||||
|
```aistudio-chart
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"type": "bar",
|
||||||
|
"title": "Chart title",
|
||||||
|
"caption": "Contextual interpretation of the chart",
|
||||||
|
"data": {
|
||||||
|
"categories": ["A", "B"],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "Series",
|
||||||
|
"values": [1, 2]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Supported types are `bar`, `stacked_bar`, `line`, `pie`, `donut`, `heatmap`, and `time_series`.
|
||||||
|
- Every series needs exactly one finite numeric value per category.
|
||||||
|
- Pie and donut charts need exactly one series and non-negative values.
|
||||||
|
- For heatmaps, categories are the columns, series names are the rows, and series values are the cells.
|
||||||
|
- Time series categories must contain at least two strictly increasing ISO 8601 timestamps with `Z` or an explicit UTC offset.
|
||||||
|
- Add a concise caption that correctly contextualizes the chart and may explain the chart's main finding.
|
||||||
|
- Use only the fields shown above.
|
||||||
|
- Keep other explanatory text outside the chart block.
|
||||||
|
````
|
||||||
- Each component table declares `Type`, an optional `Children` array, and a `Props` table that feeds the component’s parameters.
|
- Each component table declares `Type`, an optional `Children` array, and a `Props` table that feeds the component’s parameters.
|
||||||
|
|
||||||
### Example: Minimal Requirements Assistant Table
|
### Example: Minimal Requirements Assistant Table
|
||||||
@ -96,7 +130,7 @@ DEPLOYED_USING_CONFIG_SERVER = false
|
|||||||
ASSISTANT = {
|
ASSISTANT = {
|
||||||
["Title"] = "",
|
["Title"] = "",
|
||||||
["Description"] = "",
|
["Description"] = "",
|
||||||
["SystemPrompt"] = "",
|
["SystemPrompt"] = "", -- request an aistudio-chart block here when the expected result is a chart
|
||||||
["SubmitText"] = "",
|
["SubmitText"] = "",
|
||||||
["AllowProfiles"] = true,
|
["AllowProfiles"] = true,
|
||||||
["LaunchBehavior"] = "OPEN_WORKSPACE_CHAT_BY_NAME",
|
["LaunchBehavior"] = "OPEN_WORKSPACE_CHAT_BY_NAME",
|
||||||
|
|||||||
@ -63,11 +63,41 @@ ASSISTANT = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--[[
|
||||||
|
When a chart is useful or explicitly requested, you may include one or more complete chart blocks in your normal response. Use exactly this fenced JSON format:
|
||||||
|
```aistudio-chart
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"type": "bar",
|
||||||
|
"title": "Chart title",
|
||||||
|
"caption": "Contextual interpretation of the chart",
|
||||||
|
"data": {
|
||||||
|
"categories": ["A", "B"],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "Series",
|
||||||
|
"values": [1, 2]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Supported types are `bar`, `stacked_bar`, `line`, `pie`, `donut`, `heatmap`, and `time_series`.
|
||||||
|
- Every series needs exactly one finite numeric value per category.
|
||||||
|
- Pie and donut charts need exactly one series and non-negative values.
|
||||||
|
- For heatmaps, categories are the columns, series names are the rows, and series values are the cells.
|
||||||
|
- Time series categories must contain at least two strictly increasing ISO 8601 timestamps with `Z` or an explicit UTC offset.
|
||||||
|
- Add a concise caption that correctly contextualizes the chart and may explain the chart's main finding.
|
||||||
|
- Use only the fields shown above.
|
||||||
|
- Keep other explanatory text outside the chart block.
|
||||||
|
]]
|
||||||
|
|
||||||
-- usage example with the full feature set:
|
-- usage example with the full feature set:
|
||||||
ASSISTANT = {
|
ASSISTANT = {
|
||||||
["Title"] = "<main title of assistant>", -- required
|
["Title"] = "<main title of assistant>", -- required
|
||||||
["Description"] = "<assistant description>", -- required
|
["Description"] = "<assistant description>", -- required
|
||||||
["SystemPrompt"] = "<prompt that fundamentally changes behaviour, personality and task focus of your assistant. Invisible to the user>", -- required
|
["SystemPrompt"] = "<prompt that fundamentally changes behaviour, personality and task focus of your assistant. If the expected result is a chart, include the chart-output instructions above. Invisible to the user>", -- required
|
||||||
["SubmitText"] = "<label for submit button>", -- required
|
["SubmitText"] = "<label for submit button>", -- required
|
||||||
["AllowProfiles"] = true, -- if true, allows AiStudios profiles; required
|
["AllowProfiles"] = true, -- if true, allows AiStudios profiles; required
|
||||||
["LaunchBehavior"] = "<NONE|OPEN_WORKSPACE_CHAT_BY_NAME>", -- optional; when set to OPEN_WORKSPACE_CHAT_BY_NAME the tile opens a chat directly
|
["LaunchBehavior"] = "<NONE|OPEN_WORKSPACE_CHAT_BY_NAME>", -- optional; when set to OPEN_WORKSPACE_CHAT_BY_NAME the tile opens a chat directly
|
||||||
|
|||||||
@ -2799,6 +2799,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTE
|
|||||||
-- The model response used an unsupported contract version. Please try again or select another model.
|
-- The model response used an unsupported contract version. Please try again or select another model.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T921285247"] = "Die Modellantwort verwendet eine nicht unterstützte Vertragsversion. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T921285247"] = "Die Modellantwort verwendet eine nicht unterstützte Vertragsversion. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- This chart cannot be displayed: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHARTBLOCK::T1070038198"] = "Dieses Diagramm kann nicht angezeigt werden: {0}"
|
||||||
|
|
||||||
-- System
|
-- System
|
||||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T2402387132"] = "System"
|
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T2402387132"] = "System"
|
||||||
|
|
||||||
|
|||||||
@ -2799,6 +2799,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTE
|
|||||||
-- The model response used an unsupported contract version. Please try again or select another model.
|
-- The model response used an unsupported contract version. Please try again or select another model.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T921285247"] = "The model response used an unsupported contract version. Please try again or select another model."
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T921285247"] = "The model response used an unsupported contract version. Please try again or select another model."
|
||||||
|
|
||||||
|
-- This chart cannot be displayed: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHARTBLOCK::T1070038198"] = "This chart cannot be displayed: {0}"
|
||||||
|
|
||||||
-- System
|
-- System
|
||||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T2402387132"] = "System"
|
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T2402387132"] = "System"
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user