Add new features and update configurations for improved functionality
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
|
||||
class AgcGhostButton extends StatefulWidget {
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
AgcGhostButton({
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AgcGhostButton> createState() => _GhostButtonState();
|
||||
}
|
||||
|
||||
class _GhostButtonState extends State<AgcGhostButton> {
|
||||
|
||||
bool _hovering = false;
|
||||
bool _pressing = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final radius = widget.borderRadius ?? BorderRadius.circular(
|
||||
Theme.of(context).radiusSm - 4
|
||||
);
|
||||
|
||||
Color bg = Colors.transparent;
|
||||
if (_pressing) {
|
||||
bg = colorScheme.accent.withOpacity(0.8);
|
||||
} else if (_hovering) {
|
||||
bg = colorScheme.accent.withOpacity(0.5);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
cursor: widget.onPressed != null ? SystemMouseCursors.click : MouseCursor.defer,
|
||||
onEnter: (_) => setState(() => _hovering = true),
|
||||
onExit: (_) => setState(() {
|
||||
_hovering = false;
|
||||
_pressing = false;
|
||||
}),
|
||||
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => setState(() => _pressing = true),
|
||||
onTapUp: (_) {
|
||||
setState(() => _pressing = false);
|
||||
if (widget.onPressed != null) widget.onPressed!();
|
||||
},
|
||||
onTapCancel: () => setState(() => _pressing = false),
|
||||
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 80),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: radius,
|
||||
),
|
||||
padding: EdgeInsets.all(4),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AgcSecondaryButton extends StatefulWidget {
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool enabled;
|
||||
|
||||
AgcSecondaryButton({
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.borderRadius,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AgcSecondaryButton> createState() => _SecondaryButtonState();
|
||||
}
|
||||
|
||||
class _SecondaryButtonState extends State<AgcSecondaryButton> {
|
||||
|
||||
bool _hovering = false;
|
||||
bool _pressing = false;
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final radius = widget.borderRadius ?? BorderRadius.circular(
|
||||
Theme.of(context).radiusSm
|
||||
);
|
||||
|
||||
final bool active = widget.enabled && widget.onPressed != null;
|
||||
|
||||
Color bg = colorScheme.secondary;
|
||||
if (!active) {
|
||||
bg = colorScheme.secondary.withOpacity(0.4);
|
||||
} else if (_pressing) {
|
||||
bg = colorScheme.secondary.withOpacity(0.75);
|
||||
} else if (_hovering) {
|
||||
bg = colorScheme.secondary.withOpacity(0.85);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
cursor: active ? SystemMouseCursors.click : MouseCursor.defer,
|
||||
onEnter: (_) { if (active) setState(() => _hovering = true); },
|
||||
onExit: (_) => setState(() {
|
||||
_hovering = false;
|
||||
_pressing = false;
|
||||
}),
|
||||
|
||||
child: GestureDetector(
|
||||
onTapDown: active ? (_) => setState(() => _pressing = true) : null,
|
||||
onTapUp: active ? (_) {
|
||||
setState(() => _pressing = false);
|
||||
widget.onPressed!();
|
||||
} : null,
|
||||
onTapCancel: active ? () => setState(() => _pressing = false) : null,
|
||||
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 80),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: radius,
|
||||
),
|
||||
padding: EdgeInsets.all(4),
|
||||
child: DefaultTextStyle.merge(
|
||||
style: TextStyle(color: colorScheme.secondaryForeground),
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(color: colorScheme.secondaryForeground),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AgcOutlinedButton extends StatefulWidget {
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool enabled;
|
||||
|
||||
AgcOutlinedButton({
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.borderRadius,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AgcOutlinedButton> createState() => _OutlinedButtonState();
|
||||
}
|
||||
|
||||
class _OutlinedButtonState extends State<AgcOutlinedButton> {
|
||||
|
||||
bool _hovering = false;
|
||||
bool _pressing = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final radius = widget.borderRadius ?? BorderRadius.circular(
|
||||
Theme.of(context).radiusSm
|
||||
);
|
||||
|
||||
final bool active = widget.enabled && widget.onPressed != null;
|
||||
|
||||
Color bg = Colors.transparent;
|
||||
if (_pressing && active) {
|
||||
bg = colorScheme.accent.withOpacity(0.6);
|
||||
} else if (_hovering && active) {
|
||||
bg = colorScheme.accent.withOpacity(0.35);
|
||||
}
|
||||
|
||||
final borderColor = active
|
||||
? colorScheme.border
|
||||
: colorScheme.border.withOpacity(0.4);
|
||||
|
||||
return MouseRegion(
|
||||
cursor: active ? SystemMouseCursors.click : MouseCursor.defer,
|
||||
onEnter: (_) { if (active) setState(() => _hovering = true); },
|
||||
onExit: (_) => setState(() {
|
||||
_hovering = false;
|
||||
_pressing = false;
|
||||
}),
|
||||
|
||||
child: GestureDetector(
|
||||
onTapDown: active ? (_) => setState(() => _pressing = true) : null,
|
||||
onTapUp: active ? (_) {
|
||||
setState(() => _pressing = false);
|
||||
widget.onPressed!();
|
||||
} : null,
|
||||
onTapCancel: active ? () => setState(() => _pressing = false) : null,
|
||||
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 80),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: radius,
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: DefaultTextStyle.merge(
|
||||
style: TextStyle(
|
||||
color: active ? colorScheme.foreground : colorScheme.mutedForeground,
|
||||
),
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(
|
||||
color: active ? colorScheme.foreground : colorScheme.mutedForeground,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import "package:flutter/widgets.dart" hide Tooltip;
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart" hide Row, Expanded;
|
||||
|
||||
import "../../providers/chat_provider.dart";
|
||||
import "../../providers/cost_provider.dart";
|
||||
import "../../providers/settings_provider.dart";
|
||||
import "package:provider/provider.dart";
|
||||
|
||||
|
||||
|
||||
String _fmtTokens(int n) {
|
||||
final s = n.toString();
|
||||
final buf = StringBuffer();
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
if (i > 0 && (s.length - i) % 3 == 0) buf.write(",");
|
||||
buf.write(s[i]);
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
|
||||
class FooterBar extends StatelessWidget {
|
||||
const FooterBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final mutedFg = theme.colorScheme.mutedForeground;
|
||||
final borderColor = theme.colorScheme.border;
|
||||
final bg = theme.colorScheme.muted.scaleAlpha(0.3);
|
||||
|
||||
final costProvider = context.watch<CostProvider>();
|
||||
final settingsProvider = context.watch<SettingsProvider>();
|
||||
final chatProvider = context.watch<ChatProvider>();
|
||||
|
||||
final model = settingsProvider.settings.model ?? "unknown";
|
||||
final costUsd = costProvider.getTotalCostUsd();
|
||||
final cost = "\$${costUsd.toStringAsFixed(4)}";
|
||||
final inputToks = costProvider.getTotalInputTokens();
|
||||
final outputToks = costProvider.getTotalOutputTokens();
|
||||
final isLoading = chatProvider.isLoading;
|
||||
final contextTokens = chatProvider.contextTokens;
|
||||
|
||||
final textStyle = TextStyle(
|
||||
fontFamily: "monospace",
|
||||
fontSize: 11,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: mutedFg,
|
||||
);
|
||||
|
||||
Widget divider() => const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5),
|
||||
child: SizedBox(height: 12, child: VerticalDivider(width: 1)),
|
||||
);
|
||||
|
||||
Widget copyrightBlock() {
|
||||
return Text(
|
||||
"© 2026 IMBENJI.NET LTD - The Agency",
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
|
||||
Widget statusBlock() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
Text(
|
||||
isLoading ? "running..." : "idle",
|
||||
style: textStyle.copyWith(
|
||||
color: isLoading
|
||||
? theme.colorScheme.primary
|
||||
: mutedFg,
|
||||
),
|
||||
),
|
||||
|
||||
divider(),
|
||||
|
||||
Text(model.split("/").last, style: textStyle),
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget statsBlock() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
if (contextTokens > 0) ...[
|
||||
Text(_fmtTokens(contextTokens), style: textStyle),
|
||||
Text(" tokens", style: textStyle),
|
||||
divider(),
|
||||
],
|
||||
|
||||
Tooltip(
|
||||
tooltip: (_) => TooltipContainer(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
child: Text(
|
||||
"In: $inputToks\nOut: $outputToks",
|
||||
style: const TextStyle(
|
||||
fontFamily: "monospace",
|
||||
fontSize: 11,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(cost, style: textStyle),
|
||||
),
|
||||
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
border: Border(top: BorderSide(color: borderColor, width: 1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
Expanded(child: Row(children: [copyrightBlock()])),
|
||||
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [statusBlock()],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [statsBlock()],
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import "package:provider/provider.dart";
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
|
||||
import "../../providers/settings_provider.dart";
|
||||
import "../chat/model_picker.dart";
|
||||
|
||||
class SettingsSheet extends StatelessWidget {
|
||||
const SettingsSheet();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<SettingsProvider>(
|
||||
builder: (context, settingsProvider, _) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Settings",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// model picker
|
||||
const Text(
|
||||
"Model",
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const ModelPicker(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// API key setting
|
||||
const Text(
|
||||
"OpenRouter API Key",
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_ApiKeyInput(settingsProvider: settingsProvider),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// theme setting
|
||||
const Text(
|
||||
"Theme",
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SimpleDropdown<String>(
|
||||
value: settingsProvider.settings.theme,
|
||||
items: const ["dark", "light"],
|
||||
onChanged: (newTheme) {
|
||||
settingsProvider.updateTheme(newTheme);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// effort level setting
|
||||
const Text(
|
||||
"Effort Level",
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SimpleDropdown<String>(
|
||||
value: settingsProvider.settings.effortLevel ?? "medium",
|
||||
items: const ["low", "medium", "high", "max"],
|
||||
onChanged: (newLevel) {
|
||||
settingsProvider.updateEffortLevel(newLevel);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// reset button
|
||||
Button.ghost(
|
||||
onPressed: () {
|
||||
settingsProvider.resetToDefaults();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text("Reset to Defaults"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ApiKeyInput extends StatefulWidget {
|
||||
final SettingsProvider settingsProvider;
|
||||
|
||||
const _ApiKeyInput({required this.settingsProvider});
|
||||
|
||||
@override
|
||||
State<_ApiKeyInput> createState() => _ApiKeyInputState();
|
||||
}
|
||||
|
||||
class _ApiKeyInputState extends State<_ApiKeyInput> {
|
||||
late TextEditingController _controller;
|
||||
bool _obscureText = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(
|
||||
text: widget.settingsProvider.settings.openRouterApiKey ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
obscureText: _obscureText,
|
||||
onChanged: (value) {
|
||||
widget.settingsProvider.updateApiKey(value);
|
||||
},
|
||||
placeholder: const Text("sk-or-v1-..."),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
_obscureText ? "Show" : "Hide",
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF999999)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SimpleDropdown<T> extends StatelessWidget {
|
||||
final T value;
|
||||
final List<T> items;
|
||||
final Function(T) onChanged;
|
||||
|
||||
const _SimpleDropdown({
|
||||
required this.value,
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => _showMenu(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(value.toString()),
|
||||
const Text("▼", style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMenu(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: items
|
||||
.map((item) {
|
||||
final isSelected = item == value;
|
||||
return Container(
|
||||
color: isSelected ? const Color(0xFFF1F5F9) : Colors.transparent,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onChanged(item);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isSelected)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
child: Text("✓", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
Text(item.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user