Files
ai-contract-platform/mobile/lib/features/tools/presentation/tools_hub_screen.dart
T

456 lines
14 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../../app/theme/brand_assets.dart';
import '../../chat/application/review_chat_controller.dart';
import '../../chat/domain/review_chat_context.dart';
import '../../chat/presentation/review_chat_screen.dart';
import '../../contracts/presentation/contract_template_list_screen.dart';
import '../../documents/presentation/legal_document_type_list_screen.dart';
class ToolsHubScreen extends StatefulWidget {
const ToolsHubScreen({
super.key,
this.controller,
});
final ReviewChatController? controller;
@override
State<ToolsHubScreen> createState() => _ToolsHubScreenState();
}
class _ToolsHubScreenState extends State<ToolsHubScreen> {
late final ReviewChatController _chatController;
late final bool _ownsController;
@override
void initState() {
super.initState();
_chatController = widget.controller ?? ReviewChatController();
_ownsController = widget.controller == null;
}
@override
void dispose() {
if (_ownsController) {
_chatController.dispose();
}
super.dispose();
}
Future<void> _openContractsWorkspace(BuildContext context) async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ContractTemplateListScreen(),
),
);
}
Future<void> _openDocumentsWorkspace(BuildContext context) async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const LegalDocumentTypeListScreen(),
),
);
}
Future<void> _sendBusinessPrompt(ReviewChatQuickPrompt prompt) async {
await _chatController.sendMessage(message: prompt.prompt);
final error = _chatController.errorMessage.value;
if (!mounted || error == null || error.isEmpty) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error)),
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isCompact = constraints.maxHeight < 820;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: _ToolsHeader(
isCompact: isCompact,
onOpenContracts: () => _openContractsWorkspace(context),
onOpenDocuments: () => _openDocumentsWorkspace(context),
businessPrompts: buildBusinessAuxiliaryQuickPrompts(),
isSendingListenable: _chatController.isSending,
onRunBusinessPrompt: _sendBusinessPrompt,
),
),
Expanded(
child: ReviewChatScreen(
key: const ValueKey('tools-hub-legal-chat'),
embedded: true,
showBusinessAuxiliaryPrompts: false,
controller: _chatController,
),
),
],
);
},
);
}
}
class _ToolsHeader extends StatelessWidget {
const _ToolsHeader({
required this.isCompact,
required this.onOpenContracts,
required this.onOpenDocuments,
required this.businessPrompts,
required this.isSendingListenable,
required this.onRunBusinessPrompt,
});
final bool isCompact;
final Future<void> Function() onOpenContracts;
final Future<void> Function() onOpenDocuments;
final List<ReviewChatQuickPrompt> businessPrompts;
final ValueListenable<bool> isSendingListenable;
final Future<void> Function(ReviewChatQuickPrompt prompt) onRunBusinessPrompt;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(isCompact ? 16 : 20),
decoration: BoxDecoration(
gradient: BrandAssets.surfaceGradient,
borderRadius: BorderRadius.circular(BrandAssets.radius),
border: Border.all(color: Colors.white.withValues(alpha: 0.86)),
boxShadow: const <BoxShadow>[BrandAssets.cardShadow],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BrandMark(size: isCompact ? 40 : 48),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AI工具',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: BrandAssets.ink,
),
),
const SizedBox(height: 4),
Text(
'把法律问答留在当前页继续追问,把合同生成和文书起草切到独立工作台处理。',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const BrandChip(
label: 'Tools',
color: BrandAssets.primaryDark,
),
],
),
if (!isCompact) ...[
const SizedBox(height: 16),
_ToolLaunchTile(
icon: Icons.gavel_outlined,
title: '法律问答',
subtitle: '适合围绕条款风险、谈判策略和依据来源继续追问。',
actionLabel: '当前页继续',
onPressed: null,
emphasis: _ToolLaunchEmphasis.passive,
),
],
const SizedBox(height: 12),
_BusinessAuxiliaryLaunchStrip(
isCompact: isCompact,
prompts: businessPrompts,
isSendingListenable: isSendingListenable,
onRunBusinessPrompt: onRunBusinessPrompt,
),
const SizedBox(height: 12),
_WorkspaceLaunchRow(
onOpenContracts: onOpenContracts,
onOpenDocuments: onOpenDocuments,
),
],
),
);
}
}
class _BusinessAuxiliaryLaunchStrip extends StatelessWidget {
const _BusinessAuxiliaryLaunchStrip({
required this.isCompact,
required this.prompts,
required this.isSendingListenable,
required this.onRunBusinessPrompt,
});
final bool isCompact;
final List<ReviewChatQuickPrompt> prompts;
final ValueListenable<bool> isSendingListenable;
final Future<void> Function(ReviewChatQuickPrompt prompt) onRunBusinessPrompt;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: isSendingListenable,
builder: (context, isSending, _) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(isCompact ? 12 : 14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.56),
borderRadius: BorderRadius.circular(BrandAssets.radius),
border: Border.all(color: BrandAssets.line),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: BrandAssets.accentAmber.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(BrandAssets.radius),
),
child: const Icon(
Icons.auto_awesome_outlined,
color: BrandAssets.accentAmber,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'业务辅助',
style:
Theme.of(context).textTheme.titleSmall?.copyWith(
color: BrandAssets.ink,
),
),
const SizedBox(height: 4),
Text(
'从这里直接发起履约义务提取、工商备案材料、谈判策略和法律意见边界问答。',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
for (var index = 0; index < prompts.length; index++)
_BusinessAuxiliaryButton(
prompt: prompts[index],
icon: _businessPromptIcon(index),
isSending: isSending,
onPressed: () => onRunBusinessPrompt(prompts[index]),
),
],
),
],
),
);
},
);
}
}
class _BusinessAuxiliaryButton extends StatelessWidget {
const _BusinessAuxiliaryButton({
required this.prompt,
required this.icon,
required this.isSending,
required this.onPressed,
});
final ReviewChatQuickPrompt prompt;
final IconData icon;
final bool isSending;
final Future<void> Function() onPressed;
@override
Widget build(BuildContext context) {
return Tooltip(
message: prompt.prompt,
child: OutlinedButton.icon(
onPressed: isSending ? null : () => unawaited(onPressed()),
icon: Icon(icon, size: 18),
label: Text(prompt.label),
),
);
}
}
IconData _businessPromptIcon(int index) {
switch (index) {
case 0:
return Icons.checklist_outlined;
case 1:
return Icons.folder_open_outlined;
case 2:
return Icons.handshake_outlined;
default:
return Icons.gavel_outlined;
}
}
enum _ToolLaunchEmphasis {
primary,
passive,
}
class _ToolLaunchTile extends StatelessWidget {
const _ToolLaunchTile({
required this.icon,
required this.title,
required this.subtitle,
required this.actionLabel,
required this.emphasis,
this.onPressed,
});
final IconData icon;
final String title;
final String subtitle;
final String actionLabel;
final Future<void> Function()? onPressed;
final _ToolLaunchEmphasis emphasis;
@override
Widget build(BuildContext context) {
final isPrimary = emphasis == _ToolLaunchEmphasis.primary;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isPrimary ? BrandAssets.surfaceInk : BrandAssets.surface,
borderRadius: BorderRadius.circular(BrandAssets.radius),
border: Border.all(
color: isPrimary ? Colors.transparent : BrandAssets.line,
),
boxShadow:
isPrimary ? const <BoxShadow>[BrandAssets.panelShadow] : null,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isPrimary
? Colors.white.withValues(alpha: 0.1)
: BrandAssets.accentCyan.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(BrandAssets.radius),
),
child: Icon(
icon,
color:
isPrimary ? BrandAssets.accentLime : BrandAssets.accentCyan,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: isPrimary ? Colors.white : BrandAssets.ink,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isPrimary
? Colors.white.withValues(alpha: 0.72)
: BrandAssets.muted,
),
),
const SizedBox(height: 12),
if (onPressed == null)
Text(
actionLabel,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: isPrimary
? BrandAssets.accentLime
: Theme.of(context).colorScheme.primary,
),
)
else
FilledButton.icon(
onPressed: () => unawaited(onPressed!.call()),
icon: const Icon(Icons.open_in_new, size: 18),
label: Text(actionLabel),
),
],
),
),
],
),
);
}
}
class _WorkspaceLaunchRow extends StatelessWidget {
const _WorkspaceLaunchRow({
required this.onOpenContracts,
required this.onOpenDocuments,
});
final Future<void> Function() onOpenContracts;
final Future<void> Function() onOpenDocuments;
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 12,
runSpacing: 12,
children: [
Tooltip(
message: 'open-contracts-workspace',
child: FilledButton.icon(
onPressed: () => unawaited(onOpenContracts()),
icon: const Icon(Icons.edit_note_outlined),
label: const Text('打开合同工作台'),
),
),
Tooltip(
message: 'open-documents-workspace',
child: OutlinedButton.icon(
onPressed: () => unawaited(onOpenDocuments()),
icon: const Icon(Icons.post_add_outlined),
label: const Text('打开文书工作台'),
),
),
],
);
}
}