mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-09 09:09:39 +00:00
Add per-answer thinking disclosure
This commit is contained in:
1 parent
c7b42bee96
commit
bbbcdddc91
29 files changed
+360
-37
No files matched your search
@@ -31,6 +31,23 @@
|
||||
</MudButton>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.HasThinking)
|
||||
{
|
||||
<MudTooltip Text="@this.GetThinkingTooltip()" Placement="Placement.Bottom">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Default"
|
||||
Size="Size.Small"
|
||||
Class="px-2 py-1 rounded-pill"
|
||||
Style="min-width:auto; border-width:1px; text-transform:none;"
|
||||
OnClick="@this.ToggleThinking">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Psychology" Color="Color.Default" Size="Size.Small" />
|
||||
<MudText Typo="Typo.body2">@T("Thinking")</MudText>
|
||||
<MudIcon Icon="@(this.showThinking ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)" Size="Size.Small" />
|
||||
</MudStack>
|
||||
</MudButton>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
@@ -193,6 +210,14 @@
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
@if (this.HasThinking && this.showThinking)
|
||||
{
|
||||
<MudPaper Class="pa-3 mb-3 border rounded-lg" Style="border-width:1px;">
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">@T("Thinking")</MudText>
|
||||
<MudText Typo="Typo.body2" Style="white-space: pre-wrap; overflow-wrap: anywhere;">@textContent.Thinking</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
if (textContent.InitialRemoteWait)
|
||||
{
|
||||
<MudSkeleton Width="30%" Height="42px;"/>
|
||||
|
||||
@@ -127,6 +127,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
private bool hasActiveMathContainer;
|
||||
private bool isDisposed;
|
||||
private bool showToolTrace;
|
||||
private bool showThinking;
|
||||
private readonly HashSet<int> expandedToolInvocations = [];
|
||||
|
||||
/// <summary>
|
||||
@@ -139,6 +140,10 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
/// </remarks>
|
||||
private bool CanExport => this.Content is { InitialRemoteWait: false, IsStreaming: false } && this.Content.TryGetMarkdownText(out _);
|
||||
|
||||
private bool HasThinking => this.Role is ChatRole.AI &&
|
||||
this.Content is ContentText { Thinking: var thinking } &&
|
||||
!string.IsNullOrWhiteSpace(thinking);
|
||||
|
||||
/// <summary>
|
||||
/// The tables this block holds so that the export menu can offer each of them.
|
||||
/// </summary>
|
||||
@@ -305,11 +310,14 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
var textValue = text.Text;
|
||||
hash.Add(textValue.Length);
|
||||
hash.Add(textValue.GetHashCode(StringComparison.Ordinal));
|
||||
hash.Add(text.Thinking.Length);
|
||||
hash.Add(text.Thinking.GetHashCode(StringComparison.Ordinal));
|
||||
hash.Add(text.Sources.Count);
|
||||
hash.Add(text.ToolInvocations.Count);
|
||||
hash.Add(text.ToolRuntimeStatus.IsRunning);
|
||||
hash.Add(text.ToolRuntimeStatus.Message);
|
||||
hash.Add(this.showToolTrace);
|
||||
hash.Add(this.showThinking);
|
||||
hash.Add(this.expandedToolInvocations.Count);
|
||||
foreach (var expandedInvocation in this.expandedToolInvocations.Order())
|
||||
hash.Add(expandedInvocation);
|
||||
@@ -380,6 +388,10 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
private void ToggleToolTrace() => this.showToolTrace = !this.showToolTrace;
|
||||
|
||||
private void ToggleThinking() => this.showThinking = !this.showThinking;
|
||||
|
||||
private string GetThinkingTooltip() => this.showThinking ? this.T("Hide thinking") : this.T("Show thinking");
|
||||
|
||||
private bool IsToolInvocationExpanded(int order) => this.expandedToolInvocations.Contains(order);
|
||||
|
||||
private void ToggleToolInvocation(int order)
|
||||
@@ -832,4 +844,4 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
await this.DisposeMathContainerIfNeededAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@ namespace AIStudio.Chat;
|
||||
/// </summary>
|
||||
public sealed class ContentText : IContent
|
||||
{
|
||||
private const string OPEN_THINK_TAG = "<think>";
|
||||
private const string CLOSE_THINK_TAG = "</think>";
|
||||
|
||||
private static readonly ILogger<ContentText> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ContentText>();
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ContentText).Namespace, nameof(ContentText));
|
||||
@@ -26,6 +29,12 @@ public sealed class ContentText : IContent
|
||||
/// </summary>
|
||||
private static readonly TimeSpan MIN_TIME = TimeSpan.FromSeconds(3);
|
||||
|
||||
[JsonIgnore]
|
||||
private ThinkTagStreamState thinkTagStreamState;
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly StringBuilder thinkTagBuffer = new();
|
||||
|
||||
#region Implementation of IContent
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -122,11 +131,8 @@ public sealed class ContentText : IContent
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = true;
|
||||
|
||||
// Add the response to the text:
|
||||
this.Text += contentStreamChunk;
|
||||
|
||||
// Merge the sources:
|
||||
this.Sources.MergeSources(contentStreamChunk.Sources);
|
||||
// Add the response to the content:
|
||||
this.ApplyStreamChunk(contentStreamChunk);
|
||||
|
||||
// Notify the UI that the content has changed,
|
||||
// depending on the energy saving mode:
|
||||
@@ -160,7 +166,7 @@ public sealed class ContentText : IContent
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.Text = this.Text.RemoveThinkTags().Trim();
|
||||
this.FinalizeStreamContent();
|
||||
|
||||
// Inform the UI that the streaming is done:
|
||||
await this.StreamingDone();
|
||||
@@ -253,6 +259,7 @@ public sealed class ContentText : IContent
|
||||
public IContent DeepClone() => new ContentText
|
||||
{
|
||||
Text = this.Text,
|
||||
Thinking = this.Thinking,
|
||||
InitialRemoteWait = this.InitialRemoteWait,
|
||||
IsStreaming = this.IsStreaming,
|
||||
Sources = [..this.Sources],
|
||||
@@ -407,4 +414,123 @@ public sealed class ContentText : IContent
|
||||
/// The text content.
|
||||
/// </summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable thinking content exposed by the provider.
|
||||
/// </summary>
|
||||
public string Thinking { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Applies one provider stream chunk to this content.
|
||||
/// </summary>
|
||||
public void ApplyStreamChunk(ContentStreamChunk chunk)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(chunk.Thinking))
|
||||
this.Thinking += chunk.Thinking;
|
||||
|
||||
if (!string.IsNullOrEmpty(chunk.Content))
|
||||
this.ApplyAnswerChunk(chunk.Content);
|
||||
|
||||
this.Sources.MergeSources(chunk.Sources);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes parsing of provider content and normalizes the displayed values.
|
||||
/// </summary>
|
||||
public void FinalizeStreamContent()
|
||||
{
|
||||
switch (this.thinkTagStreamState)
|
||||
{
|
||||
case ThinkTagStreamState.UNDECIDED:
|
||||
this.Text += this.thinkTagBuffer;
|
||||
break;
|
||||
|
||||
case ThinkTagStreamState.THINKING:
|
||||
this.Thinking += this.thinkTagBuffer;
|
||||
break;
|
||||
}
|
||||
|
||||
this.thinkTagBuffer.Clear();
|
||||
this.thinkTagStreamState = ThinkTagStreamState.ANSWER;
|
||||
this.Text = this.Text.Trim();
|
||||
this.Thinking = this.Thinking.Trim();
|
||||
}
|
||||
|
||||
private void ApplyAnswerChunk(string content)
|
||||
{
|
||||
if (this.thinkTagStreamState is ThinkTagStreamState.UNDECIDED && this.Text.Length > 0)
|
||||
this.thinkTagStreamState = ThinkTagStreamState.ANSWER;
|
||||
|
||||
switch (this.thinkTagStreamState)
|
||||
{
|
||||
case ThinkTagStreamState.UNDECIDED:
|
||||
this.thinkTagBuffer.Append(content);
|
||||
var undecidedContent = this.thinkTagBuffer.ToString();
|
||||
if (undecidedContent.Length < OPEN_THINK_TAG.Length &&
|
||||
OPEN_THINK_TAG.StartsWith(undecidedContent, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
if (!undecidedContent.StartsWith(OPEN_THINK_TAG, StringComparison.Ordinal))
|
||||
{
|
||||
this.Text += undecidedContent;
|
||||
this.thinkTagBuffer.Clear();
|
||||
this.thinkTagStreamState = ThinkTagStreamState.ANSWER;
|
||||
return;
|
||||
}
|
||||
|
||||
this.thinkTagBuffer.Clear();
|
||||
this.thinkTagStreamState = ThinkTagStreamState.THINKING;
|
||||
this.ApplyThinkingTagContent(undecidedContent[OPEN_THINK_TAG.Length..]);
|
||||
return;
|
||||
|
||||
case ThinkTagStreamState.THINKING:
|
||||
this.ApplyThinkingTagContent(content);
|
||||
return;
|
||||
|
||||
case ThinkTagStreamState.ANSWER:
|
||||
this.Text += content;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyThinkingTagContent(string content)
|
||||
{
|
||||
this.thinkTagBuffer.Append(content);
|
||||
var thinkingContent = this.thinkTagBuffer.ToString();
|
||||
var closeTagIndex = thinkingContent.IndexOf(CLOSE_THINK_TAG, StringComparison.Ordinal);
|
||||
if (closeTagIndex >= 0)
|
||||
{
|
||||
this.Thinking += thinkingContent[..closeTagIndex];
|
||||
this.Text += thinkingContent[(closeTagIndex + CLOSE_THINK_TAG.Length)..];
|
||||
this.thinkTagBuffer.Clear();
|
||||
this.thinkTagStreamState = ThinkTagStreamState.ANSWER;
|
||||
return;
|
||||
}
|
||||
|
||||
var pendingLength = GetMarkerPrefixSuffixLength(thinkingContent, CLOSE_THINK_TAG);
|
||||
var completedLength = thinkingContent.Length - pendingLength;
|
||||
if (completedLength <= 0)
|
||||
return;
|
||||
|
||||
this.Thinking += thinkingContent[..completedLength];
|
||||
this.thinkTagBuffer.Clear();
|
||||
this.thinkTagBuffer.Append(thinkingContent.AsSpan(completedLength));
|
||||
}
|
||||
|
||||
private static int GetMarkerPrefixSuffixLength(string content, string marker)
|
||||
{
|
||||
var maximumLength = Math.Min(content.Length, marker.Length - 1);
|
||||
for (var length = maximumLength; length > 0; length--)
|
||||
if (content.AsSpan(content.Length - length).SequenceEqual(marker.AsSpan(0, length)))
|
||||
return length;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private enum ThinkTagStreamState
|
||||
{
|
||||
UNDECIDED,
|
||||
THINKING,
|
||||
ANSWER,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user