Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
138 changes: 138 additions & 0 deletions packages/stac_gen_ui/README.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions packages/stac_gen_ui/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include: package:flutter_lints/flutter.yaml

analyzer:
exclude:
- lib/**.g.dart
40 changes: 40 additions & 0 deletions packages/stac_gen_ui/lib/src/models/stac_custom_widget_schema.dart
Original file line number Diff line number Diff line change
@@ -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;
}
96 changes: 96 additions & 0 deletions packages/stac_gen_ui/lib/src/models/stac_gen_ui_config.dart
Original file line number Diff line number Diff line change
@@ -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<StacCustomWidgetSchema> _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<void> initialize({
required String apiKey,
String? model,
int? maxTokens,
StacOptions? options,
List<StacParser> parsers = const [],
List<StacActionParser> actionParsers = const [],
List<StacCustomWidgetSchema> 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<StacCustomWidgetSchema> get customWidgets => _customWidgets;
}
69 changes: 69 additions & 0 deletions packages/stac_gen_ui/lib/src/models/stac_gen_ui_model.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> json) =>
_$StacGenUiModelFromJson(json);

/// Converts this [StacGenUiModel] instance to a JSON map.
@override
Map<String, dynamic> toJson() => _$StacGenUiModelToJson(this);
}
32 changes: 32 additions & 0 deletions packages/stac_gen_ui/lib/src/models/stac_gen_ui_model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading