♻️ Refactor the thought insight to support new API

This commit is contained in:
2025-11-15 16:59:22 +08:00
parent ea8e7ead2d
commit 645a6dca93
8 changed files with 1397 additions and 124 deletions

View File

@@ -30,9 +30,9 @@ class _FunctionCallsSectionState extends State<FunctionCallsSection> {
if (widget.isStreaming) {
return widget.streamingFunctionCalls.isNotEmpty;
} else {
return widget.thought!.chunks.isNotEmpty &&
widget.thought!.chunks.any(
(chunk) => chunk.type == ThinkingChunkType.functionCall,
return widget.thought!.parts.isNotEmpty &&
widget.thought!.parts.any(
(part) => part.type == ThinkingMessagePartType.functionCall,
);
}
}
@@ -115,13 +115,14 @@ class _FunctionCallsSectionState extends State<FunctionCallsSection> {
),
),
] else ...[
...widget.thought!.chunks
...widget.thought!.parts
.where(
(chunk) =>
chunk.type == ThinkingChunkType.functionCall,
(part) =>
part.type ==
ThinkingMessagePartType.functionCall,
)
.map(
(chunk) => Container(
(part) => Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.only(bottom: 4),
@@ -138,7 +139,7 @@ class _FunctionCallsSectionState extends State<FunctionCallsSection> {
child: SelectableText(
JsonEncoder.withIndent(
' ',
).convert(chunk.data),
).convert(part.functionCall?.toJson() ?? {}),
style: GoogleFonts.robotoMono(
fontSize: 11,
color:

View File

@@ -15,29 +15,62 @@ class ThoughtContent extends StatelessWidget {
final String streamingText;
final SnThinkingThought? thought;
bool get _isErrorMessage {
if (thought == null) return false;
// Check if this is an error thought by ID or content
if (thought!.id.startsWith('error-')) return true;
final textParts = thought!.parts
.where((p) => p.type == ThinkingMessagePartType.text)
.map((p) => p.text ?? '')
.join('');
return textParts.startsWith('Error:');
}
@override
Widget build(BuildContext context) {
if (isStreaming) {
// Streaming text with spinner
if (streamingText.isNotEmpty) {
final isStreamingError = streamingText.startsWith('Error:');
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: MarkdownTextContent(
isSelectable: true,
content: streamingText,
extraBlockSyntaxList: [ProposalBlockSyntax()],
textStyle: Theme.of(context).textTheme.bodyMedium,
extraGenerators: [
ProposalGenerator(
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onSecondaryContainer,
borderColor: Theme.of(context).colorScheme.outline,
child: Container(
padding:
isStreamingError
? const EdgeInsets.all(8)
: EdgeInsets.zero,
decoration:
isStreamingError
? BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.error,
width: 1,
),
borderRadius: BorderRadius.circular(8),
)
: null,
child: MarkdownTextContent(
isSelectable: true,
content: streamingText,
extraBlockSyntaxList: [ProposalBlockSyntax()],
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
color:
isStreamingError
? Theme.of(context).colorScheme.error
: null,
),
],
extraGenerators: [
ProposalGenerator(
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onSecondaryContainer,
borderColor: Theme.of(context).colorScheme.outline,
),
],
),
),
),
const SizedBox(width: 8),
@@ -56,22 +89,53 @@ class ThoughtContent extends StatelessWidget {
}
return const SizedBox.shrink();
} else {
// Regular thought content
if (thought!.content != null && thought!.content!.isNotEmpty) {
return MarkdownTextContent(
isSelectable: true,
content: thought!.content!,
extraBlockSyntaxList: [ProposalBlockSyntax()],
textStyle: Theme.of(context).textTheme.bodyMedium,
extraGenerators: [
ProposalGenerator(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onSecondaryContainer,
borderColor: Theme.of(context).colorScheme.outline,
// Regular thought content - render parts
if (thought!.parts.isNotEmpty) {
final textParts = thought!.parts
.where((p) => p.type == ThinkingMessagePartType.text)
.map((p) => p.text ?? '')
.join('');
if (textParts.isNotEmpty) {
return Container(
padding:
_isErrorMessage
? const EdgeInsets.symmetric(horizontal: 12, vertical: 4)
: EdgeInsets.zero,
decoration:
_isErrorMessage
? BoxDecoration(
color: Theme.of(
context,
).colorScheme.error.withOpacity(0.1),
border: Border.all(
color: Theme.of(context).colorScheme.error,
width: 1,
),
borderRadius: BorderRadius.circular(8),
)
: null,
child: MarkdownTextContent(
isSelectable: true,
content: textParts,
extraBlockSyntaxList: [ProposalBlockSyntax()],
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
color:
_isErrorMessage
? Theme.of(context).colorScheme.error
: null,
),
extraGenerators: [
ProposalGenerator(
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onSecondaryContainer,
borderColor: Theme.of(context).colorScheme.outline,
),
],
),
],
);
);
}
}
return const SizedBox.shrink();
}

View File

@@ -211,8 +211,13 @@ class ThoughtItem extends StatelessWidget {
(!isStreaming && thought!.role == ThinkingThoughtRole.assistant);
final List<Map<String, String>> proposals =
!isStreaming && thought!.content != null
? _extractProposals(thought!.content!)
!isStreaming
? _extractProposals(
thought!.parts
.where((p) => p.type == ThinkingMessagePartType.text)
.map((p) => p.text ?? '')
.join(''),
)
: [];
return Container(
@@ -251,10 +256,10 @@ class ThoughtItem extends StatelessWidget {
// Function calls
if (streamingFunctionCalls.isNotEmpty ||
(thought?.chunks.isNotEmpty ?? false) &&
thought!.chunks.any(
(chunk) =>
chunk.type == ThinkingChunkType.functionCall,
(thought?.parts.isNotEmpty ?? false) &&
thought!.parts.any(
(part) =>
part.type == ThinkingMessagePartType.functionCall,
))
FunctionCallsSection(
isStreaming: isStreaming,