Add initial project files and configurations for clawd_code

This commit is contained in:
ImBenji
2026-04-03 17:48:07 +01:00
parent 7541a5279b
commit c88a1badc7
273 changed files with 28339 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
import "package:shadcn_flutter/shadcn_flutter.dart";
class GarageHeader extends StatelessWidget {
const GarageHeader({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"THE AGENCY",
style: TextStyle(
fontSize: 32,
height: 1,
fontWeight: FontWeight.w900,
),
),
Text(
"by IMBENJI.NET LTD",
style: TextStyle(
fontSize: 12,
height: 1,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.mutedForeground,
),
),
],
);
}
}
+61
View File
@@ -0,0 +1,61 @@
import "package:flutter/widgets.dart";
import "package:provider/provider.dart";
import "../providers/chat_provider.dart";
import "message_bubble.dart";
class ChatView extends StatefulWidget {
const ChatView();
@override
State<ChatView> createState() => _ChatViewState();
}
class _ChatViewState extends State<ChatView> {
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
@override
Widget build(BuildContext context) {
return Consumer<ChatProvider>(
builder: (context, chatProvider, _) {
// scroll to bottom when new messages arrive
if (chatProvider.messages.isNotEmpty) {
_scrollToBottom();
}
return ListView.builder(
controller: _scrollController,
itemCount: chatProvider.messages.length,
itemBuilder: (context, index) {
final message = chatProvider.messages[index];
return MessageBubble(message: message);
},
);
},
);
}
}
+33
View File
@@ -0,0 +1,33 @@
import "package:provider/provider.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
import "../providers/cost_provider.dart";
class CostBadge extends StatelessWidget {
const CostBadge();
@override
Widget build(BuildContext context) {
return Consumer<CostProvider>(
builder: (context, costProvider, _) {
final costStr = costProvider.getFormattedTotalCost();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(4),
),
child: Text(
costStr,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF475569),
),
),
);
},
);
}
}
+82
View File
@@ -0,0 +1,82 @@
import "package:provider/provider.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
import "../providers/chat_provider.dart";
class InputBar extends StatefulWidget {
const InputBar({super.key});
@override
State<InputBar> createState() => _InputBarState();
}
class _InputBarState extends State<InputBar> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _send(ChatProvider provider) {
final text = _controller.text.trim();
if (text.isEmpty) return;
provider.sendMessage(text);
_controller.clear();
}
@override
Widget build(BuildContext context) {
return Consumer<ChatProvider>(
builder: (context, chatProvider, _) {
return Container(
padding: const EdgeInsets.all(14),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: Color(0xFFE2E8F0)),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextField(
controller: _controller,
minLines: 1,
maxLines: 4,
placeholder: const Text("Type a message..."),
enabled: !chatProvider.isLoading,
onSubmitted: chatProvider.isLoading ? null : (_) => _send(chatProvider),
),
),
const SizedBox(width: 10),
chatProvider.isLoading
? const Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(value: null),
),
)
: PrimaryButton(
onPressed: () => _send(chatProvider),
child: const Text("Send"),
),
],
),
);
},
);
}
}
+94
View File
@@ -0,0 +1,94 @@
import "package:flutter/material.dart" as material hide Card;
import "package:flutter_markdown/flutter_markdown.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
import "../../src/session/session_types.dart";
class MessageBubble extends StatelessWidget {
const MessageBubble({required this.message});
final Message message;
@override
Widget build(BuildContext context) {
final isUser = message.role == "user";
final isTool = message.role == "tool";
final isAssistant = message.role == "assistant";
final accentColor = isTool
? const Color(0xFF64748B)
: const Color(0xFF94A3B8);
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
constraints: BoxConstraints(
maxWidth: material.MediaQuery.of(context).size.width * 0.7,
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: material.Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message.role,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: accentColor,
),
),
const SizedBox(height: 4),
if (isAssistant || isTool)
MarkdownBody(
data: isTool
? _buildToolMarkdown(message.content)
: message.content,
selectable: true,
shrinkWrap: true,
styleSheet: isTool
? _toolMarkdownStyleSheet(context)
: null,
)
else
Text(message.content),
],
),
),
),
),
);
}
String _buildToolMarkdown(String content) {
final lines = content.split("\n");
if (lines.isEmpty) {
return "```text\n\n```";
}
final title = lines.first.trim();
final body = lines.skip(1).join("\n").trimRight();
if (body.isEmpty) {
return title;
}
return "$title\n\n```text\n$body\n```";
}
MarkdownStyleSheet _toolMarkdownStyleSheet(BuildContext context) {
final theme = Theme.of(context);
return MarkdownStyleSheet.fromTheme(material.Theme.of(context)).copyWith(
p: theme.typography.base.copyWith(height: 1.35),
codeblockDecoration: BoxDecoration(
color: theme.colorScheme.muted.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(10),
),
codeblockPadding: const EdgeInsets.all(12),
code: theme.typography.base.copyWith(
fontFamily: "monospace",
height: 1.35,
),
);
}
}
+145
View File
@@ -0,0 +1,145 @@
import "package:flutter/material.dart";
import "package:provider/provider.dart";
import "../../src/api/openrouter_client.dart";
import "../providers/settings_provider.dart";
class ModelPicker extends StatefulWidget {
const ModelPicker();
@override
State<ModelPicker> createState() => _ModelPickerState();
}
class _ModelPickerState extends State<ModelPicker> {
late Future<List<Map<String, dynamic>>> _modelsFuture;
@override
void initState() {
super.initState();
_modelsFuture = _loadModels();
}
Future<List<Map<String, dynamic>>> _loadModels() async {
try {
final apiKey = context.read<SettingsProvider>().settings.openRouterApiKey;
if (apiKey == null || apiKey.isEmpty) {
return [
{"id": "openai/gpt-4o", "name": "GPT-4o"},
{"id": "anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6"},
{"id": "google/gemini-2.0-flash-001", "name": "Gemini 2.0 Flash"},
];
}
final client = await OpenRouterClientFactory.create(apiKey: apiKey);
final models = await client.listModels();
client.close();
return models;
} catch (e) {
return [
{"id": "openai/gpt-4o", "name": "GPT-4o"},
{"id": "anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6"},
{"id": "google/gemini-2.0-flash-001", "name": "Gemini 2.0 Flash"},
];
}
}
@override
Widget build(BuildContext context) {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, _) {
final currentModel = settingsProvider.normalizeModelId(
settingsProvider.settings.model,
);
return FutureBuilder<List<Map<String, dynamic>>>(
future: _modelsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading models...");
}
final models = snapshot.data ?? [];
final selectedModel = models.firstWhere(
(m) => m["id"] == currentModel,
orElse: () => {"id": currentModel, "name": currentModel},
);
return GestureDetector(
onTap: () => _showModelMenu(
context,
models,
currentModel,
settingsProvider,
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE2E8F0)),
borderRadius: BorderRadius.circular(6),
),
child: Text(selectedModel["name"] as String? ?? currentModel),
),
);
},
);
},
);
}
void _showModelMenu(
BuildContext context,
List<Map<String, dynamic>> models,
String currentModel,
SettingsProvider settingsProvider,
) {
showDialog(
context: context,
builder: (ctx) => Dialog(
child: Container(
constraints: const BoxConstraints(maxHeight: 400),
child: SingleChildScrollView(
child: Column(
children: models.map((model) {
final modelId = model["id"] as String;
final modelName = model["name"] as String? ?? modelId;
final isSelected = modelId == currentModel;
return Container(
color: isSelected
? const Color(0xFFF1F5F9)
: Colors.transparent,
child: GestureDetector(
onTap: () {
settingsProvider.updateModel(modelId);
Navigator.pop(ctx);
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
if (isSelected)
const Padding(
padding: EdgeInsets.only(right: 8),
child: Text(
"",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(child: Text(modelName)),
],
),
),
),
);
}).toList(),
),
),
),
),
);
}
}
+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 "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(),
),
),
);
}
}
+97
View File
@@ -0,0 +1,97 @@
import "package:provider/provider.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
import "../providers/chat_provider.dart";
import "../providers/session_provider.dart";
class Sidebar extends StatelessWidget {
const Sidebar();
@override
Widget build(BuildContext context) {
return Consumer<SessionProvider>(
builder: (context, sessionProvider, _) {
return Column(
children: [
// sessions list
Expanded(
child: ListView.builder(
itemCount: sessionProvider.sessions.length,
itemBuilder: (context, index) {
final session = sessionProvider.sessions[index];
final isSelected =
sessionProvider.currentSessionId == session.id;
return GestureDetector(
onTap: () async {
await sessionProvider.loadSession(session.id);
if (context.mounted) {
final chatProvider =
Provider.of<ChatProvider>(context, listen: false);
chatProvider.setConversation(
sessionProvider.getConversationHistory(),
);
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFFEFF6FF)
: Colors.transparent,
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
session.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
),
),
Text(
"${session.messageCount} msgs",
style: const TextStyle(
fontSize: 12,
color: Color(0xFF94A3B8),
),
),
],
),
),
);
},
),
),
// new session button
Padding(
padding: const EdgeInsets.all(12),
child: PrimaryButton(
onPressed: () async {
await sessionProvider.createNewSession();
if (context.mounted) {
final chatProvider =
Provider.of<ChatProvider>(context, listen: false);
chatProvider.setConversation(
sessionProvider.getConversationHistory(),
);
}
},
child: const Text("+ New"),
),
),
],
);
},
);
}
}