Add new features and update configurations for improved functionality

This commit is contained in:
ImBenji
2026-04-11 12:34:00 +01:00
parent fa4415553d
commit 0b6b604c56
125 changed files with 14119 additions and 1664 deletions
+233
View File
@@ -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,
),
),
),
),
);
}
}
+147
View File
@@ -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()],
),
),
],
),
);
}
}
+220
View File
@@ -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(),
),
),
);
}
}