chatmcp/lib/page/layout/widgets/chat_setting.dart

288 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:chatmcp/provider/settings_provider.dart';
import 'package:chatmcp/generated/app_localizations.dart';
class ChatSetting extends StatelessWidget {
const ChatSetting({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
final modelSetting = settingsProvider.modelSetting;
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle(context, l10n.modelSettings, CupertinoIcons.slider_horizontal_3),
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colorScheme.outline.withAlpha(26), width: 1),
boxShadow: [BoxShadow(color: colorScheme.shadow.withAlpha(13), blurRadius: 10, offset: const Offset(0, 2))],
),
child: Column(
children: [
_buildSliderWithLabel(
context: context,
label: l10n.temperature(modelSetting.temperature.toStringAsFixed(2)),
value: modelSetting.temperature,
min: 0.0,
max: 2.0,
onChanged: (value) => _updateModelSettings(settingsProvider, temperature: value),
tooltip: l10n.temperatureTooltip,
),
_buildDivider(context),
_buildSliderWithLabel(
context: context,
label: l10n.topP(modelSetting.topP.toStringAsFixed(2)),
value: modelSetting.topP,
min: 0.0,
max: 1.0,
onChanged: (value) => _updateModelSettings(settingsProvider, topP: value),
tooltip: l10n.topPTooltip,
),
_buildDivider(context),
_buildNumberInput(
context: context,
label: l10n.maxTokens,
value: modelSetting.maxTokens,
onChanged: (value) => _updateModelSettings(settingsProvider, maxTokens: value),
tooltip: l10n.maxTokensTooltip,
),
_buildDivider(context),
_buildSliderWithLabel(
context: context,
label: l10n.frequencyPenalty(modelSetting.frequencyPenalty.toStringAsFixed(2)),
value: modelSetting.frequencyPenalty,
min: -2.0,
max: 2.0,
onChanged: (value) => _updateModelSettings(settingsProvider, frequencyPenalty: value),
tooltip: l10n.frequencyPenaltyTooltip,
),
_buildDivider(context),
_buildSliderWithLabel(
context: context,
label: l10n.presencePenalty(modelSetting.presencePenalty.toStringAsFixed(2)),
value: modelSetting.presencePenalty,
min: -2.0,
max: 2.0,
onChanged: (value) => _updateModelSettings(settingsProvider, presencePenalty: value),
tooltip: l10n.presencePenaltyTooltip,
),
],
),
),
const SizedBox(height: 20),
Align(
alignment: Alignment.centerRight,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colorScheme.primary.withAlpha(51), width: 1),
),
child: TextButton.icon(
onPressed: () => _resetModelSettings(settingsProvider),
icon: Icon(CupertinoIcons.refresh_thin, size: 18, color: colorScheme.primary),
label: Text(
'Reset',
style: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.w500),
),
style: TextButton.styleFrom(
foregroundColor: colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
),
],
),
),
);
},
);
}
Widget _buildSectionTitle(BuildContext context, String title, IconData icon) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 4.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: colorScheme.primary.withAlpha(26), borderRadius: BorderRadius.circular(8)),
child: Icon(icon, size: 20, color: colorScheme.primary),
),
const SizedBox(width: 12),
Text(
title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colorScheme.onSurface, letterSpacing: -0.5),
),
],
),
);
}
Widget _buildDivider(BuildContext context) {
return Divider(height: 1, indent: 20, endIndent: 20, color: Theme.of(context).colorScheme.outline.withAlpha(21));
}
Widget _buildSliderWithLabel({
required BuildContext context,
required String label,
required double value,
required double min,
required double max,
required ValueChanged<double> onChanged,
String? tooltip,
}) {
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
label,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: colorScheme.onSurface),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: colorScheme.primary.withAlpha(26), borderRadius: BorderRadius.circular(6)),
child: Text(
value.toStringAsFixed(2),
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colorScheme.primary),
),
),
],
),
if (tooltip != null)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(tooltip, style: TextStyle(fontSize: 13, color: colorScheme.onSurface.withAlpha(153), height: 1.3)),
),
const SizedBox(height: 12),
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: colorScheme.primary,
inactiveTrackColor: colorScheme.primary.withAlpha(39),
thumbColor: colorScheme.primary,
overlayColor: colorScheme.primary.withAlpha(26),
trackHeight: 4,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 16),
),
child: Slider(value: value, min: min, max: max, onChanged: onChanged),
),
],
),
);
}
Widget _buildNumberInput({
required BuildContext context,
required String label,
required int? value,
required ValueChanged<int?> onChanged,
String? tooltip,
}) {
final l10n = AppLocalizations.of(context)!;
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: colorScheme.onSurface),
),
if (tooltip != null)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(tooltip, style: TextStyle(fontSize: 13, color: colorScheme.onSurface.withAlpha(153), height: 1.3)),
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colorScheme.outline.withAlpha(51), width: 1),
),
child: TextField(
controller: TextEditingController(text: value?.toString() ?? ''),
keyboardType: TextInputType.number,
style: TextStyle(fontSize: 15, color: colorScheme.onSurface, fontWeight: FontWeight.w500),
decoration: InputDecoration(
hintText: l10n.enterMaxTokens,
hintStyle: TextStyle(color: colorScheme.onSurface.withAlpha(128), fontWeight: FontWeight.normal),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
),
onChanged: (value) {
if (value.isEmpty) {
onChanged(null);
} else {
onChanged(int.tryParse(value));
}
},
),
),
],
),
);
}
void _updateModelSettings(
SettingsProvider provider, {
double? temperature,
int? maxTokens,
double? topP,
double? frequencyPenalty,
double? presencePenalty,
}) {
final currentSettings = provider.modelSetting;
provider.updateModelSettings(
temperature: temperature != null ? _roundToOneDecimal(temperature) : currentSettings.temperature,
maxTokens: maxTokens,
topP: topP != null ? _roundToOneDecimal(topP) : currentSettings.topP,
frequencyPenalty: frequencyPenalty != null ? _roundToOneDecimal(frequencyPenalty) : currentSettings.frequencyPenalty,
presencePenalty: presencePenalty != null ? _roundToOneDecimal(presencePenalty) : currentSettings.presencePenalty,
);
}
/// 将double值四舍五入到小数点后一位
double _roundToOneDecimal(double value) {
return (value * 10).round() / 10;
}
Future<void> _resetModelSettings(SettingsProvider provider) async {
await provider.resetModelSettings();
}
}