| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629 |
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:go_router/go_router.dart';
- import 'package:provider/provider.dart';
- import 'package:flutter_lucide/flutter_lucide.dart';
- import '../widgets/layout/app_layout.dart';
- import '../widgets/common/app_button.dart';
- import '../providers/document_provider.dart';
- import '../providers/element_provider.dart';
- import '../models/element.dart';
- import '../services/mock_data_service.dart';
- import '../theme/app_colors.dart';
- /// AI结果页
- class ResultPage extends StatefulWidget {
- final String documentId;
- const ResultPage({Key? key, required this.documentId}) : super(key: key);
- @override
- State<ResultPage> createState() => _ResultPageState();
- }
- class _ResultPageState extends State<ResultPage> {
- final prompt = MockDataService.getMockPrompt();
- final response = MockDataService.getMockAIResponse();
- bool _isRegenerating = false;
- bool _isSending = false;
- late final TextEditingController _inputController;
- @override
- void initState() {
- super.initState();
- _inputController = TextEditingController();
- }
- @override
- void dispose() {
- _inputController.dispose();
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- return Consumer2<DocumentProvider, ElementProvider>(
- builder: (context, docProvider, elementProvider, child) {
- final document = docProvider.getDocumentById(widget.documentId);
- var documentElements = elementProvider.elements
- .where(
- (element) =>
- element.documentId == widget.documentId ||
- element.documentId == null,
- )
- .toList();
- if (documentElements.isEmpty) {
- documentElements = elementProvider.elements.take(10).toList();
- }
- return AppLayout(
- maxContentWidth: 1200,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _buildHeader(context, document),
- const SizedBox(height: 24),
- _buildChatInterface(context),
- const SizedBox(height: 32),
- _buildActionComposer(context, documentElements),
- ],
- ),
- );
- },
- );
- }
- Widget _buildHeader(BuildContext context, document) {
- return Container(
- padding: const EdgeInsets.all(20),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(16),
- border: Border.all(color: AppColors.border),
- boxShadow: [
- BoxShadow(
- color: Colors.black.withOpacity(0.04),
- blurRadius: 12,
- offset: const Offset(0, 4),
- ),
- ],
- ),
- child: Row(
- children: [
- IconButton(
- icon: const Icon(Icons.arrow_back),
- onPressed: () => context.pop(),
- style: IconButton.styleFrom(
- backgroundColor: AppColors.backgroundLight,
- padding: const EdgeInsets.all(12),
- ),
- ),
- const SizedBox(width: 16),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- gradient: LinearGradient(
- colors: [AppColors.success, Colors.green.shade300],
- ),
- borderRadius: BorderRadius.circular(8),
- ),
- child: const Icon(
- LucideIcons.circle_check,
- color: Colors.white,
- size: 18,
- ),
- ),
- const SizedBox(width: 12),
- const Text(
- 'AI处理结果',
- style: TextStyle(
- fontSize: 22,
- fontWeight: FontWeight.bold,
- color: AppColors.textPrimary,
- ),
- ),
- ],
- ),
- const SizedBox(height: 8),
- Text(
- '基于人工牵引生成的精准Prompt与AI回复',
- style: TextStyle(
- fontSize: 14,
- color: AppColors.textSecondary,
- ),
- ),
- ],
- ),
- ),
- AppButton(
- text: '导出',
- type: ButtonType.secondary,
- size: ButtonSize.small,
- icon: LucideIcons.download,
- onPressed: () {},
- ),
- ],
- ),
- );
- }
- Widget _buildChatInterface(BuildContext context) {
- return Expanded(
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
- decoration: BoxDecoration(
- color: const Color(0xFFF7F8FA),
- borderRadius: BorderRadius.circular(18),
- border: Border.all(color: AppColors.borderLight),
- boxShadow: [
- BoxShadow(
- color: Colors.black.withOpacity(0.02),
- blurRadius: 18,
- offset: const Offset(0, 8),
- ),
- ],
- ),
- child: SingleChildScrollView( // 改为可滚动
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- _buildChatMessage(
- context,
- isAI: false,
- title: '系统生成的精准 Prompt',
- content: prompt,
- icon: LucideIcons.sparkles,
- ),
- const SizedBox(height: 28),
- _buildChatMessage(
- context,
- isAI: true,
- title: 'AI 回复',
- content: response,
- icon: LucideIcons.bot,
- ),
- // 可以继续添加更多消息
- ],
- ),
- ),
- ),
- );
- }
- Widget _buildChatMessage(
- BuildContext context, {
- required bool isAI,
- required String title,
- required String content,
- required IconData icon,
- }) {
- final theme = Theme.of(context);
- final bubbleColor =
- isAI ? Colors.white : AppColors.primary.withOpacity(0.08);
- final borderColor =
- isAI ? AppColors.borderLight : AppColors.primary.withOpacity(0.25);
- final bubble = Container(
- constraints: const BoxConstraints(maxWidth: 760),
- padding: const EdgeInsets.all(20),
- decoration: BoxDecoration(
- color: bubbleColor,
- borderRadius: BorderRadius.only(
- topLeft: const Radius.circular(20),
- topRight: const Radius.circular(20),
- bottomLeft: Radius.circular(isAI ? 20 : 6),
- bottomRight: Radius.circular(isAI ? 6 : 20),
- ),
- border: Border.all(color: borderColor),
- boxShadow: [
- BoxShadow(
- color: Colors.black.withOpacity(0.04),
- blurRadius: 12,
- offset: const Offset(0, 6),
- ),
- ],
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Icon(
- icon,
- size: 18,
- color: isAI ? AppColors.success : AppColors.primary,
- ),
- const SizedBox(width: 8),
- Text(
- title,
- style: theme.textTheme.titleSmall?.copyWith(
- fontWeight: FontWeight.w600,
- color: AppColors.textPrimary,
- ),
- ),
- if (!isAI) ...[
- const SizedBox(width: 8),
- Container(
- padding:
- const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- decoration: BoxDecoration(
- color: AppColors.primary.withOpacity(0.12),
- borderRadius: BorderRadius.circular(20),
- ),
- child: Text(
- '系统生成',
- style: theme.textTheme.bodySmall?.copyWith(
- fontSize: 11,
- color: AppColors.primary,
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- ],
- ],
- ),
- const SizedBox(height: 12),
- SelectableText.rich(
- TextSpan(
- children: _buildFormattedContent(content),
- style: const TextStyle(
- fontSize: 15,
- height: 1.8,
- color: AppColors.textPrimary,
- ),
- ),
- ),
- ],
- ),
- );
- final avatar = CircleAvatar(
- radius: 20,
- backgroundColor: isAI
- ? AppColors.success.withOpacity(0.15)
- : AppColors.primary.withOpacity(0.15),
- child: Icon(
- icon,
- color: isAI ? AppColors.success : AppColors.primary,
- size: 20,
- ),
- );
- return Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: isAI ? MainAxisAlignment.start : MainAxisAlignment.end,
- children: [
- if (isAI) ...[
- avatar,
- const SizedBox(width: 12),
- ],
- Flexible(child: bubble),
- if (!isAI) ...[
- const SizedBox(width: 12),
- avatar,
- ],
- ],
- );
- }
- List<TextSpan> _buildFormattedContent(String content) {
- final lines = content.split('\n');
- final spans = <TextSpan>[];
- for (int i = 0; i < lines.length; i++) {
- final line = lines[i];
- if (line.trim().isEmpty) {
- spans.add(const TextSpan(text: '\n'));
- continue;
- }
- // 检测公式或计算结果
- if (line.contains('=') || line.contains('计算')) {
- spans.add(
- TextSpan(
- text: line,
- style: TextStyle(
- color: AppColors.primary,
- fontWeight: FontWeight.w600,
- backgroundColor: AppColors.primary.withOpacity(0.1),
- ),
- ),
- );
- } else if (line.startsWith('-') || line.startsWith('•')) {
- // 列表项
- spans.add(
- TextSpan(
- text: line,
- style: const TextStyle(
- fontWeight: FontWeight.w500,
- ),
- ),
- );
- } else {
- spans.add(TextSpan(text: line));
- }
- if (i < lines.length - 1) {
- spans.add(const TextSpan(text: '\n'));
- }
- }
- return spans;
- }
- Widget _buildActionComposer(
- BuildContext context,
- List<DocumentElement> elements,
- ) {
- final theme = Theme.of(context);
- final hasElements = elements.isNotEmpty;
- return Container(
- padding: const EdgeInsets.all(10),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(18),
- border: Border.all(color: AppColors.borderLight),
- boxShadow: [
- BoxShadow(
- color: Colors.black.withOpacity(0.03),
- blurRadius: 18,
- offset: const Offset(0, 10),
- ),
- ],
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Container(
- padding:
- const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
- decoration: BoxDecoration(
- color: AppColors.primary.withOpacity(0.12),
- borderRadius: BorderRadius.circular(20),
- ),
- child: Row(
- children: [
- Icon(
- LucideIcons.sparkles,
- size: 16,
- color: AppColors.primary,
- ),
- const SizedBox(width: 6),
- Text(
- '生成新的 AI 回复',
- style: theme.textTheme.bodySmall?.copyWith(
- color: AppColors.primary,
- fontWeight: FontWeight.w600,
- ),
- ),
- ],
- ),
- ),
- const SizedBox(width: 12),
- Text(
- '可结合要素补充或修改 Prompt 内容',
- style: theme.textTheme.bodySmall?.copyWith(
- color: AppColors.textSecondary,
- ),
- ),
- ],
- ),
- if (hasElements) ...[
- const SizedBox(height: 16),
- SizedBox(
- height: 44,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Row(
- children: [
- for (int i = 0; i < elements.length; i++) ...[
- if (i > 0) const SizedBox(width: 8),
- _buildElementChip(elements[i]),
- ],
- ],
- ),
- ),
- ),
- ],
- const SizedBox(height: 16),
- Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: TextField(
- controller: _inputController,
- minLines: 4,
- maxLines: 10,
- onChanged: (_) => setState(() {}),
- decoration: InputDecoration(
- hintText: '描述需要 AI 调整或补充的内容,可点击要素快速引用…',
- filled: true,
- fillColor: AppColors.backgroundLight,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(14),
- borderSide: const BorderSide(color: AppColors.border),
- ),
- enabledBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(14),
- borderSide: const BorderSide(color: AppColors.border),
- ),
- focusedBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(14),
- borderSide: const BorderSide(
- color: AppColors.primary,
- width: 2,
- ),
- ),
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 18,
- vertical: 16,
- ),
- suffixIcon: _inputController.text.isEmpty
- ? null
- : IconButton(
- icon: const Icon(LucideIcons.x),
- onPressed: () {
- _inputController.clear();
- setState(() {});
- },
- ),
- ),
- ),
- ),
- const SizedBox(width: 16),
- SizedBox(
- width: 152,
- child: Column(
- children: [
- AppButton(
- text: '发送',
- type: ButtonType.primary,
- icon: LucideIcons.send,
- loading: _isSending,
- onPressed:
- _isSending || _inputController.text.trim().isEmpty
- ? null
- : _handleSend,
- fullWidth: true,
- ),
- const SizedBox(height: 10),
- AppButton(
- text: '重新生成',
- type: ButtonType.secondary,
- icon: LucideIcons.refresh_cw,
- loading: _isRegenerating,
- onPressed: _isRegenerating ? null : _handleRegenerate,
- fullWidth: true,
- ),
- ],
- ),
- ),
- ],
- ),
- const SizedBox(height: 16),
- Row(
- children: [
- AppButton(
- text: '导出结果',
- type: ButtonType.secondary,
- icon: LucideIcons.download,
- onPressed: () {},
- ),
- const SizedBox(width: 12),
- AppButton(
- text: '复制回复',
- type: ButtonType.text,
- icon: LucideIcons.copy,
- onPressed: () {
- Clipboard.setData(ClipboardData(text: response));
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('AI 回复已复制')),
- );
- },
- ),
- ],
- ),
- ],
- ),
- );
- }
- Widget _buildElementChip(DocumentElement element) {
- final color = _getElementColor(element.type);
- return ActionChip(
- label: Text(
- '${element.type.label}|${element.value}',
- style: TextStyle(
- fontSize: 12,
- color: color,
- fontWeight: FontWeight.w600,
- ),
- overflow: TextOverflow.ellipsis,
- ),
- avatar: Icon(
- LucideIcons.tag,
- size: 14,
- color: color,
- ),
- backgroundColor: color.withOpacity(0.12),
- shape: StadiumBorder(
- side: BorderSide(color: color.withOpacity(0.3)),
- ),
- onPressed: () => _insertElementIntoInput(element),
- );
- }
- void _insertElementIntoInput(DocumentElement element) {
- final insertion = '[${element.label}: ${element.value}]';
- final text = _inputController.text;
- TextSelection selection = _inputController.selection;
- if (!selection.isValid) {
- selection = TextSelection.collapsed(offset: text.length);
- }
- final newText = text.replaceRange(
- selection.start,
- selection.end,
- insertion,
- );
- final cursorPosition = selection.start + insertion.length;
- _inputController.value = TextEditingValue(
- text: newText,
- selection: TextSelection.collapsed(offset: cursorPosition),
- );
- setState(() {});
- }
- Future<void> _handleSend() async {
- final content = _inputController.text.trim();
- if (content.isEmpty) return;
- setState(() => _isSending = true);
- await Future.delayed(const Duration(seconds: 2));
- if (!mounted) return;
- _inputController.clear();
- setState(() => _isSending = false);
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('已发送请求,等待 AI 生成新回复')),
- );
- }
- Future<void> _handleRegenerate() async {
- setState(() => _isRegenerating = true);
- await Future.delayed(const Duration(seconds: 2));
- if (!mounted) return;
- setState(() => _isRegenerating = false);
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('已重新生成 AI 回复')),
- );
- }
- Color _getElementColor(ElementType type) {
- switch (type) {
- case ElementType.amount:
- return AppColors.amount;
- case ElementType.company:
- return AppColors.company;
- case ElementType.person:
- return AppColors.person;
- case ElementType.location:
- return AppColors.location;
- case ElementType.date:
- return AppColors.date;
- case ElementType.other:
- return AppColors.other;
- }
- }
- }
|