diff --git a/melos.yaml b/melos.yaml index d5ad2aa3..80b9a5ed 100644 --- a/melos.yaml +++ b/melos.yaml @@ -18,7 +18,7 @@ scripts: - melos exec --scope="stac_core" -- "dart run build_runner build --delete-conflicting-outputs" - melos exec --scope="stac" -- "dart run build_runner build --delete-conflicting-outputs" - melos exec --scope="stac_cli" -- "dart run build_runner build --delete-conflicting-outputs" - - melos exec --scope="counter_example" --scope="stac_gallery" --scope="stac_webview" -- "dart run build_runner build --delete-conflicting-outputs" + - melos exec --scope="counter_example" --scope="stac_gallery" --scope="stac_webview" --scope="stac_gen_ui" -- "dart run build_runner build --delete-conflicting-outputs" watch: exec: dart run build_runner watch --delete-conflicting-outputs packageFilters: diff --git a/packages/stac_gen_ui/README.md b/packages/stac_gen_ui/README.md new file mode 100644 index 00000000..ab2a4fd1 --- /dev/null +++ b/packages/stac_gen_ui/README.md @@ -0,0 +1,138 @@ +# Stac Gen UI + +AI-powered UI generation for [Stac](https://stac.dev) using the Claude API. Generate Flutter widgets from natural language prompts at runtime. + +## How It Works + +1. You provide a natural language prompt (e.g., "Create a login form") +2. The package sends it to the Claude API with a Stac widget catalog +3. Claude generates a stac-compatible JSON specification +4. The JSON is rendered as Flutter widgets via Stac's existing parser system + +## Getting Started + +### 1. Add the dependency + +```yaml +dependencies: + stac_gen_ui: + path: ../stac_gen_ui # or from pub.dev when published +``` + +### 2. Initialize + +A single call initializes both Stac Gen UI and the underlying Stac framework — no need to call `Stac.initialize` separately. + +```dart +import 'package:stac_gen_ui/stac_gen_ui.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await StacGenUiConfig.initialize(apiKey: 'your-claude-api-key'); + + runApp(MyApp()); +} +``` + +You can pass all standard Stac options through the same call: + +```dart +await StacGenUiConfig.initialize( + apiKey: 'your-claude-api-key', + options: StacOptions(projectId: 'your-project-id'), + parsers: [MyCustomWidgetParser()], + actionParsers: [MyCustomActionParser()], +); +``` + +### 3. Use in your app + +#### Programmatic usage (in Flutter code) + +```dart +StacGenUiView( + model: StacGenUiModel( + prompt: 'Create a login form with email and password fields and a submit button', + ), +) +``` + +#### JSON spec usage (server-driven) + +```json +{ + "type": "genUi", + "prompt": "Create a user profile card with avatar, name, email, and edit button", + "loaderWidget": { + "type": "center", + "child": { "type": "circularProgressIndicator" } + }, + "errorWidget": { + "type": "center", + "child": { "type": "text", "data": "Failed to generate UI" } + } +} +``` + +#### Direct API usage + +```dart +final jsonSpec = await ClaudeApiService.generateStacJson( + prompt: 'Create a settings page with dark mode toggle', +); +final widget = Stac.fromJson(jsonSpec, context); +``` + +## Configuration + +```dart +await StacGenUiConfig.initialize( + apiKey: 'your-claude-api-key', + model: 'claude-sonnet-4-20250514', // default + maxTokens: 4096, // default +); +``` + +## Custom Widgets + +If you have custom Stac parsers, register them with `customWidgets` so the AI knows how to use them in generated UI: + +```dart +await StacGenUiConfig.initialize( + apiKey: 'your-claude-api-key', + parsers: [const RatingBarParser(), const VideoPlayerParser()], + customWidgets: [ + StacCustomWidgetSchema( + type: 'ratingBar', + description: 'A star rating bar widget', + example: '{"type": "ratingBar", "rating": 4.5, "maxRating": 5, "size": 24, "color": "#FFD700"}', + ), + StacCustomWidgetSchema( + type: 'videoPlayer', + description: 'A video player widget with controls', + example: '{"type": "videoPlayer", "url": "https://example.com/video.mp4", "autoPlay": false}', + ), + ], +); +``` + +The `parsers` parameter registers them with Stac's parser system, while `customWidgets` teaches the AI their JSON structure. Both are needed for the AI to generate and render custom widgets. + +## Custom System Prompt + +Add extra instructions for Claude using `systemPromptExtras`: + +```dart +StacGenUiView( + model: StacGenUiModel( + prompt: 'Create a dashboard', + systemPromptExtras: 'Use brand color #1A73E8 for all primary elements. ' + 'Follow Material Design 3 guidelines.', + ), +) +``` + +## Security Note + +The API key is passed programmatically and stored in memory only. For production apps, consider proxying Claude API calls through your own backend to avoid exposing the key in the client. diff --git a/packages/stac_gen_ui/analysis_options.yaml b/packages/stac_gen_ui/analysis_options.yaml new file mode 100644 index 00000000..e0394e28 --- /dev/null +++ b/packages/stac_gen_ui/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - lib/**.g.dart diff --git a/packages/stac_gen_ui/lib/src/models/stac_custom_widget_schema.dart b/packages/stac_gen_ui/lib/src/models/stac_custom_widget_schema.dart new file mode 100644 index 00000000..4f982d5e --- /dev/null +++ b/packages/stac_gen_ui/lib/src/models/stac_custom_widget_schema.dart @@ -0,0 +1,40 @@ +/// Describes a custom widget type so the AI knows how to use it. +/// +/// When you register custom [StacParser]s with Stac, the AI needs to know +/// about them to include them in generated UI. Use [StacCustomWidgetSchema] +/// to teach the AI your custom widget's JSON structure. +/// +/// ```dart +/// await StacGenUiConfig.initialize( +/// apiKey: 'sk-ant-...', +/// parsers: [const RatingBarParser()], +/// customWidgets: [ +/// StacCustomWidgetSchema( +/// type: 'ratingBar', +/// description: 'A star rating bar widget', +/// example: '{"type": "ratingBar", "rating": 4.5, "maxRating": 5, "size": 24}', +/// ), +/// ], +/// ); +/// ``` +class StacCustomWidgetSchema { + /// Creates a custom widget schema. + /// + /// [type] must match the parser's `type` getter (e.g., 'ratingBar'). + /// [description] is a short human-readable description of what the widget does. + /// [example] is a compact JSON example showing the widget's key properties. + const StacCustomWidgetSchema({ + required this.type, + required this.description, + this.example, + }); + + /// The widget type name, matching the parser's `type` getter. + final String type; + + /// A short description of what the widget does. + final String description; + + /// An optional compact JSON example showing the widget's properties. + final String? example; +} diff --git a/packages/stac_gen_ui/lib/src/models/stac_gen_ui_config.dart b/packages/stac_gen_ui/lib/src/models/stac_gen_ui_config.dart new file mode 100644 index 00000000..0910d117 --- /dev/null +++ b/packages/stac_gen_ui/lib/src/models/stac_gen_ui_config.dart @@ -0,0 +1,96 @@ +import 'package:dio/dio.dart'; +import 'package:stac/stac.dart'; +import 'package:stac_gen_ui/src/models/stac_custom_widget_schema.dart'; +import 'package:stac_gen_ui/src/parsers/stac_gen_ui_parser.dart'; + +/// Configuration and initialization for the Stac Gen UI package. +/// +/// Call [initialize] once at app startup. This also initializes Stac +/// internally, so you do NOT need to call [Stac.initialize] separately. +/// +/// ```dart +/// await StacGenUiConfig.initialize(apiKey: 'sk-ant-...'); +/// ``` +class StacGenUiConfig { + StacGenUiConfig._(); + + static String? _apiKey; + static String _model = 'claude-sonnet-4-20250514'; + static int _maxTokens = 4096; + static List _customWidgets = const []; + + /// Initializes both Stac Gen UI and the underlying Stac framework. + /// + /// This is the single entry point — no need to call [Stac.initialize] + /// separately. The [StacGenUiParser] is automatically registered. + /// + /// [apiKey] is required and must be a valid Claude API key. + /// [model] defaults to `claude-sonnet-4-20250514`. + /// [maxTokens] defaults to 4096. + /// + /// All other parameters are forwarded to [Stac.initialize]: + /// - [options]: Stac Cloud project configuration. + /// - [parsers]: Additional custom widget parsers (genUi is added automatically). + /// - [actionParsers]: Custom action parsers. + /// - [customWidgets]: Descriptions of custom widgets so the AI can use them. + /// - [dio]: Custom Dio instance for Stac network requests. + /// - [override]: If `true`, allows re-initialization. + /// - [showErrorWidgets]: Show error widgets on parse failure (default: true). + /// - [logStackTraces]: Log stack traces for debugging (default: true). + /// - [errorWidgetBuilder]: Custom builder for error widgets. + /// - [cacheConfig]: Global cache configuration. + static Future initialize({ + required String apiKey, + String? model, + int? maxTokens, + StacOptions? options, + List parsers = const [], + List actionParsers = const [], + List customWidgets = const [], + Dio? dio, + bool override = false, + bool showErrorWidgets = true, + bool logStackTraces = true, + StacErrorWidgetBuilder? errorWidgetBuilder, + StacCacheConfig? cacheConfig, + }) async { + _apiKey = apiKey; + if (model != null) _model = model; + if (maxTokens != null) _maxTokens = maxTokens; + _customWidgets = customWidgets; + + await Stac.initialize( + options: options, + parsers: [const StacGenUiParser(), ...parsers], + actionParsers: actionParsers, + dio: dio, + override: override, + showErrorWidgets: showErrorWidgets, + logStackTraces: logStackTraces, + errorWidgetBuilder: errorWidgetBuilder, + cacheConfig: cacheConfig, + ); + } + + /// The Claude API key. + /// + /// Throws if not initialized. + static String get apiKey { + if (_apiKey == null) { + throw StateError( + 'StacGenUiConfig has not been initialized. ' + 'Call StacGenUiConfig.initialize(apiKey: ...) first.', + ); + } + return _apiKey!; + } + + /// The Claude model to use for generation. + static String get model => _model; + + /// The maximum number of tokens for the Claude response. + static int get maxTokens => _maxTokens; + + /// Custom widget schemas registered during initialization. + static List get customWidgets => _customWidgets; +} diff --git a/packages/stac_gen_ui/lib/src/models/stac_gen_ui_model.dart b/packages/stac_gen_ui/lib/src/models/stac_gen_ui_model.dart new file mode 100644 index 00000000..48ff75ae --- /dev/null +++ b/packages/stac_gen_ui/lib/src/models/stac_gen_ui_model.dart @@ -0,0 +1,69 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:stac_core/core/stac_widget.dart'; + +part 'stac_gen_ui_model.g.dart'; + +/// A Stac model for generating UI from a natural language prompt using Claude AI. +/// +/// This widget sends the [prompt] to the Claude API, which generates a +/// stac-compatible JSON spec. The generated JSON is then rendered as +/// Flutter widgets via the existing Stac parser system. +/// +/// ```dart +/// StacGenUiModel( +/// prompt: 'Create a login form with email and password fields', +/// loaderWidget: StacWidget.fromJson({ +/// 'type': 'center', +/// 'child': {'type': 'circularProgressIndicator'}, +/// }), +/// ) +/// ``` +/// +/// ```json +/// { +/// "type": "genUi", +/// "prompt": "Create a login form with email and password fields", +/// "loaderWidget": { +/// "type": "center", +/// "child": {"type": "circularProgressIndicator"} +/// }, +/// "errorWidget": { +/// "type": "center", +/// "child": {"type": "text", "data": "Failed to generate UI"} +/// } +/// } +/// ``` +@JsonSerializable() +class StacGenUiModel extends StacWidget { + /// Creates a [StacGenUiModel] with the given properties. + const StacGenUiModel({ + required this.prompt, + this.loaderWidget, + this.errorWidget, + this.systemPromptExtras, + }); + + /// The natural language prompt describing the UI to generate. + final String prompt; + + /// Optional StacWidget to display while the AI is generating the UI. + final StacWidget? loaderWidget; + + /// Optional StacWidget to display if generation fails. + final StacWidget? errorWidget; + + /// Optional additional instructions to include in the Claude system prompt. + final String? systemPromptExtras; + + /// Widget type identifier. + @override + String get type => 'genUi'; + + /// Creates a [StacGenUiModel] from a JSON map. + factory StacGenUiModel.fromJson(Map json) => + _$StacGenUiModelFromJson(json); + + /// Converts this [StacGenUiModel] instance to a JSON map. + @override + Map toJson() => _$StacGenUiModelToJson(this); +} diff --git a/packages/stac_gen_ui/lib/src/models/stac_gen_ui_model.g.dart b/packages/stac_gen_ui/lib/src/models/stac_gen_ui_model.g.dart new file mode 100644 index 00000000..7d55fd89 --- /dev/null +++ b/packages/stac_gen_ui/lib/src/models/stac_gen_ui_model.g.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'stac_gen_ui_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +StacGenUiModel _$StacGenUiModelFromJson(Map json) => + StacGenUiModel( + prompt: json['prompt'] as String, + loaderWidget: json['loaderWidget'] == null + ? null + : StacWidget.fromJson( + json['loaderWidget'] as Map, + ), + errorWidget: json['errorWidget'] == null + ? null + : StacWidget.fromJson( + json['errorWidget'] as Map, + ), + systemPromptExtras: json['systemPromptExtras'] as String?, + ); + +Map _$StacGenUiModelToJson(StacGenUiModel instance) => + { + 'type': instance.type, + 'prompt': instance.prompt, + 'loaderWidget': instance.loaderWidget?.toJson(), + 'errorWidget': instance.errorWidget?.toJson(), + 'systemPromptExtras': instance.systemPromptExtras, + }; diff --git a/packages/stac_gen_ui/lib/src/parsers/stac_gen_ui_parser.dart b/packages/stac_gen_ui/lib/src/parsers/stac_gen_ui_parser.dart new file mode 100644 index 00000000..62bfdb80 --- /dev/null +++ b/packages/stac_gen_ui/lib/src/parsers/stac_gen_ui_parser.dart @@ -0,0 +1,38 @@ +import 'package:flutter/widgets.dart'; +import 'package:stac_framework/stac_framework.dart'; +import 'package:stac_gen_ui/src/models/stac_gen_ui_model.dart'; +import 'package:stac_gen_ui/src/widgets/stac_gen_ui_view.dart'; + +/// Parser for the `genUi` widget type. +/// +/// Register this parser during Stac initialization to enable AI-generated +/// UI in your stac JSON specs: +/// +/// ```dart +/// await Stac.initialize( +/// parsers: [const StacGenUiParser()], +/// ); +/// ``` +/// +/// Then use in JSON specs: +/// ```json +/// { +/// "type": "genUi", +/// "prompt": "Create a settings page with dark mode toggle" +/// } +/// ``` +class StacGenUiParser extends StacParser { + const StacGenUiParser(); + + @override + String get type => 'genUi'; + + @override + StacGenUiModel getModel(Map json) => + StacGenUiModel.fromJson(json); + + @override + Widget parse(BuildContext context, StacGenUiModel model) { + return StacGenUiView(model: model); + } +} diff --git a/packages/stac_gen_ui/lib/src/services/claude_api_service.dart b/packages/stac_gen_ui/lib/src/services/claude_api_service.dart new file mode 100644 index 00000000..66fbab67 --- /dev/null +++ b/packages/stac_gen_ui/lib/src/services/claude_api_service.dart @@ -0,0 +1,123 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:stac_gen_ui/src/models/stac_gen_ui_config.dart'; +import 'package:stac_gen_ui/src/services/stac_schema_provider.dart'; + +/// Service for communicating with the Claude API to generate Stac JSON specs. +/// +/// Uses Claude's tool_use feature to guarantee valid JSON output. +/// +/// ```dart +/// final json = await ClaudeApiService.generateStacJson( +/// prompt: 'Create a login form', +/// ); +/// ``` +class ClaudeApiService { + ClaudeApiService._(); + + static const String _baseUrl = 'https://api.anthropic.com/v1/messages'; + + static const Map _tool = { + 'name': 'generate_ui', + 'description': 'Generate a Stac UI JSON specification', + 'input_schema': { + 'type': 'object', + 'properties': { + 'stac_json': { + 'type': 'object', + 'description': 'The complete Stac widget JSON specification', + }, + }, + 'required': ['stac_json'], + }, + }; + + /// Generates a Stac JSON specification from a natural language prompt. + /// + /// Returns a [Map] representing the generated Stac widget + /// tree that can be passed to [Stac.fromJson]. + /// + /// Throws [DioException] on network errors or [FormatException] if the + /// response cannot be parsed. + static Future> generateStacJson({ + required String prompt, + String? systemPromptExtras, + }) async { + final dio = Dio(); + final systemPrompt = StacSchemaProvider.buildSystemPrompt( + extras: systemPromptExtras, + ); + + final response = await dio.post( + _baseUrl, + options: Options( + headers: { + 'x-api-key': StacGenUiConfig.apiKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }, + ), + data: { + 'model': StacGenUiConfig.model, + 'max_tokens': StacGenUiConfig.maxTokens, + 'system': systemPrompt, + 'tools': [_tool], + 'tool_choice': {'type': 'tool', 'name': 'generate_ui'}, + 'messages': [ + {'role': 'user', 'content': prompt}, + ], + }, + ); + + return _parseResponse(response.data); + } + + /// Parses the Claude API response to extract the generated Stac JSON. + /// + /// First attempts to extract from tool_use blocks, then falls back to + /// text content parsing. + static Map _parseResponse(dynamic responseData) { + final content = responseData['content'] as List; + + // Look for tool_use block + for (final block in content) { + if (block['type'] == 'tool_use' && block['name'] == 'generate_ui') { + final input = block['input'] as Map; + final stacJson = input['stac_json']; + if (stacJson is Map) { + return stacJson; + } + } + } + + // Fallback: look for text content and try to extract JSON + for (final block in content) { + if (block['type'] == 'text') { + final text = block['text'] as String; + return _extractJsonFromText(text); + } + } + + throw const FormatException( + 'Could not extract Stac JSON from Claude response', + ); + } + + /// Extracts JSON from text content, handling markdown code fences. + static Map _extractJsonFromText(String text) { + // Try to extract from markdown code fences + final fenceRegex = RegExp(r'```(?:json)?\s*([\s\S]*?)\s*```'); + final match = fenceRegex.firstMatch(text); + final jsonString = match != null ? match.group(1)! : text.trim(); + + final decoded = jsonDecode(jsonString); + if (decoded is Map) { + return decoded; + } + + throw const FormatException( + 'Claude response did not contain a valid JSON object', + ); + } +} diff --git a/packages/stac_gen_ui/lib/src/services/stac_schema_provider.dart b/packages/stac_gen_ui/lib/src/services/stac_schema_provider.dart new file mode 100644 index 00000000..75d5efca --- /dev/null +++ b/packages/stac_gen_ui/lib/src/services/stac_schema_provider.dart @@ -0,0 +1,104 @@ +import 'package:stac_gen_ui/src/models/stac_custom_widget_schema.dart'; +import 'package:stac_gen_ui/src/models/stac_gen_ui_config.dart'; + +/// Builds the system prompt for Claude with the Stac widget catalog. +/// +/// Uses a minimal prompt approach (~2000 tokens) with widget type names +/// and a few representative examples to keep API costs low. +class StacSchemaProvider { + StacSchemaProvider._(); + + /// Builds the complete system prompt for Claude. + /// + /// Includes built-in widget types, key rules, examples, and any + /// [StacCustomWidgetSchema]s registered via [StacGenUiConfig.initialize]. + static String buildSystemPrompt({String? extras}) { + final buffer = StringBuffer(); + + buffer.writeln(_role); + buffer.writeln(); + buffer.writeln(_widgetTypes); + + final customWidgets = StacGenUiConfig.customWidgets; + if (customWidgets.isNotEmpty) { + buffer.writeln(); + buffer.writeln(_buildCustomWidgetSection(customWidgets)); + } + + buffer.writeln(); + buffer.writeln(_rules); + buffer.writeln(); + buffer.writeln(_examples); + + if (customWidgets.isNotEmpty) { + buffer.writeln(); + buffer.writeln(_buildCustomWidgetExamples(customWidgets)); + } + + if (extras != null && extras.isNotEmpty) { + buffer.writeln(); + buffer.writeln('## Additional Instructions'); + buffer.writeln(extras); + } + + return buffer.toString(); + } + + static String _buildCustomWidgetSection( + List widgets, + ) { + final buffer = StringBuffer(); + buffer.writeln('## Custom Widget Types'); + for (final widget in widgets) { + buffer.writeln('- ${widget.type}: ${widget.description}'); + } + return buffer.toString(); + } + + static String _buildCustomWidgetExamples( + List widgets, + ) { + final withExamples = widgets.where((w) => w.example != null).toList(); + if (withExamples.isEmpty) return ''; + + final buffer = StringBuffer(); + buffer.writeln('## Custom Widget Examples'); + for (final widget in withExamples) { + buffer.writeln(); + buffer.writeln('### ${widget.type}'); + buffer.writeln(widget.example); + } + return buffer.toString(); + } + + static const String _role = + 'You are a UI generator for the Stac Server-Driven UI framework for Flutter. ' + 'Generate a JSON specification using the available widget types below. ' + 'The JSON will be rendered as Flutter widgets at runtime.'; + + static const String _widgetTypes = '''## Available Widget Types +alertDialog, align, appBar, aspectRatio, autocomplete, backdropFilter, badge, bottomNavigationBar, bottomNavigationView, card, carouselView, center, checkBox, chip, clipOval, clipRRect, circleAvatar, circularProgressIndicator, coloredBox, column, conditional, container, drawer, dropdownMenu, customScrollView, defaultBottomNavigationController, defaultTabController, divider, dynamicView, elevatedButton, expanded, filledButton, fittedBox, flexible, floatingActionButton, form, fractionallySizedBox, gestureDetector, gridView, hero, icon, iconButton, image, inkWell, limitedBox, linearProgressIndicator, listTile, listView, networkWidget, opacity, outlinedButton, padding, pageView, placeholder, positioned, radio, radioGroup, refreshIndicator, row, safeArea, scaffold, selectableText, setValue, singleChildScrollView, sizedBox, slider, sliverAppBar, sliverGrid, sliverFillRemaining, sliverList, sliverVisibility, sliverOpacity, sliverSafeArea, sliverPadding, sliverToBoxAdapter, spacer, stack, tab, tabBar, tabBarView, table, tableCell, text, textButton, textField, textFormField, tooltip, wrap, visibility, verticalDivider'''; + + static const String _rules = '''## Key Rules +- Every widget object MUST have a "type" field (camelCase, matching the types above) +- Single child: use "child" key. Multiple children: use "children" key (array) +- Colors: hex strings like "#FF2196F3" (ARGB) or "#2196F3" (RGB), or theme colors like "primary", "primary@50" +- Padding/margin: {"left": n, "top": n, "right": n, "bottom": n} or a single number for uniform padding +- Text styles: {"fontSize": n, "fontWeight": "w600", "color": "#HEX"} +- Icons: {"type": "icon", "iconType": "material", "icon": "icon_name", "size": n} +- Buttons have "onPressed" for actions: {"actionType": "navigate", ...} or {} for no-op +- Use "scaffold" as root for full-screen layouts +- Use "form" with "textFormField" for input forms +- Use "sizedBox" for spacing between widgets'''; + + static const String _examples = r'''## Examples + +### Login Form +{"type":"scaffold","appBar":{"type":"appBar","title":{"type":"text","data":"Sign In"}},"body":{"type":"padding","padding":{"left":16,"top":16,"right":16,"bottom":16},"child":{"type":"singleChildScrollView","child":{"type":"form","child":{"type":"column","children":[{"type":"textFormField","id":"email","decoration":{"hintText":"Email"},"keyboardType":"emailAddress","textInputAction":"next"},{"type":"sizedBox","height":16},{"type":"textFormField","id":"password","decoration":{"hintText":"Password"},"keyboardType":"visiblePassword","textInputAction":"done"},{"type":"sizedBox","height":24},{"type":"elevatedButton","child":{"type":"text","data":"Sign In"},"style":{"backgroundColor":"primary","foregroundColor":"#ffffff"},"onPressed":{}}]}}}}} + +### Card with ListTile +{"type":"card","elevation":4,"margin":{"top":8,"bottom":8,"left":16,"right":16},"child":{"type":"listTile","leading":{"type":"circleAvatar","child":{"type":"icon","iconType":"material","icon":"person","size":24}},"title":{"type":"text","data":"John Doe","style":{"fontSize":18,"fontWeight":"w600"}},"subtitle":{"type":"text","data":"Software Engineer","style":{"fontSize":14,"color":"#666666"}},"trailing":{"type":"icon","iconType":"material","icon":"chevron_right","size":24}}} + +### Grid Layout +{"type":"padding","padding":{"left":10,"top":10,"right":10,"bottom":10},"child":{"type":"gridView","crossAxisCount":2,"crossAxisSpacing":10,"mainAxisSpacing":10,"children":[{"type":"container","decoration":{"type":"boxDecoration","color":"#FFCDD2","borderRadius":{"all":8}},"child":{"type":"center","child":{"type":"text","data":"Item 1"}}},{"type":"container","decoration":{"type":"boxDecoration","color":"#C8E6C9","borderRadius":{"all":8}},"child":{"type":"center","child":{"type":"text","data":"Item 2"}}}]}}'''; +} diff --git a/packages/stac_gen_ui/lib/src/widgets/stac_gen_ui_view.dart b/packages/stac_gen_ui/lib/src/widgets/stac_gen_ui_view.dart new file mode 100644 index 00000000..bbdb5866 --- /dev/null +++ b/packages/stac_gen_ui/lib/src/widgets/stac_gen_ui_view.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:stac/stac.dart'; +import 'package:stac_gen_ui/src/models/stac_gen_ui_model.dart'; +import 'package:stac_gen_ui/src/services/claude_api_service.dart'; + +/// A Flutter widget that generates and renders Stac UI from a natural +/// language prompt using the Claude API. +/// +/// Used both by [StacGenUiParser] and directly in Flutter widget trees: +/// +/// ```dart +/// StacGenUiView( +/// model: StacGenUiModel( +/// prompt: 'Create a login form with email and password', +/// ), +/// ) +/// ``` +class StacGenUiView extends StatefulWidget { + /// Creates a [StacGenUiView] with the given model. + const StacGenUiView({super.key, required this.model}); + + /// The model containing the prompt and optional loader/error widgets. + final StacGenUiModel model; + + @override + State createState() => _StacGenUiViewState(); +} + +class _StacGenUiViewState extends State { + late Future> _generationFuture; + + @override + void initState() { + super.initState(); + _generationFuture = ClaudeApiService.generateStacJson( + prompt: widget.model.prompt, + systemPromptExtras: widget.model.systemPromptExtras, + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _generationFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return _buildLoaderWidget(context); + } + + if (snapshot.hasError) { + return _buildErrorWidget(context, snapshot.error); + } + + if (snapshot.hasData) { + return Stac.fromJson(snapshot.data!, context) ?? const SizedBox(); + } + + return const SizedBox(); + }, + ); + } + + Widget _buildLoaderWidget(BuildContext context) { + final loaderJson = widget.model.loaderWidget?.toJson(); + if (loaderJson != null) { + return Stac.fromJson(loaderJson, context) ?? + const Center(child: CircularProgressIndicator()); + } + return const Center(child: CircularProgressIndicator()); + } + + Widget _buildErrorWidget(BuildContext context, Object? error) { + final errorJson = widget.model.errorWidget?.toJson(); + if (errorJson != null) { + return Stac.fromJson(errorJson, context) ?? + Center(child: Text('Generation failed: $error')); + } + return Center(child: Text('Generation failed: $error')); + } +} diff --git a/packages/stac_gen_ui/lib/stac_gen_ui.dart b/packages/stac_gen_ui/lib/stac_gen_ui.dart new file mode 100644 index 00000000..4ed76be5 --- /dev/null +++ b/packages/stac_gen_ui/lib/stac_gen_ui.dart @@ -0,0 +1,9 @@ +library; + +export 'src/models/stac_custom_widget_schema.dart'; +export 'src/models/stac_gen_ui_config.dart'; +export 'src/models/stac_gen_ui_model.dart'; +export 'src/parsers/stac_gen_ui_parser.dart'; +export 'src/services/claude_api_service.dart'; +export 'src/services/stac_schema_provider.dart'; +export 'src/widgets/stac_gen_ui_view.dart'; diff --git a/packages/stac_gen_ui/pubspec.yaml b/packages/stac_gen_ui/pubspec.yaml new file mode 100644 index 00000000..d9ce7f2c --- /dev/null +++ b/packages/stac_gen_ui/pubspec.yaml @@ -0,0 +1,34 @@ +name: stac_gen_ui +description: AI-powered UI generation for Stac using Claude API. Generate Flutter widgets from natural language prompts at runtime. +version: 0.1.0 +homepage: https://github.com/StacDev/stac + +environment: + sdk: ">=3.1.0 <4.0.0" + flutter: ">=1.17.0" + +topics: + - ui + - widget + - server-driven-ui + - dynamic-widgets + - ai-generated-ui + +dependencies: + flutter: + sdk: flutter + dio: ^5.0.0 + json_annotation: ^4.10.0 + stac: + path: ../stac + stac_core: + path: ../stac_core + stac_framework: + path: ../stac_framework + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + build_runner: ^2.10.1 + json_serializable: ^6.11.1