♻️ Updated the thought rendering
This commit is contained in:
@@ -1302,7 +1302,8 @@
|
|||||||
"thoughtInputHint": "Ask sn-chan anything...",
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
"thoughtNewConversation": "Start New Conversation",
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
"thoughtParseError": "Failed to parse AI response",
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
"thoughtFunctionCall": "Function Call",
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "Tool {} respond",
|
||||||
"aiThought": "AI Thought",
|
"aiThought": "AI Thought",
|
||||||
"aiThoughtTitle": "Let sn-chan think",
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
"postReferenceUnavailable": "Referenced post is unavailable",
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
|||||||
@@ -2,46 +2,30 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:island/models/thought.dart';
|
|
||||||
|
|
||||||
class FunctionCallsSection extends StatefulWidget {
|
class FunctionCallsSection extends HookWidget {
|
||||||
const FunctionCallsSection({
|
const FunctionCallsSection({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.isFinish,
|
||||||
required this.isStreaming,
|
required this.isStreaming,
|
||||||
required this.streamingFunctionCalls,
|
required this.functionCallData,
|
||||||
this.thought,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final bool isFinish;
|
||||||
final bool isStreaming;
|
final bool isStreaming;
|
||||||
final List<String> streamingFunctionCalls;
|
final String? functionCallData;
|
||||||
final SnThinkingThought? thought;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FunctionCallsSection> createState() => _FunctionCallsSectionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FunctionCallsSectionState extends State<FunctionCallsSection> {
|
|
||||||
bool _isExpanded = false;
|
|
||||||
|
|
||||||
bool get _hasFunctionCalls {
|
|
||||||
if (widget.isStreaming) {
|
|
||||||
return widget.streamingFunctionCalls.isNotEmpty;
|
|
||||||
} else {
|
|
||||||
return widget.thought!.parts.isNotEmpty &&
|
|
||||||
widget.thought!.parts.any(
|
|
||||||
(part) => part.type == ThinkingMessagePartType.functionCall,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!_hasFunctionCalls) {
|
final isExpanded = useState(false);
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
var functionCallName =
|
||||||
|
jsonDecode(functionCallData ?? '{}')?['name'] as String?;
|
||||||
|
if (functionCallName?.isEmpty ?? true) functionCallName = 'unknown'.tr();
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -56,7 +40,7 @@ class _FunctionCallsSectionState extends State<FunctionCallsSection> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => setState(() => _isExpanded = !_isExpanded),
|
onTap: () => isExpanded.value = !isExpanded.value,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
@@ -67,7 +51,9 @@ class _FunctionCallsSectionState extends State<FunctionCallsSection> {
|
|||||||
const Gap(4),
|
const Gap(4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'thoughtFunctionCall'.tr(),
|
isFinish
|
||||||
|
? 'thoughtFunctionCallFinish'.tr(args: [])
|
||||||
|
: 'thoughtFunctionCallBegin'.tr(args: []),
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Theme.of(context).colorScheme.tertiary,
|
color: Theme.of(context).colorScheme.tertiary,
|
||||||
@@ -75,22 +61,22 @@ class _FunctionCallsSectionState extends State<FunctionCallsSection> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
_isExpanded ? Symbols.expand_more : Symbols.expand_less,
|
isExpanded.value
|
||||||
|
? Symbols.expand_more
|
||||||
|
: Symbols.expand_less,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Theme.of(context).colorScheme.tertiary,
|
color: Theme.of(context).colorScheme.tertiary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Visibility(visible: _isExpanded, child: const Gap(4)),
|
Visibility(visible: isExpanded.value, child: const Gap(4)),
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: _isExpanded,
|
visible: isExpanded.value,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.isStreaming) ...[
|
Container(
|
||||||
...widget.streamingFunctionCalls.map(
|
|
||||||
(call) => Container(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
margin: const EdgeInsets.only(bottom: 4),
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
@@ -105,7 +91,7 @@ class _FunctionCallsSectionState extends State<FunctionCallsSection> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SelectableText(
|
child: SelectableText(
|
||||||
call,
|
functionCallData!,
|
||||||
style: GoogleFonts.robotoMono(
|
style: GoogleFonts.robotoMono(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
@@ -113,43 +99,6 @@ class _FunctionCallsSectionState extends State<FunctionCallsSection> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
...widget.thought!.parts
|
|
||||||
.where(
|
|
||||||
(part) =>
|
|
||||||
part.type ==
|
|
||||||
ThinkingMessagePartType.functionCall,
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(part) => Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
margin: const EdgeInsets.only(bottom: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.outline.withOpacity(0.3),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SelectableText(
|
|
||||||
JsonEncoder.withIndent(
|
|
||||||
' ',
|
|
||||||
).convert(part.functionCall?.toJson() ?? {}),
|
|
||||||
style: GoogleFonts.robotoMono(
|
|
||||||
fontSize: 11,
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onSurface,
|
|
||||||
height: 1.3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ import 'package:island/widgets/thought/token_info.dart';
|
|||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
|
class StreamItem {
|
||||||
|
const StreamItem(this.type, this.data);
|
||||||
|
final String type;
|
||||||
|
final dynamic data;
|
||||||
|
}
|
||||||
|
|
||||||
class ThoughtChatState {
|
class ThoughtChatState {
|
||||||
final ValueNotifier<String?> sequenceId;
|
final ValueNotifier<String?> sequenceId;
|
||||||
final ValueNotifier<List<SnThinkingThought>> localThoughts;
|
final ValueNotifier<List<SnThinkingThought>> localThoughts;
|
||||||
@@ -28,8 +34,7 @@ class ThoughtChatState {
|
|||||||
final TextEditingController messageController;
|
final TextEditingController messageController;
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
final ValueNotifier<bool> isStreaming;
|
final ValueNotifier<bool> isStreaming;
|
||||||
final ValueNotifier<List<SnThinkingMessagePart>> streamingParts;
|
final ValueNotifier<List<StreamItem>> streamingItems;
|
||||||
final ValueNotifier<List<String>> reasoningChunks;
|
|
||||||
final ListController listController;
|
final ListController listController;
|
||||||
final ValueNotifier<ValueNotifier<double>> bottomGradientNotifier;
|
final ValueNotifier<ValueNotifier<double>> bottomGradientNotifier;
|
||||||
final Future<void> Function() sendMessage;
|
final Future<void> Function() sendMessage;
|
||||||
@@ -41,8 +46,7 @@ class ThoughtChatState {
|
|||||||
required this.messageController,
|
required this.messageController,
|
||||||
required this.scrollController,
|
required this.scrollController,
|
||||||
required this.isStreaming,
|
required this.isStreaming,
|
||||||
required this.streamingParts,
|
required this.streamingItems,
|
||||||
required this.reasoningChunks,
|
|
||||||
required this.listController,
|
required this.listController,
|
||||||
required this.bottomGradientNotifier,
|
required this.bottomGradientNotifier,
|
||||||
required this.sendMessage,
|
required this.sendMessage,
|
||||||
@@ -67,8 +71,7 @@ ThoughtChatState useThoughtChat(
|
|||||||
final messageController = useTextEditingController();
|
final messageController = useTextEditingController();
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final isStreaming = useState(false);
|
final isStreaming = useState(false);
|
||||||
final streamingParts = useState<List<SnThinkingMessagePart>>([]);
|
final streamingItems = useState<List<StreamItem>>([]);
|
||||||
final reasoningChunks = useState<List<String>>([]);
|
|
||||||
|
|
||||||
final listController = useMemoized(() => ListController(), []);
|
final listController = useMemoized(() => ListController(), []);
|
||||||
|
|
||||||
@@ -143,8 +146,7 @@ ThoughtChatState useThoughtChat(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
isStreaming.value = true;
|
isStreaming.value = true;
|
||||||
streamingParts.value = [];
|
streamingItems.value = [];
|
||||||
reasoningChunks.value = [];
|
|
||||||
|
|
||||||
final apiClient = ref.read(apiClientProvider);
|
final apiClient = ref.read(apiClientProvider);
|
||||||
final response = await apiClient.post(
|
final response = await apiClient.post(
|
||||||
@@ -177,42 +179,31 @@ ThoughtChatState useThoughtChat(
|
|||||||
final type = event['type'];
|
final type = event['type'];
|
||||||
final eventData = event['data'];
|
final eventData = event['data'];
|
||||||
if (type == 'text') {
|
if (type == 'text') {
|
||||||
if (streamingParts.value.isNotEmpty &&
|
streamingItems.value = [
|
||||||
streamingParts.value.last.type ==
|
...streamingItems.value,
|
||||||
ThinkingMessagePartType.text) {
|
StreamItem('text', eventData),
|
||||||
final last = streamingParts.value.last;
|
|
||||||
final newParts = [...streamingParts.value];
|
|
||||||
newParts[newParts.length - 1] = last.copyWith(
|
|
||||||
text: (last.text ?? '') + eventData,
|
|
||||||
);
|
|
||||||
streamingParts.value = newParts;
|
|
||||||
} else {
|
|
||||||
streamingParts.value = [
|
|
||||||
...streamingParts.value,
|
|
||||||
SnThinkingMessagePart(
|
|
||||||
type: ThinkingMessagePartType.text,
|
|
||||||
text: eventData,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
}
|
|
||||||
} else if (type == 'function_call') {
|
} else if (type == 'function_call') {
|
||||||
streamingParts.value = [
|
streamingItems.value = [
|
||||||
...streamingParts.value,
|
...streamingItems.value,
|
||||||
SnThinkingMessagePart(
|
StreamItem(
|
||||||
type: ThinkingMessagePartType.functionCall,
|
'function_call',
|
||||||
functionCall: SnFunctionCall.fromJson(eventData),
|
SnFunctionCall.fromJson(eventData),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
} else if (type == 'function_result') {
|
} else if (type == 'function_result') {
|
||||||
streamingParts.value = [
|
streamingItems.value = [
|
||||||
...streamingParts.value,
|
...streamingItems.value,
|
||||||
SnThinkingMessagePart(
|
StreamItem(
|
||||||
type: ThinkingMessagePartType.functionResult,
|
'function_result',
|
||||||
functionResult: SnFunctionResult.fromJson(eventData),
|
SnFunctionResult.fromJson(eventData),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
} else if (type == 'reasoning') {
|
} else if (type == 'reasoning') {
|
||||||
reasoningChunks.value = [...reasoningChunks.value, eventData];
|
streamingItems.value = [
|
||||||
|
...streamingItems.value,
|
||||||
|
StreamItem('reasoning', eventData),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
} else if (line.startsWith('topic: ')) {
|
} else if (line.startsWith('topic: ')) {
|
||||||
final jsonStr = line.substring(7);
|
final jsonStr = line.substring(7);
|
||||||
@@ -336,8 +327,7 @@ ThoughtChatState useThoughtChat(
|
|||||||
messageController: messageController,
|
messageController: messageController,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
isStreaming: isStreaming,
|
isStreaming: isStreaming,
|
||||||
streamingParts: streamingParts,
|
streamingItems: streamingItems,
|
||||||
reasoningChunks: reasoningChunks,
|
|
||||||
listController: listController,
|
listController: listController,
|
||||||
bottomGradientNotifier: bottomGradientNotifier,
|
bottomGradientNotifier: bottomGradientNotifier,
|
||||||
sendMessage: sendMessage,
|
sendMessage: sendMessage,
|
||||||
@@ -392,40 +382,16 @@ class ThoughtChatInterface extends HookConsumerWidget {
|
|||||||
(chatState.isStreaming.value ? 1 : 0),
|
(chatState.isStreaming.value ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (chatState.isStreaming.value && index == 0) {
|
if (chatState.isStreaming.value && index == 0) {
|
||||||
final streamingText = chatState.streamingParts.value
|
|
||||||
.where(
|
|
||||||
(p) => p.type == ThinkingMessagePartType.text,
|
|
||||||
)
|
|
||||||
.map((p) => p.text ?? '')
|
|
||||||
.join('');
|
|
||||||
final streamingFunctionCalls =
|
|
||||||
chatState.streamingParts.value
|
|
||||||
.where(
|
|
||||||
(p) =>
|
|
||||||
p.type ==
|
|
||||||
ThinkingMessagePartType.functionCall,
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(p) => JsonEncoder.withIndent(
|
|
||||||
' ',
|
|
||||||
).convert(p.functionCall?.toJson() ?? {}),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
return ThoughtItem(
|
return ThoughtItem(
|
||||||
isStreaming: true,
|
isStreaming: true,
|
||||||
streamingText: streamingText,
|
streamingItems: chatState.streamingItems.value,
|
||||||
reasoningChunks: chatState.reasoningChunks.value,
|
|
||||||
streamingFunctionCalls: streamingFunctionCalls,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final thoughtIndex =
|
final thoughtIndex =
|
||||||
chatState.isStreaming.value ? index - 1 : index;
|
chatState.isStreaming.value ? index - 1 : index;
|
||||||
final thought =
|
final thought =
|
||||||
chatState.localThoughts.value[thoughtIndex];
|
chatState.localThoughts.value[thoughtIndex];
|
||||||
return ThoughtItem(
|
return ThoughtItem(thought: thought);
|
||||||
thought: thought,
|
|
||||||
thoughtIndex: thoughtIndex,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -661,39 +627,21 @@ class ThoughtItem extends StatelessWidget {
|
|||||||
const ThoughtItem({
|
const ThoughtItem({
|
||||||
super.key,
|
super.key,
|
||||||
this.thought,
|
this.thought,
|
||||||
this.thoughtIndex,
|
|
||||||
this.isStreaming = false,
|
this.isStreaming = false,
|
||||||
this.streamingText = '',
|
this.streamingItems,
|
||||||
this.reasoningChunks = const [],
|
|
||||||
this.streamingFunctionCalls = const [],
|
|
||||||
}) : assert(
|
}) : assert(
|
||||||
(thought != null && !isStreaming) || (thought == null && isStreaming),
|
(streamingItems != null && isStreaming) ||
|
||||||
'Either thought or streaming parameters must be provided',
|
(thought != null && !isStreaming),
|
||||||
|
'Either streamingItems or thought must be provided',
|
||||||
);
|
);
|
||||||
|
|
||||||
final SnThinkingThought? thought;
|
final SnThinkingThought? thought;
|
||||||
final int? thoughtIndex;
|
|
||||||
final bool isStreaming;
|
final bool isStreaming;
|
||||||
final String streamingText;
|
final List<StreamItem>? streamingItems;
|
||||||
final List<String> reasoningChunks;
|
|
||||||
final List<String> streamingFunctionCalls;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isUser = !isStreaming && thought!.role == ThinkingThoughtRole.user;
|
final isUser = !isStreaming && thought!.role == ThinkingThoughtRole.user;
|
||||||
final isAI =
|
|
||||||
isStreaming ||
|
|
||||||
(!isStreaming && thought!.role == ThinkingThoughtRole.assistant);
|
|
||||||
|
|
||||||
final List<Map<String, String>> proposals =
|
|
||||||
!isStreaming
|
|
||||||
? _extractProposals(
|
|
||||||
thought!.parts
|
|
||||||
.where((p) => p.type == ThinkingMessagePartType.text)
|
|
||||||
.map((p) => p.text ?? '')
|
|
||||||
.join(''),
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
@@ -717,66 +665,134 @@ class ThoughtItem extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: buildWidgetsList(),
|
||||||
// Main content
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: ThoughtContent(
|
|
||||||
isStreaming: isStreaming,
|
|
||||||
streamingText: streamingText,
|
|
||||||
thought: thought,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (isStreaming && isAI)
|
|
||||||
SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2.5,
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// Reasoning chunks (streaming only)
|
|
||||||
if (reasoningChunks.isNotEmpty)
|
|
||||||
ReasoningSection(reasoningChunks: reasoningChunks),
|
|
||||||
|
|
||||||
// Function calls
|
|
||||||
if (streamingFunctionCalls.isNotEmpty ||
|
|
||||||
(thought?.parts.isNotEmpty ?? false) &&
|
|
||||||
thought!.parts.any(
|
|
||||||
(part) =>
|
|
||||||
part.type == ThinkingMessagePartType.functionCall,
|
|
||||||
))
|
|
||||||
FunctionCallsSection(
|
|
||||||
isStreaming: isStreaming,
|
|
||||||
streamingFunctionCalls: streamingFunctionCalls,
|
|
||||||
thought: thought,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Token count and model name (for completed AI thoughts only)
|
|
||||||
if (!isStreaming &&
|
|
||||||
isAI &&
|
|
||||||
thought != null &&
|
|
||||||
!thought!.id.startsWith('error-'))
|
|
||||||
TokenInfo(thought: thought!),
|
|
||||||
|
|
||||||
// Proposals (for completed AI thoughts only)
|
|
||||||
if (!isStreaming && proposals.isNotEmpty && isAI)
|
|
||||||
ProposalsSection(
|
|
||||||
proposals: proposals,
|
|
||||||
onProposalAction: _handleProposalAction,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Widget> buildWidgetsList() {
|
||||||
|
final List<StreamItem> items =
|
||||||
|
isStreaming
|
||||||
|
? (streamingItems ?? [])
|
||||||
|
: thought!.parts.map((p) {
|
||||||
|
String type;
|
||||||
|
switch (p.type) {
|
||||||
|
case ThinkingMessagePartType.text:
|
||||||
|
type = 'text';
|
||||||
|
break;
|
||||||
|
case ThinkingMessagePartType.functionCall:
|
||||||
|
type = 'function_call';
|
||||||
|
break;
|
||||||
|
case ThinkingMessagePartType.functionResult:
|
||||||
|
type = 'function_result';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return StreamItem(
|
||||||
|
type,
|
||||||
|
p.type == ThinkingMessagePartType.text
|
||||||
|
? p.text ?? ''
|
||||||
|
: p.functionCall ?? p.functionResult,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final isAI =
|
||||||
|
isStreaming ||
|
||||||
|
(!isStreaming && thought!.role == ThinkingThoughtRole.assistant);
|
||||||
|
final List<Map<String, String>> proposals =
|
||||||
|
!isStreaming
|
||||||
|
? _extractProposals(
|
||||||
|
thought!.parts
|
||||||
|
.where((p) => p.type == ThinkingMessagePartType.text)
|
||||||
|
.map((p) => p.text ?? '')
|
||||||
|
.join(),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
final List<Widget> widgets = [];
|
||||||
|
String currentText = '';
|
||||||
|
bool hasOpenText = false;
|
||||||
|
for (int i = 0; i < items.length; i++) {
|
||||||
|
final item = items[i];
|
||||||
|
if (item.type == 'text') {
|
||||||
|
currentText += item.data as String;
|
||||||
|
hasOpenText = true;
|
||||||
|
} else {
|
||||||
|
if (hasOpenText) {
|
||||||
|
bool isLastTextBlock =
|
||||||
|
!items.sublist(i).any((it) => it.type == 'text');
|
||||||
|
widgets.add(buildTextRow(currentText, isLastTextBlock));
|
||||||
|
currentText = '';
|
||||||
|
hasOpenText = false;
|
||||||
|
}
|
||||||
|
widgets.add(buildItemWidget(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasOpenText) {
|
||||||
|
widgets.add(buildTextRow(currentText, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The proposals and token info at the end
|
||||||
|
if (!isStreaming && proposals.isNotEmpty && isAI) {
|
||||||
|
widgets.add(
|
||||||
|
ProposalsSection(
|
||||||
|
proposals: proposals,
|
||||||
|
onProposalAction: _handleProposalAction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isStreaming &&
|
||||||
|
isAI &&
|
||||||
|
thought != null &&
|
||||||
|
!thought!.id.startsWith('error-')) {
|
||||||
|
widgets.add(TokenInfo(thought: thought!));
|
||||||
|
}
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
Row buildTextRow(String text, bool hasSpinner) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: ThoughtContent(
|
||||||
|
isStreaming: isStreaming && hasSpinner,
|
||||||
|
streamingText: text,
|
||||||
|
thought: thought,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isStreaming && hasSpinner)
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
padding: EdgeInsets.all(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildItemWidget(StreamItem item) {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'reasoning':
|
||||||
|
return ReasoningSection(reasoningChunks: [item.data]);
|
||||||
|
case 'function_call':
|
||||||
|
case 'function_result':
|
||||||
|
final jsonStr = JsonEncoder.withIndent(
|
||||||
|
' ',
|
||||||
|
).convert(item.data.toJson());
|
||||||
|
return FunctionCallsSection(
|
||||||
|
isFinish: item.type == 'function_result',
|
||||||
|
isStreaming: isStreaming,
|
||||||
|
functionCallData: jsonStr,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw 'unknown item type ${item.type}';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user