This document describes the system architecture, design decisions, and technical structure of PromptResponse.
┌─────────────────────────────────────────────────────────┐
│ PromptResponse │
│ │
│ ┌───────────────┐ ┌──────────────────┐ │
│ │ Desktop UI │ │ Mobile UI │ │
│ │ (AvaloniaUI) │ │ (Future) │ │
│ └───────┬───────┘ └────────┬─────────┘ │
│ │ │ │
│ └───────────┬───────────────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ PromptResponse.Core │ │
│ │ │ │
│ │ ├── Models │ │
│ │ ├── Serialization │ │
│ │ ├── Validation │ │
│ │ └── Logging │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ File System (.apr) │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────┘
- Core Library: Platform-agnostic business logic
- UI Layer: Platform-specific presentation
- Clear boundaries: No UI code in Core, no business logic in UI
View (XAML) ←→ ViewModel ←→ Model (Core)
- Views: XAML-based UI components
- ViewModels: UI state and commands
- Models: Domain objects from Core library
- Core library has no dependencies on UI
- UI depends on Core through interfaces
- Services injected via dependency injection
Purpose: Platform-agnostic form processing logic
PromptResponse.Core/
├── Models/
│ ├── AprDocument.cs # Root document
│ ├── DocumentType.cs # Enum: Template/FilledForm
│ ├── Metadata.cs # Document metadata
│ ├── Section.cs # Section grouping (recursive)
│ ├── Prompt.cs # Individual prompt
│ ├── PromptHints.cs # Type hints, suggestions
│ └── ResponseMetadata.cs # Response tracking
├── Serialization/
│ ├── IAprSerializer.cs # Serialization interface
│ ├── AprJsonSerializer.cs # JSON implementation
│ └── SerializationException.cs
├── Validation/
│ ├── IValidator.cs
│ ├── DocumentValidator.cs # Validate structure
│ └── DataTypeValidator.cs # Validate type hints
└── Logging/
└── CoreLogger.cs # Logging utilities
Dependencies:
System.Text.Json(built-in)Microsoft.Extensions.Logging.Abstractions
Purpose: Cross-platform desktop UI (Linux, Windows, macOS)
PromptResponse.Desktop/
├── ViewModels/
│ ├── MainWindowViewModel.cs
│ ├── TemplateEditorViewModel.cs
│ ├── FormFillingViewModel.cs
│ ├── SectionViewModel.cs
│ ├── PromptViewModel.cs
│ └── ViewModelBase.cs
├── Views/
│ ├── MainWindow.axaml
│ ├── TemplateEditorView.axaml
│ ├── FormFillingView.axaml
│ ├── SectionView.axaml
│ └── PromptView.axaml
├── Services/
│ ├── IFileService.cs
│ ├── FileService.cs
│ ├── IModeDetectionService.cs
│ ├── ModeDetectionService.cs
│ └── IDialogService.cs
├── Converters/
│ └── DataTypeToWidgetConverter.cs
├── App.axaml.cs
└── Program.cs
Dependencies:
Avalonia(MIT License)Avalonia.Desktop(MIT License)PromptResponse.Core(local)
PromptResponse.Core.Tests/
├── Models/
│ ├── AprDocumentTests.cs
│ ├── SectionTests.cs
│ ├── PromptTests.cs
│ └── ...
├── Serialization/
│ ├── AprJsonSerializerTests.cs
│ └── DeserializationTests.cs
├── Validation/
│ └── DocumentValidatorTests.cs
└── Integration/
└── EndToEndTests.cs
PromptResponse.Desktop.Tests/
├── ViewModels/
│ ├── TemplateEditorViewModelTests.cs
│ └── FormFillingViewModelTests.cs
└── Services/
└── FileServiceTests.cs
/// <summary>
/// Root document representing an APR file.
/// </summary>
public class AprDocument
{
public string Version { get; set; } = "1.0";
public DocumentType DocumentType { get; set; }
public Metadata Metadata { get; set; } = new();
public List<Section> Sections { get; set; } = new();
}/// <summary>
/// Grouping of prompts in a document, can be nested to unlimited depth.
/// </summary>
public class Section
{
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public List<Section> Sections { get; set; } = new();
public List<Prompt> Prompts { get; set; } = new();
}/// <summary>
/// Individual question/field in the form.
/// </summary>
public class Prompt
{
public string Id { get; set; } = string.Empty;
public string Label { get; set; } = string.Empty;
public string Response { get; set; } = string.Empty;
public PromptHints Hints { get; set; } = new();
public ResponseMetadata ResponseMetadata { get; set; } = new();
}Design: Strategy pattern for multiple formats
public interface IAprSerializer
{
/// <summary>
/// Serializes an APR document to a string.
/// </summary>
string Serialize(AprDocument document);
/// <summary>
/// Deserializes a string to an APR document.
/// </summary>
AprDocument Deserialize(string content);
/// <summary>
/// Asynchronously deserializes a stream to an APR document.
/// </summary>
Task<AprDocument> DeserializeAsync(Stream stream);
}Implementation: JSON using System.Text.Json
Two-tier validation:
- Structural: Required fields, valid structure
- Semantic: Type hints, suggested values format
public interface IValidator<T>
{
ValidationResult Validate(T item);
}
public class ValidationResult
{
public bool IsValid { get; set; }
public List<ValidationError> Errors { get; set; } = new();
}Responsibilities:
- Load/save APR files
- Detect document type
- Handle file I/O errors
public interface IFileService
{
Task<AprDocument> LoadAsync(string filePath);
Task SaveAsync(string filePath, AprDocument document);
DocumentType DetectDocumentType(string filePath);
}Base ViewModel:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}Example ViewModel:
public class PromptViewModel : ViewModelBase
{
private string _response = string.Empty;
public Prompt Model { get; }
public string Response
{
get => _response;
set
{
if (_response != value)
{
_response = value;
Model.Response = value;
OnPropertyChanged();
}
}
}
public ICommand ClearCommand { get; }
}XAML Binding:
<TextBox Text="{Binding Response, Mode=TwoWay}"
Watermark="{Binding Hints.Placeholder}"
AutoCompleteSource="{Binding Hints.SuggestedValues}" />App Start
↓
MainWindow
├─→ New Template → TemplateEditorView
├─→ Open File → ModeDetectionService
│ ├─→ Template
│ │ ├─→ Edit Template → TemplateEditorView
│ │ └─→ Fill Form → FormFillingView
│ └─→ FilledForm → FormFillingView
└─→ Recent Files → (above flows)
public class ModeDetectionService : IModeDetectionService
{
public async Task<DocumentMode> DetermineMode(string filePath)
{
var document = await _fileService.LoadAsync(filePath);
if (document.DocumentType == DocumentType.FilledForm)
{
// Go directly to filling mode
return DocumentMode.Filling;
}
// Template - ask user
var choice = await _dialogService.ShowChoice(
"Open Template",
"Would you like to edit the template or fill it out?",
new[] { "Fill Out Form", "Edit Template" }
);
return choice == 0 ? DocumentMode.Filling : DocumentMode.Editing;
}
}User clicks Open
↓
FileService.LoadAsync(path)
↓
AprJsonSerializer.DeserializeAsync(stream)
↓
DocumentValidator.Validate(document)
↓
ModeDetectionService.DetermineMode(path)
↓
Navigate to appropriate View
↓
ViewModel wraps Models
↓
View binds to ViewModel
User clicks Save
↓
ViewModel updates Models
↓
DocumentValidator.Validate(document)
↓
AprJsonSerializer.Serialize(document)
↓
FileService.SaveAsync(path, document)
↓
Update UI with success/error
User types in TextBox
↓
Two-way binding updates ViewModel.Response
↓
ViewModel updates Prompt.Response
↓
ResponseMetadata.LastModified = DateTime.UtcNow
↓
PropertyChanged event raised
↓
UI updated (if needed)
// Program.cs
var services = new ServiceCollection();
// Core services
services.AddSingleton<IAprSerializer, AprJsonSerializer>();
services.AddSingleton<IValidator<AprDocument>, DocumentValidator>();
// Desktop services
services.AddSingleton<IFileService, FileService>();
services.AddSingleton<IModeDetectionService, ModeDetectionService>();
services.AddSingleton<IDialogService, DialogService>();
// ViewModels
services.AddTransient<MainWindowViewModel>();
services.AddTransient<TemplateEditorViewModel>();
services.AddTransient<FormFillingViewModel>();
// Logging
services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});- Core Layer: Throw specific exceptions
- Service Layer: Catch, log, and re-throw or wrap
- ViewModel Layer: Catch, show user-friendly message
- View Layer: Display error UI
// Core
public class SerializationException : Exception { }
public class ValidationException : Exception { }
// Services
public class FileAccessException : Exception { }
public class UnsupportedVersionException : Exception { }try
{
_logger.LogInformation("Loading document from {FilePath}", filePath);
var document = await _serializer.DeserializeAsync(stream);
_logger.LogDebug("Document loaded: {DocumentType}", document.DocumentType);
return document;
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to deserialize APR document");
throw new SerializationException("Invalid APR file format", ex);
}- Lazy loading: Load sections on-demand
- Virtualization: Use virtual scrolling for 1000+ prompts
- Async I/O: All file operations async
- Streaming: Stream large JSON files
- Dispose patterns: Implement IDisposable for file handles
- Weak references: For cached data
- Object pooling: For frequently created objects
- Validate all file inputs
- Sanitize responses for display
- Prevent path traversal in file operations
- APR files are pure data
- No scripting or executable code
- Safe to open untrusted files
- Test each class in isolation
- Mock dependencies
- Cover edge cases and error conditions
- Test component interactions
- Test serialization round-trips
- Test file I/O operations
- Test ViewModel logic
- Test data binding
- Manual testing for UI/UX
PromptResponse supports multiple form submission workflows:
User fills form → Save locally → Manual send (email, file transfer)
- No infrastructure required
- Maximum privacy
- Simple workflow
- Works offline
Template with S3 config → User fills form → Direct POST to S3 → No server needed
Architecture:
┌──────────────────┐
│ Template Author │
│ (Has AWS creds) │
└────────┬─────────┘
│
│ 1. Generate pre-signed POST policy
│ (AWSAccessKeyId, policy, signature)
│
▼
┌─────────────────────────────────┐
│ APR Template with │
│ submissionConfig in metadata │
└────────┬────────────────────────┘
│
│ 2. Distribute template
│
▼
┌─────────────────┐
│ User fills out │
│ form │
└────────┬────────┘
│
│ 3. Click "Submit"
│
▼
┌─────────────────────────────────┐
│ PromptResponse Desktop/CLI │
│ reads submissionConfig │
└────────┬────────────────────────┘
│
│ 4. HTTPS POST directly to S3
│ with policy + signature
│
▼
┌─────────────────┐
│ S3 validates │
│ signature & │
│ accepts file │
└─────────────────┘
Benefits:
- No server-side code required
- No serverless functions needed
- Direct client-to-S3 upload
- S3 handles validation via policy
- Can combine with APR digital signatures
- Template signature proves authenticity
- S3 policy controls upload authorization
Security Model:
- Pre-signed POST policy includes expiration (typically 7 days max)
- Policy restricts file size, content type, key prefix
- Access key ID visible in template (not secret key)
- Signature computed from secret key but doesn't expose it
- Anyone with template can upload (until expiration)
- S3 bucket can have additional policies (encryption, lifecycle)
Implementation Considerations:
- Check policy expiration before submitting
- Handle CORS requirements (S3 bucket configuration)
- Provide fallback to manual save if submission fails
- Show policy expiration warning in UI
- Optional: Support policy refresh from URL
Template with webhook URL → User fills form → POST to webhook
- Simple HTTP POST to custom endpoint
- Template includes webhook URL
- Implementation handles auth, validation, storage
- Share Core library
- Platform-specific UI (Avalonia Mobile or MAUI)
- Mobile-optimized input widgets
- Custom validators
- Custom data types
- Export formats
- Custom submission handlers
- Conflict resolution for concurrent edits
- Version control integration
- Cloud sync
Office workers constantly need to create and fill forms, but existing tools are painful:
- PDF Forms: Difficult to create, weird rendering issues, hard to extract data
- Word Tables/Forms: Break easily, formatting nightmares, no structured data export
- Custom CRUD Apps: Same form logic rebuilt over and over for every project
PromptResponse solves these by providing:
- Simple form creation without layout hassles
- Direct database import (it's just JSON)
- Built-in submission to S3/webhooks (no custom server code)
- Easy programmatic filling for automation
- Human-readable
- Wide tool support
- Easy to parse and import into databases
- Interoperable with any language or system
- Maximum flexibility
- No data loss from type coercion
- Portable across systems
- Accessibility
- Easy database import (no type conversion issues)
- Separation of content and presentation
- Dynamic UI adaptation
- Accessibility
- Portability
- Focus on data, not formatting headaches
- Clear intent (template vs filled)
- Different workflows
- Prevent accidental template modification
- Eliminates need to build custom CRUD apps for every form
- S3 pre-signed POST means no server-side code needed
- Template publishers control where submissions go
- Users just click "Submit" - no configuration needed
- FILE_FORMAT.md - APR format specification
- DEVELOPMENT.md - Development guidelines
- Avalonia Documentation
- .NET Documentation