Initial commit - RUSTLE Wails implementation
- Added Go-based Wails application for RUSTLE UCXL browser - Implemented basic application structure and configuration - Added project documentation and setup instructions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
3
rustle/.gitignore
vendored
Normal file
3
rustle/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
build/bin
|
||||
node_modules
|
||||
frontend/dist
|
||||
19
rustle/README.md
Normal file
19
rustle/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# README
|
||||
|
||||
## About
|
||||
|
||||
This is the official Wails React-TS template.
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
|
||||
## Live Development
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
|
||||
## Building
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
438
rustle/app.go
Normal file
438
rustle/app.go
Normal file
@@ -0,0 +1,438 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// Envelope represents a UCXL content envelope
|
||||
type Envelope struct {
|
||||
UCXLURI string `json:"ucxl_uri"`
|
||||
Content Content `json:"content"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Version string `json:"version"`
|
||||
ContentHash string `json:"content_hash"`
|
||||
}
|
||||
|
||||
// Content represents the actual content data
|
||||
type Content struct {
|
||||
Raw string `json:"raw"`
|
||||
ContentType string `json:"content_type"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
// UCXL Address parsing types and functions
|
||||
type UCXLAddress struct {
|
||||
Agent string `json:"agent"`
|
||||
Role string `json:"role"`
|
||||
Project string `json:"project"`
|
||||
Task string `json:"task"`
|
||||
Temporal string `json:"temporal"`
|
||||
Path string `json:"path"`
|
||||
Raw string `json:"raw"`
|
||||
}
|
||||
|
||||
// Regular expression for UCXL address parsing
|
||||
var ucxlAddressPattern = regexp.MustCompile(`^ucxl://([^:]+):([^@]+)@([^:]+):([^/]+)/([^/]+)/?(.*)$`)
|
||||
|
||||
// ParseUCXL parses a UCXL address string
|
||||
func ParseUCXL(address string) (*UCXLAddress, error) {
|
||||
if address == "" {
|
||||
return nil, fmt.Errorf("address cannot be empty")
|
||||
}
|
||||
|
||||
// Normalize the address
|
||||
normalized := strings.TrimSpace(address)
|
||||
if !strings.HasPrefix(strings.ToLower(normalized), "ucxl://") {
|
||||
return nil, fmt.Errorf("address must start with 'ucxl://'")
|
||||
}
|
||||
|
||||
// Use regex for parsing
|
||||
matches := ucxlAddressPattern.FindStringSubmatch(normalized)
|
||||
if matches == nil || len(matches) != 7 {
|
||||
return nil, fmt.Errorf("address format must be 'ucxl://agent:role@project:task/temporal_segment/path'")
|
||||
}
|
||||
|
||||
return &UCXLAddress{
|
||||
Agent: strings.ToLower(strings.TrimSpace(matches[1])),
|
||||
Role: strings.ToLower(strings.TrimSpace(matches[2])),
|
||||
Project: strings.ToLower(strings.TrimSpace(matches[3])),
|
||||
Task: strings.ToLower(strings.TrimSpace(matches[4])),
|
||||
Temporal: strings.TrimSpace(matches[5]),
|
||||
Path: matches[6], // Path can be empty
|
||||
Raw: address,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String returns the canonical string representation
|
||||
func (addr *UCXLAddress) String() string {
|
||||
if addr.Path != "" {
|
||||
return fmt.Sprintf("ucxl://%s:%s@%s:%s/%s/%s", addr.Agent, addr.Role, addr.Project, addr.Task, addr.Temporal, addr.Path)
|
||||
}
|
||||
return fmt.Sprintf("ucxl://%s:%s@%s:%s/%s", addr.Agent, addr.Role, addr.Project, addr.Task, addr.Temporal)
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
|
||||
// Log startup
|
||||
runtime.LogInfo(ctx, "RUSTLE started with BZZZ integration (mock mode)")
|
||||
}
|
||||
|
||||
// GetUCXLContent retrieves content by UCXL address
|
||||
func (a *App) GetUCXLContent(uri string) (map[string]interface{}, error) {
|
||||
runtime.LogInfo(a.ctx, fmt.Sprintf("Fetching UCXL content: %s", uri))
|
||||
|
||||
// Parse the UCXL address
|
||||
addr, err := ParseUCXL(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid UCXL address: %w", err)
|
||||
}
|
||||
|
||||
// For now, return mock content based on the address
|
||||
envelope, err := a.generateMockContent(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate content: %w", err)
|
||||
}
|
||||
|
||||
// Convert to map for JSON serialization
|
||||
result := map[string]interface{}{
|
||||
"ucxl_uri": envelope.UCXLURI,
|
||||
"content": envelope.Content,
|
||||
"metadata": envelope.Metadata,
|
||||
"timestamp": envelope.Timestamp,
|
||||
"version": envelope.Version,
|
||||
"content_hash": envelope.ContentHash,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PostUCXLContent stores content at UCXL address
|
||||
func (a *App) PostUCXLContent(uri string, content string, contentType string, metadata map[string]string) error {
|
||||
runtime.LogInfo(a.ctx, fmt.Sprintf("Storing UCXL content: %s", uri))
|
||||
|
||||
// Parse the UCXL address
|
||||
addr, err := ParseUCXL(uri)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid UCXL address: %w", err)
|
||||
}
|
||||
|
||||
// Create envelope
|
||||
envelope := &Envelope{
|
||||
UCXLURI: addr.String(),
|
||||
Content: Content{
|
||||
Raw: content,
|
||||
ContentType: contentType,
|
||||
Encoding: "utf-8",
|
||||
},
|
||||
Metadata: metadata,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Version: "1.0",
|
||||
ContentHash: fmt.Sprintf("sha256:%x", []byte(content)), // Simple hash for demo
|
||||
}
|
||||
|
||||
// In a real implementation, this would store to the DHT
|
||||
runtime.LogInfo(a.ctx, fmt.Sprintf("Mock stored content: %d bytes", len(content)))
|
||||
|
||||
// For demo purposes, we'll just log the operation
|
||||
envelopeJSON, _ := json.MarshalIndent(envelope, "", " ")
|
||||
runtime.LogInfo(a.ctx, fmt.Sprintf("Stored envelope: %s", string(envelopeJSON)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchUCXLContent searches for content matching a pattern
|
||||
func (a *App) SearchUCXLContent(query string, tags []string, limit int) ([]map[string]interface{}, error) {
|
||||
runtime.LogInfo(a.ctx, fmt.Sprintf("Searching UCXL content: query=%s, tags=%v, limit=%d", query, tags, limit))
|
||||
|
||||
// For demo purposes, generate some mock search results
|
||||
results := make([]map[string]interface{}, 0, limit)
|
||||
|
||||
// Generate a few mock results based on the query
|
||||
for i := 0; i < min(limit, 3); i++ {
|
||||
mockURI := fmt.Sprintf("ucxl://search:result@%s:demo/*^/result_%d", strings.ToLower(query), i+1)
|
||||
addr, err := ParseUCXL(mockURI)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
envelope, err := a.generateMockContent(addr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"ucxl_uri": envelope.UCXLURI,
|
||||
"content": envelope.Content,
|
||||
"metadata": envelope.Metadata,
|
||||
"timestamp": envelope.Timestamp,
|
||||
"version": envelope.Version,
|
||||
"content_hash": envelope.ContentHash,
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ValidateUCXLAddress validates a UCXL address format
|
||||
func (a *App) ValidateUCXLAddress(uri string) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"valid": false,
|
||||
"error": "",
|
||||
"components": map[string]string{},
|
||||
}
|
||||
|
||||
addr, err := ParseUCXL(uri)
|
||||
if err != nil {
|
||||
result["error"] = err.Error()
|
||||
return result
|
||||
}
|
||||
|
||||
result["valid"] = true
|
||||
result["components"] = map[string]string{
|
||||
"agent": addr.Agent,
|
||||
"role": addr.Role,
|
||||
"project": addr.Project,
|
||||
"task": addr.Task,
|
||||
"temporal": addr.Temporal,
|
||||
"path": addr.Path,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetBZZZStatus returns the status of BZZZ DHT connection
|
||||
func (a *App) GetBZZZStatus() map[string]interface{} {
|
||||
status := map[string]interface{}{
|
||||
"connected": false,
|
||||
"mode": "mock",
|
||||
"peers": 0,
|
||||
"agent": "rustle",
|
||||
"role": "browser",
|
||||
"project": "ucxl-content-browser",
|
||||
"capabilities": []string{"content-retrieval", "content-storage", "search", "address-validation"},
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
// generateMockContent creates mock content based on UCXL address components
|
||||
func (a *App) generateMockContent(addr *UCXLAddress) (*Envelope, error) {
|
||||
var content string
|
||||
var contentType string
|
||||
|
||||
// Generate different content types based on the path
|
||||
switch {
|
||||
case strings.HasSuffix(addr.Path, ".md"):
|
||||
content = a.generateMarkdownContent(addr)
|
||||
contentType = "text/markdown"
|
||||
case strings.HasSuffix(addr.Path, ".json"):
|
||||
content = a.generateJSONContent(addr)
|
||||
contentType = "application/json"
|
||||
case strings.HasSuffix(addr.Path, ".go") || strings.HasSuffix(addr.Path, ".rs") || strings.HasSuffix(addr.Path, ".js"):
|
||||
content = a.generateCodeContent(addr)
|
||||
contentType = "text/x-" + filepath.Ext(addr.Path)[1:]
|
||||
default:
|
||||
content = a.generateDefaultContent(addr)
|
||||
contentType = "text/plain"
|
||||
}
|
||||
|
||||
return &Envelope{
|
||||
UCXLURI: addr.String(),
|
||||
Content: Content{
|
||||
Raw: content,
|
||||
ContentType: contentType,
|
||||
Encoding: "utf-8",
|
||||
},
|
||||
Metadata: map[string]string{
|
||||
"title": fmt.Sprintf("%s/%s Content", strings.Title(addr.Agent), strings.Title(addr.Role)),
|
||||
"author": addr.Agent,
|
||||
"tags": fmt.Sprintf("%s,%s,%s", addr.Project, addr.Task, addr.Role),
|
||||
"source": "rustle-mock-generator",
|
||||
},
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Version: "1.0",
|
||||
ContentHash: fmt.Sprintf("sha256:%x", []byte(content)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *App) generateMarkdownContent(addr *UCXLAddress) string {
|
||||
return fmt.Sprintf(`# %s/%s - %s
|
||||
|
||||
This is mock content generated for the UCXL address: %s
|
||||
|
||||
## Project Context
|
||||
- **Project**: %s
|
||||
- **Task**: %s
|
||||
- **Agent**: %s
|
||||
- **Role**: %s
|
||||
|
||||
## Content Details
|
||||
- **Path**: %s
|
||||
- **Temporal**: %s
|
||||
|
||||
This content demonstrates the RUSTLE browser's ability to render Markdown content from UCXL addresses.
|
||||
|
||||
### Features
|
||||
- ✅ UCXL address parsing
|
||||
- ✅ Content type detection
|
||||
- ✅ Markdown rendering
|
||||
- ✅ BZZZ DHT integration (mock mode)
|
||||
|
||||
### Integration Notes
|
||||
This content is being served through the Wails desktop application with Go backend and React frontend.
|
||||
`, strings.Title(addr.Agent), strings.Title(addr.Role), strings.Title(addr.Task),
|
||||
addr.String(), addr.Project, addr.Task, addr.Agent, addr.Role, addr.Path, addr.Temporal)
|
||||
}
|
||||
|
||||
func (a *App) generateJSONContent(addr *UCXLAddress) string {
|
||||
data := map[string]interface{}{
|
||||
"ucxl_address": addr.String(),
|
||||
"components": map[string]string{
|
||||
"agent": addr.Agent,
|
||||
"role": addr.Role,
|
||||
"project": addr.Project,
|
||||
"task": addr.Task,
|
||||
"path": addr.Path,
|
||||
"temporal": addr.Temporal,
|
||||
},
|
||||
"mock_data": map[string]interface{}{
|
||||
"generated_at": time.Now().Format(time.RFC3339),
|
||||
"generator": "rustle-wails-backend",
|
||||
"content_type": "application/json",
|
||||
"capabilities": []string{
|
||||
"content-rendering",
|
||||
"address-parsing",
|
||||
"mock-generation",
|
||||
},
|
||||
},
|
||||
"bzzz_integration": map[string]interface{}{
|
||||
"mode": "mock",
|
||||
"agent": "rustle",
|
||||
"role": "browser",
|
||||
"project": "ucxl-content-browser",
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, _ := json.MarshalIndent(data, "", " ")
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func (a *App) generateCodeContent(addr *UCXLAddress) string {
|
||||
ext := filepath.Ext(addr.Path)
|
||||
switch ext {
|
||||
case ".go":
|
||||
return fmt.Sprintf(`package %s
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// %sHandler handles %s operations
|
||||
type %sHandler struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// New%sHandler creates a new handler
|
||||
func New%sHandler(ctx context.Context) *%sHandler {
|
||||
return &%sHandler{ctx: ctx}
|
||||
}
|
||||
|
||||
// Process handles the %s task
|
||||
func (h *%sHandler) Process() error {
|
||||
log.Printf("Processing %s task for project %s")
|
||||
return nil
|
||||
}
|
||||
`, addr.Task, strings.Title(addr.Role), addr.Task, strings.Title(addr.Role),
|
||||
strings.Title(addr.Role), strings.Title(addr.Role), strings.Title(addr.Role),
|
||||
strings.Title(addr.Role), addr.Task, strings.Title(addr.Role), addr.Task, addr.Project)
|
||||
|
||||
case ".js":
|
||||
return fmt.Sprintf(`// %s/%s - %s
|
||||
// UCXL Address: %s
|
||||
|
||||
class %sHandler {
|
||||
constructor() {
|
||||
this.project = '%s';
|
||||
this.task = '%s';
|
||||
this.agent = '%s';
|
||||
this.role = '%s';
|
||||
}
|
||||
|
||||
async process() {
|
||||
console.log('Processing task: ' + this.task + ' for project: ' + this.project);
|
||||
// Mock implementation
|
||||
return {
|
||||
status: 'success',
|
||||
ucxl_address: '%s',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = %sHandler;
|
||||
`, strings.Title(addr.Agent), strings.Title(addr.Role), strings.Title(addr.Task), addr.String(),
|
||||
strings.Title(addr.Role), addr.Project, addr.Task, addr.Agent, addr.Role, addr.String(), strings.Title(addr.Role))
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("// Mock code content for %s\n// Generated from UCXL address: %s\n", addr.Path, addr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) generateDefaultContent(addr *UCXLAddress) string {
|
||||
return fmt.Sprintf(`UCXL Content - %s
|
||||
|
||||
Address: %s
|
||||
Agent: %s
|
||||
Role: %s
|
||||
Project: %s
|
||||
Task: %s
|
||||
Path: %s
|
||||
Temporal: %s
|
||||
|
||||
This is mock content generated by the RUSTLE browser's BZZZ-integrated backend.
|
||||
Generated at: %s
|
||||
|
||||
Content demonstrates successful UCXL address parsing and mock content generation.
|
||||
`, strings.Title(addr.Task), addr.String(), addr.Agent, addr.Role,
|
||||
addr.Project, addr.Task, addr.Path, addr.Temporal,
|
||||
time.Now().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
// Helper function for min
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name (legacy function)
|
||||
func (a *App) Greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, Welcome to RUSTLE - UCXL Content Browser!", name)
|
||||
}
|
||||
35
rustle/build/README.md
Normal file
35
rustle/build/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Build Directory
|
||||
|
||||
The build directory is used to house all the build files and assets for your application.
|
||||
|
||||
The structure is:
|
||||
|
||||
* bin - Output directory
|
||||
* darwin - macOS specific files
|
||||
* windows - Windows specific files
|
||||
|
||||
## Mac
|
||||
|
||||
The `darwin` directory holds files specific to Mac builds.
|
||||
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||
and
|
||||
build with `wails build`.
|
||||
|
||||
The directory contains the following files:
|
||||
|
||||
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
|
||||
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
|
||||
|
||||
## Windows
|
||||
|
||||
The `windows` directory contains the manifest and rc files used when building with `wails build`.
|
||||
These may be customised for your application. To return these files to the default state, simply delete them and
|
||||
build with `wails build`.
|
||||
|
||||
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
|
||||
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
|
||||
will be created using the `appicon.png` file in the build directory.
|
||||
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
|
||||
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
|
||||
as well as the application itself (right click the exe -> properties -> details)
|
||||
- `wails.exe.manifest` - The main application manifest file.
|
||||
BIN
rustle/build/appicon.png
Normal file
BIN
rustle/build/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
68
rustle/build/darwin/Info.dev.plist
Normal file
68
rustle/build/darwin/Info.dev.plist
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
63
rustle/build/darwin/Info.plist
Normal file
63
rustle/build/darwin/Info.plist
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
rustle/build/windows/icon.ico
Normal file
BIN
rustle/build/windows/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
15
rustle/build/windows/info.json
Normal file
15
rustle/build/windows/info.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fixed": {
|
||||
"file_version": "{{.Info.ProductVersion}}"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||
"CompanyName": "{{.Info.CompanyName}}",
|
||||
"FileDescription": "{{.Info.ProductName}}",
|
||||
"LegalCopyright": "{{.Info.Copyright}}",
|
||||
"ProductName": "{{.Info.ProductName}}",
|
||||
"Comments": "{{.Info.Comments}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
114
rustle/build/windows/installer/project.nsi
Normal file
114
rustle/build/windows/installer/project.nsi
Normal file
@@ -0,0 +1,114 @@
|
||||
Unicode true
|
||||
|
||||
####
|
||||
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||
## mentioned underneath.
|
||||
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||
## from outside of Wails for debugging and development of the installer.
|
||||
##
|
||||
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||
## > wails build --target windows/amd64 --nsis
|
||||
## Then you can call makensis on this file with specifying the path to your binary:
|
||||
## For a AMD64 only installer:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||
## For a ARM64 only installer:
|
||||
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||
## For a installer with both architectures:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||
####
|
||||
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
####
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
####
|
||||
## Include the wails tools
|
||||
####
|
||||
!include "wails_tools.nsh"
|
||||
|
||||
# The version information for this two must consist of 4 parts
|
||||
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||
|
||||
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||
ManifestDPIAware true
|
||||
|
||||
!include "MUI.nsh"
|
||||
|
||||
!define MUI_ICON "..\icon.ico"
|
||||
!define MUI_UNICON "..\icon.ico"
|
||||
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||
|
||||
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||
|
||||
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||
#!uninstfinalize 'signtool --file "%1"'
|
||||
#!finalize 'signtool --file "%1"'
|
||||
|
||||
Name "${INFO_PRODUCTNAME}"
|
||||
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
ShowInstDetails show # This will always show the installation details.
|
||||
|
||||
Function .onInit
|
||||
!insertmacro wails.checkArchitecture
|
||||
FunctionEnd
|
||||
|
||||
Section
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
!insertmacro wails.webview2runtime
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!insertmacro wails.files
|
||||
|
||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
|
||||
!insertmacro wails.associateFiles
|
||||
!insertmacro wails.associateCustomProtocols
|
||||
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
|
||||
Section "uninstall"
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||
|
||||
RMDir /r $INSTDIR
|
||||
|
||||
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||
|
||||
!insertmacro wails.unassociateFiles
|
||||
!insertmacro wails.unassociateCustomProtocols
|
||||
|
||||
!insertmacro wails.deleteUninstaller
|
||||
SectionEnd
|
||||
249
rustle/build/windows/installer/wails_tools.nsh
Normal file
249
rustle/build/windows/installer/wails_tools.nsh
Normal file
@@ -0,0 +1,249 @@
|
||||
# DO NOT EDIT - Generated automatically by `wails build`
|
||||
|
||||
!include "x64.nsh"
|
||||
!include "WinVer.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "{{.Name}}"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
!endif
|
||||
!ifndef UNINST_KEY_NAME
|
||||
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
!endif
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||
|
||||
!ifndef REQUEST_EXECUTION_LEVEL
|
||||
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||
!endif
|
||||
|
||||
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!ifdef ARG_WAILS_AMD64_BINARY
|
||||
!define SUPPORTS_AMD64
|
||||
!endif
|
||||
|
||||
!ifdef ARG_WAILS_ARM64_BINARY
|
||||
!define SUPPORTS_ARM64
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_AMD64
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "amd64_arm64"
|
||||
!else
|
||||
!define ARCH "amd64"
|
||||
!endif
|
||||
!else
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "arm64"
|
||||
!else
|
||||
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!macro wails.checkArchitecture
|
||||
!ifndef WAILS_WIN10_REQUIRED
|
||||
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||
!endif
|
||||
|
||||
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||
!endif
|
||||
|
||||
${If} ${AtLeastWin10}
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
IfSilent silentArch notSilentArch
|
||||
silentArch:
|
||||
SetErrorLevel 65
|
||||
Abort
|
||||
notSilentArch:
|
||||
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||
Quit
|
||||
${else}
|
||||
IfSilent silentWin notSilentWin
|
||||
silentWin:
|
||||
SetErrorLevel 64
|
||||
Abort
|
||||
notSilentWin:
|
||||
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
!macro wails.files
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro wails.writeUninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
IntFmt $0 "0x%08X" $0
|
||||
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||
!macroend
|
||||
|
||||
!macro wails.deleteUninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||
!macroend
|
||||
|
||||
!macro wails.setShellContext
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||
SetShellVarContext all
|
||||
${else}
|
||||
SetShellVarContext current
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
# Install webview2 by launching the bootstrapper
|
||||
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||
!macro wails.webview2runtime
|
||||
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||
!endif
|
||||
|
||||
SetRegView 64
|
||||
# If the admin key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
SetDetailsPrint both
|
||||
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||
SetDetailsPrint listonly
|
||||
|
||||
InitPluginsDir
|
||||
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||
|
||||
SetDetailsPrint both
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||
|
||||
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||
!macroend
|
||||
|
||||
!macro wails.associateFiles
|
||||
; Create file associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
File "..\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateFiles
|
||||
; Delete app associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||
|
||||
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
!macroend
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||
{{end}}
|
||||
!macroend
|
||||
15
rustle/build/windows/wails.exe.manifest
Normal file
15
rustle/build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
13
rustle/frontend/index.html
Normal file
13
rustle/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>rustle</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./src/main.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1419
rustle/frontend/package-lock.json
generated
Normal file
1419
rustle/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
rustle/frontend/package.json
Normal file
22
rustle/frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
1
rustle/frontend/package.json.md5
Executable file
1
rustle/frontend/package.json.md5
Executable file
@@ -0,0 +1 @@
|
||||
f26173c7304a0bf8ea5c86eb567e7db2
|
||||
277
rustle/frontend/src/App.css
Normal file
277
rustle/frontend/src/App.css
Normal file
@@ -0,0 +1,277 @@
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/* Tab Styles */
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #657786;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: #1da1f2;
|
||||
background-color: #f7f9fa;
|
||||
}
|
||||
|
||||
.tab-active {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #1da1f2;
|
||||
border-bottom: 2px solid #1da1f2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Tab Content */
|
||||
.tab-content {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e1e8ed;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: #14171a;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input[type="text"], .uri-input, .search-input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #ccd6dd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, .uri-input:focus, .search-input:focus {
|
||||
outline: none;
|
||||
border-color: #1da1f2;
|
||||
box-shadow: 0 0 0 2px rgba(29, 161, 242, 0.1);
|
||||
}
|
||||
|
||||
.markdown-editor {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #ccd6dd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
resize: vertical;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.markdown-editor:focus {
|
||||
outline: none;
|
||||
border-color: #1da1f2;
|
||||
box-shadow: 0 0 0 2px rgba(29, 161, 242, 0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
background-color: #1da1f2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background-color: #1991da;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #aab8c2;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Response Section */
|
||||
.response-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.response-section h3 {
|
||||
margin-bottom: 10px;
|
||||
color: #14171a;
|
||||
}
|
||||
|
||||
.response {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e1e8ed;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.response:empty {
|
||||
color: #657786;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.response:empty::before {
|
||||
content: "Response will appear here...";
|
||||
}
|
||||
|
||||
/* DHT and Network specific styles */
|
||||
.dht-status {
|
||||
color: #657786;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.network-info {
|
||||
background: #f7f9fa;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.network-info p {
|
||||
margin: 0 0 12px 0;
|
||||
color: #14171a;
|
||||
}
|
||||
|
||||
.network-info ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.network-info li {
|
||||
margin: 6px 0;
|
||||
color: #657786;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.network-info strong {
|
||||
color: #14171a;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
/* Status indicators */
|
||||
.status-connected {
|
||||
color: #17bf63;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
color: #e0245e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
color: #ffad1f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Demo Content Styles */
|
||||
.demo-content {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #e1e8ed;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
font-weight: 600;
|
||||
color: #14171a;
|
||||
min-width: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccd6dd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.form-row select:focus {
|
||||
outline: none;
|
||||
border-color: #1da1f2;
|
||||
box-shadow: 0 0 0 2px rgba(29, 161, 242, 0.1);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.form-row input[type="text"],
|
||||
.form-row .uri-input,
|
||||
.form-row .search-input {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
min-width: auto;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
590
rustle/frontend/src/App.tsx
Normal file
590
rustle/frontend/src/App.tsx
Normal file
@@ -0,0 +1,590 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './App.css';
|
||||
import { ContentRenderer } from './components/ContentRenderer';
|
||||
import { createMockEnvelope, Envelope } from './types/Envelope';
|
||||
import * as WailsApp from './wails/go/main/App';
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState<'get' | 'post' | 'search' | 'renderer'>('renderer');
|
||||
const [uri, setUri] = useState('ucxl://rustle:browser@demo:showcase/*^/sample.md');
|
||||
const [response, setResponse] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentEnvelope, setCurrentEnvelope] = useState<Envelope | null>(null);
|
||||
const [renderMode, setRenderMode] = useState<'full' | 'preview' | 'minimal'>('full');
|
||||
const [bzzzStatus, setBzzzStatus] = useState<any>(null);
|
||||
|
||||
// Form states
|
||||
const [markdownContent, setMarkdownContent] = useState('# Example Document\\n\\nThis is example markdown content.');
|
||||
const [author, setAuthor] = useState('User');
|
||||
const [title, setTitle] = useState('Test Document');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [searchTags, setSearchTags] = useState('');
|
||||
|
||||
// Load BZZZ status on mount
|
||||
useEffect(() => {
|
||||
const loadBZZZStatus = async () => {
|
||||
try {
|
||||
const status = await WailsApp.GetBZZZStatus();
|
||||
setBzzzStatus(status);
|
||||
} catch (error) {
|
||||
console.error('Failed to load BZZZ status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadBZZZStatus();
|
||||
// Refresh status every 30 seconds
|
||||
const interval = setInterval(loadBZZZStatus, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Function to fetch real content from backend
|
||||
const fetchRealContent = async (uri: string): Promise<Envelope | null> => {
|
||||
try {
|
||||
const result = await WailsApp.GetUCXLContent(uri);
|
||||
|
||||
return {
|
||||
id: result.content_hash.substring(0, 8), // Use hash substring as ID
|
||||
ucxl_uri: { toString: () => result.ucxl_uri },
|
||||
content: {
|
||||
raw: result.content.raw,
|
||||
content_type: result.content.content_type,
|
||||
encoding: result.content.encoding
|
||||
},
|
||||
metadata: {
|
||||
author: result.metadata.author,
|
||||
title: result.metadata.title,
|
||||
tags: result.metadata.tags ? result.metadata.tags.split(',') : [],
|
||||
source: result.metadata.source,
|
||||
context_data: {}
|
||||
},
|
||||
timestamp: result.timestamp,
|
||||
version: result.version,
|
||||
content_hash: result.content_hash
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch content:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to validate UCXL address
|
||||
const validateAddress = async (uri: string) => {
|
||||
try {
|
||||
const result = await WailsApp.ValidateUCXLAddress(uri);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to validate address:', error);
|
||||
return { valid: false, error: 'Validation failed' };
|
||||
}
|
||||
};
|
||||
|
||||
// Demo content creation functions
|
||||
const createDemoMarkdownEnvelope = (): Envelope => {
|
||||
return createMockEnvelope(
|
||||
'ucxl://claude:coder@chorus:content-demo/docs/markdown-sample.md',
|
||||
`# RUSTLE - UCXL Content Browser
|
||||
|
||||
This is a **demonstration** of the RUSTLE content rendering engine running on **Wails**.
|
||||
|
||||
## Features
|
||||
|
||||
- Native desktop application with Go backend
|
||||
- React-based content rendering components
|
||||
- Multi-format content support:
|
||||
- Markdown with syntax highlighting
|
||||
- Interactive JSON visualization
|
||||
- Code rendering for multiple languages
|
||||
- Image display with zoom functionality
|
||||
- UCXL contextual metadata rendering
|
||||
|
||||
### Code Example
|
||||
|
||||
\`\`\`go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
runtime.LogInfo(ctx, "RUSTLE started successfully!")
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Integration with BZZZ
|
||||
|
||||
RUSTLE integrates seamlessly with the BZZZ distributed system:
|
||||
|
||||
1. **SLURP Integration** - Contextual intelligence processing
|
||||
2. **COOEE Messaging** - Agent-to-agent communication
|
||||
3. **DHT Storage** - Distributed content storage
|
||||
4. **UCXL Addressing** - Universal contextual locators
|
||||
|
||||
This native desktop application provides a much cleaner and more reliable experience than the previous Tauri implementation.`,
|
||||
'text/markdown',
|
||||
{
|
||||
title: 'RUSTLE Wails Demo',
|
||||
author: 'BZZZ Team',
|
||||
tags: ['demo', 'markdown', 'wails', 'native-app'],
|
||||
source: 'rustle-wails-demo',
|
||||
context_data: {
|
||||
demo_type: 'markdown',
|
||||
platform: 'wails',
|
||||
features: ['native-desktop', 'go-backend', 'react-frontend'],
|
||||
complexity: 'medium'
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const createDemoJSONEnvelope = (): Envelope => {
|
||||
const jsonContent = {
|
||||
application_info: {
|
||||
name: "RUSTLE",
|
||||
platform: "Wails v2",
|
||||
backend: "Go",
|
||||
frontend: "React + TypeScript"
|
||||
},
|
||||
system_integration: {
|
||||
bzzz_connected: true,
|
||||
slurp_enabled: true,
|
||||
cooee_messaging: true,
|
||||
dht_available: true
|
||||
},
|
||||
performance_metrics: {
|
||||
startup_time_ms: 250,
|
||||
memory_usage_mb: 45,
|
||||
render_performance: "excellent",
|
||||
native_integration: true
|
||||
},
|
||||
content_capabilities: {
|
||||
markdown_rendering: true,
|
||||
code_highlighting: true,
|
||||
json_visualization: true,
|
||||
image_display: true,
|
||||
context_metadata: true,
|
||||
supported_formats: [
|
||||
"text/markdown",
|
||||
"application/json",
|
||||
"text/x-go",
|
||||
"text/x-rust",
|
||||
"application/javascript",
|
||||
"text/x-python",
|
||||
"image/png",
|
||||
"image/jpeg"
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
return createMockEnvelope(
|
||||
'ucxl://rustle:system@wails:info/app/capabilities.json',
|
||||
JSON.stringify(jsonContent, null, 2),
|
||||
'application/json',
|
||||
{
|
||||
title: 'RUSTLE Application Info',
|
||||
author: 'RUSTLE System',
|
||||
tags: ['json', 'app-info', 'capabilities', 'wails'],
|
||||
source: 'rustle-system-info',
|
||||
context_data: {
|
||||
demo_type: 'json',
|
||||
data_structure: 'nested',
|
||||
interactive: true,
|
||||
platform: 'wails'
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const createDemoCodeEnvelope = (): Envelope => {
|
||||
const goCode = `package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
// GetUCXLContent retrieves content by UCXL address
|
||||
func (a *App) GetUCXLContent(uri string) (map[string]interface{}, error) {
|
||||
// Integration with BZZZ/SLURP system
|
||||
content, err := bzzz.RetrieveContent(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve content: %w", err)
|
||||
}
|
||||
|
||||
// Process through SLURP if needed
|
||||
if contextRequired(uri) {
|
||||
content, err = slurp.ProcessContext(content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("context processing failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// PostUCXLContent stores content at UCXL address
|
||||
func (a *App) PostUCXLContent(uri string, content string) error {
|
||||
envelope := &bzzz.Envelope{
|
||||
UCXL: uri,
|
||||
Content: content,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
return bzzz.StoreContent(envelope)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "RUSTLE - UCXL Content Browser",
|
||||
Width: 1200,
|
||||
Height: 800,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
OnStartup: app.startup,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}`;
|
||||
|
||||
return createMockEnvelope(
|
||||
'ucxl://rustle:wails@main:app/src/main.go',
|
||||
goCode,
|
||||
'text/x-go',
|
||||
{
|
||||
title: 'RUSTLE Wails Main Application',
|
||||
author: 'BZZZ Team',
|
||||
tags: ['go', 'wails', 'main', 'application'],
|
||||
source: 'rustle-main-go',
|
||||
context_data: {
|
||||
demo_type: 'code',
|
||||
language: 'go',
|
||||
framework: 'wails',
|
||||
complexity: 'high'
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container" style={{ padding: 0, margin: 0, height: '100vh' }}>
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
padding: '16px',
|
||||
textAlign: 'center',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
🚀 RUSTLE - UCXL Content Browser (Wails)
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="tabs" style={{ padding: '16px', background: '#f8f9fa' }}>
|
||||
<button
|
||||
className={activeTab === 'renderer' ? 'tab-active' : 'tab'}
|
||||
onClick={() => setActiveTab('renderer')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
margin: '0 8px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
background: activeTab === 'renderer' ? '#007bff' : '#e9ecef',
|
||||
color: activeTab === 'renderer' ? 'white' : '#495057',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Content Renderer
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === 'get' ? 'tab-active' : 'tab'}
|
||||
onClick={() => setActiveTab('get')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
margin: '0 8px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
background: activeTab === 'get' ? '#007bff' : '#e9ecef',
|
||||
color: activeTab === 'get' ? 'white' : '#495057',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Get Content
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="tab-content" style={{ padding: '16px', height: 'calc(100vh - 140px)', overflow: 'auto' }}>
|
||||
{activeTab === 'renderer' && (
|
||||
<div className="section">
|
||||
<div className="form-row" style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="render-mode" style={{ marginRight: '12px' }}>Render Mode:</label>
|
||||
<select
|
||||
id="render-mode"
|
||||
value={renderMode}
|
||||
onChange={(e) => setRenderMode(e.target.value as 'full' | 'preview' | 'minimal')}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
marginRight: '12px',
|
||||
border: '1px solid #ced4da',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
<option value="full">Full</option>
|
||||
<option value="preview">Preview</option>
|
||||
<option value="minimal">Minimal</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const envelope = await fetchRealContent('ucxl://rustle:browser@demo:showcase/*^/sample.md');
|
||||
if (envelope) setCurrentEnvelope(envelope);
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
margin: '0 6px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: '#28a745',
|
||||
color: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Load Real Markdown
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const envelope = await fetchRealContent('ucxl://rustle:browser@demo:showcase/*^/data.json');
|
||||
if (envelope) setCurrentEnvelope(envelope);
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
margin: '0 6px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: '#17a2b8',
|
||||
color: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Load Real JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const envelope = await fetchRealContent('ucxl://rustle:browser@demo:showcase/*^/main.go');
|
||||
if (envelope) setCurrentEnvelope(envelope);
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
margin: '0 6px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: '#ffc107',
|
||||
color: '#212529',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Load Real Code
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentEnvelope(createDemoMarkdownEnvelope())}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
margin: '0 6px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: '#6c757d',
|
||||
color: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Mock Demo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentEnvelope && (
|
||||
<div className="demo-content">
|
||||
<ContentRenderer
|
||||
envelope={currentEnvelope}
|
||||
renderMode={renderMode}
|
||||
showMetadata={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!currentEnvelope && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '60px',
|
||||
color: '#6c757d',
|
||||
fontSize: '18px'
|
||||
}}>
|
||||
👆 Click a demo button above to test the content rendering engine
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'get' && (
|
||||
<div className="section">
|
||||
<h3>Retrieve Content</h3>
|
||||
|
||||
{/* BZZZ Status */}
|
||||
{bzzzStatus && (
|
||||
<div style={{
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
background: '#f8f9fa',
|
||||
border: '1px solid #dee2e6',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
<strong>BZZZ Status:</strong>
|
||||
<span style={{ color: bzzzStatus.connected ? '#28a745' : '#dc3545', marginLeft: '8px' }}>
|
||||
{bzzzStatus.connected ? 'Connected' : 'Mock Mode'}
|
||||
</span>
|
||||
<span style={{ marginLeft: '16px' }}>
|
||||
Agent: {bzzzStatus.agent}/{bzzzStatus.role} | Project: {bzzzStatus.project} | Peers: {bzzzStatus.peers}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-row">
|
||||
<input
|
||||
type="text"
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
placeholder="ucxl://agent:role@project:task/temporal/path"
|
||||
style={{
|
||||
width: '70%',
|
||||
padding: '8px 12px',
|
||||
marginRight: '12px',
|
||||
border: '1px solid #ced4da',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const envelope = await fetchRealContent(uri);
|
||||
if (envelope) {
|
||||
setResponse(JSON.stringify(envelope, null, 2));
|
||||
setCurrentEnvelope(envelope);
|
||||
} else {
|
||||
setResponse('Failed to fetch content');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: loading ? '#6c757d' : '#007bff',
|
||||
color: 'white',
|
||||
cursor: loading ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Get Content'}
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const validation = await validateAddress(uri);
|
||||
setResponse(JSON.stringify(validation, null, 2));
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
margin: '0 8px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: '#6f42c1',
|
||||
color: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Validate Address
|
||||
</button>
|
||||
</div>
|
||||
<div style={{
|
||||
marginTop: '16px',
|
||||
padding: '12px',
|
||||
background: '#f8f9fa',
|
||||
border: '1px solid #dee2e6',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px',
|
||||
minHeight: '100px',
|
||||
maxHeight: '400px',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{response || 'Content will appear here...'}
|
||||
</div>
|
||||
|
||||
{/* If we have current envelope, show it rendered */}
|
||||
{currentEnvelope && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<h4>Rendered Content:</h4>
|
||||
<div className="demo-content">
|
||||
<ContentRenderer
|
||||
envelope={currentEnvelope}
|
||||
renderMode={renderMode}
|
||||
showMetadata={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
93
rustle/frontend/src/assets/fonts/OFL.txt
Normal file
93
rustle/frontend/src/assets/fonts/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
rustle/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
BIN
rustle/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
Binary file not shown.
BIN
rustle/frontend/src/assets/images/logo-universal.png
Normal file
BIN
rustle/frontend/src/assets/images/logo-universal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
99
rustle/frontend/src/components/ContentRenderer.css
Normal file
99
rustle/frontend/src/components/ContentRenderer.css
Normal file
@@ -0,0 +1,99 @@
|
||||
.content-renderer {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.content-renderer--minimal {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.content-renderer--preview {
|
||||
max-height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.content-renderer--minimal .content-body {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.content-unknown {
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-left: 4px solid #ff9800;
|
||||
}
|
||||
|
||||
.content-unknown h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #ff9800;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content-raw {
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.content-footer {
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.content-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.content-stats span {
|
||||
padding: 2px 6px;
|
||||
background: #e9ecef;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.content-renderer {
|
||||
background: #1e1e1e;
|
||||
border-color: #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.content-unknown {
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.content-raw {
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.content-footer {
|
||||
background: #2d2d2d;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.content-stats span {
|
||||
background: #333;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
118
rustle/frontend/src/components/ContentRenderer.tsx
Normal file
118
rustle/frontend/src/components/ContentRenderer.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
import { Envelope } from '../types/Envelope';
|
||||
import { MarkdownRenderer } from './renderers/MarkdownRenderer';
|
||||
import { CodeRenderer } from './renderers/CodeRenderer';
|
||||
import { JSONRenderer } from './renderers/JSONRenderer';
|
||||
import { ImageRenderer } from './renderers/ImageRenderer';
|
||||
// import { ContextRenderer as UCXLContextRenderer } from './renderers/ContextRenderer';
|
||||
import { ErrorRenderer } from './renderers/ErrorRenderer';
|
||||
import { MetadataRenderer } from './renderers/MetadataRenderer';
|
||||
import './ContentRenderer.css';
|
||||
|
||||
interface ContentRendererProps {
|
||||
envelope: Envelope;
|
||||
showMetadata?: boolean;
|
||||
renderMode?: 'full' | 'preview' | 'minimal';
|
||||
}
|
||||
|
||||
export const ContentRenderer: React.FC<ContentRendererProps> = ({
|
||||
envelope,
|
||||
showMetadata = true,
|
||||
renderMode = 'full'
|
||||
}) => {
|
||||
const renderContent = () => {
|
||||
const contentType = envelope.content.content_type;
|
||||
const content = envelope.content.raw;
|
||||
|
||||
try {
|
||||
switch (contentType) {
|
||||
// Markdown content
|
||||
case 'text/markdown':
|
||||
case 'text/x-markdown':
|
||||
return <MarkdownRenderer content={content} renderMode={renderMode} />;
|
||||
|
||||
// Code files
|
||||
case 'text/plain':
|
||||
case 'text/x-go':
|
||||
case 'text/x-rust':
|
||||
case 'text/x-python':
|
||||
case 'text/x-javascript':
|
||||
case 'text/x-typescript':
|
||||
case 'application/javascript':
|
||||
case 'application/typescript':
|
||||
return <CodeRenderer content={content} contentType={contentType} renderMode={renderMode} />;
|
||||
|
||||
// JSON and structured data
|
||||
case 'application/json':
|
||||
case 'text/json':
|
||||
return <JSONRenderer content={content} renderMode={renderMode} />;
|
||||
|
||||
// Image content
|
||||
case 'image/png':
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/gif':
|
||||
case 'image/webp':
|
||||
case 'image/svg+xml':
|
||||
return <ImageRenderer content={content} contentType={contentType} renderMode={renderMode} />;
|
||||
|
||||
// UCXL contextual content
|
||||
case 'application/ucxl-context':
|
||||
case 'application/ucxl-metadata':
|
||||
// return <UCXLContextRenderer envelope={envelope} renderMode={renderMode} />;
|
||||
return <div className="context-placeholder">Context rendering temporarily disabled</div>;
|
||||
|
||||
// Decision records and reports
|
||||
case 'application/ucxl-decision':
|
||||
case 'application/ucxl-report':
|
||||
return <MarkdownRenderer
|
||||
content={content}
|
||||
renderMode={renderMode}
|
||||
specialType="decision-record"
|
||||
/>;
|
||||
|
||||
// Unknown or unsupported content types
|
||||
default:
|
||||
return (
|
||||
<div className="content-unknown">
|
||||
<h4>Unknown Content Type: {contentType}</h4>
|
||||
<pre className="content-raw">{content}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return <ErrorRenderer error={error} contentType={contentType} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`content-renderer content-renderer--${renderMode}`}>
|
||||
{showMetadata && (
|
||||
<MetadataRenderer
|
||||
metadata={envelope.metadata}
|
||||
uri={envelope.ucxl_uri}
|
||||
timestamp={envelope.timestamp}
|
||||
version={envelope.version}
|
||||
renderMode={renderMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="content-body">
|
||||
{renderContent()}
|
||||
</div>
|
||||
|
||||
{renderMode === 'full' && (
|
||||
<div className="content-footer">
|
||||
<div className="content-stats">
|
||||
<span>Size: {envelope.content.raw.length} bytes</span>
|
||||
<span>Type: {envelope.content.content_type}</span>
|
||||
<span>Encoding: {envelope.content.encoding}</span>
|
||||
<span>Hash: {envelope.content_hash.substring(0, 8)}...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentRenderer;
|
||||
262
rustle/frontend/src/components/renderers/CodeRenderer.css
Normal file
262
rustle/frontend/src/components/renderers/CodeRenderer.css
Normal file
@@ -0,0 +1,262 @@
|
||||
.code-renderer {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #e9ecef;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.language-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.language-badge {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.content-type {
|
||||
font-size: 11px;
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.code-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.code-container {
|
||||
display: flex;
|
||||
background: white;
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-renderer--preview .code-container {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.code-renderer--minimal .code-container {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #e9ecef;
|
||||
padding: 12px 8px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
min-width: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
line-height: 1.4;
|
||||
color: #6c757d;
|
||||
font-size: 12px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.code-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-content pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.code-content code {
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Syntax highlighting styles */
|
||||
.keyword {
|
||||
color: #0066cc;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.type {
|
||||
color: #007f7f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.string {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.url {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.path {
|
||||
color: #8e44ad;
|
||||
}
|
||||
|
||||
.number {
|
||||
color: #009999;
|
||||
}
|
||||
|
||||
/* Language-specific badge colors */
|
||||
.language-badge:has-text("go") {
|
||||
background: #00add8;
|
||||
}
|
||||
|
||||
.language-badge:has-text("rust") {
|
||||
background: #ce422b;
|
||||
}
|
||||
|
||||
.language-badge:has-text("python") {
|
||||
background: #3776ab;
|
||||
}
|
||||
|
||||
.language-badge:has-text("javascript") {
|
||||
background: #f7df1e;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.language-badge:has-text("typescript") {
|
||||
background: #3178c6;
|
||||
}
|
||||
|
||||
.content-truncated {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e9ecef;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Minimal mode styling */
|
||||
.code-renderer--minimal .code-header {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.code-renderer--minimal .language-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
|
||||
.code-renderer--minimal .code-content pre {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.code-renderer--minimal .code-content code {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.code-renderer {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
background: #404040;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.content-type {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.code-container {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.code-content code {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.keyword {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.type {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: #6a9955;
|
||||
}
|
||||
|
||||
.string {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.url {
|
||||
color: #74c0fc;
|
||||
}
|
||||
|
||||
.path {
|
||||
color: #c678dd;
|
||||
}
|
||||
|
||||
.number {
|
||||
color: #b5cea8;
|
||||
}
|
||||
|
||||
.content-truncated {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
color: #adb5bd;
|
||||
}
|
||||
}
|
||||
216
rustle/frontend/src/components/renderers/CodeRenderer.tsx
Normal file
216
rustle/frontend/src/components/renderers/CodeRenderer.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import './CodeRenderer.css';
|
||||
|
||||
interface CodeRendererProps {
|
||||
content: string;
|
||||
contentType: string;
|
||||
renderMode?: 'full' | 'preview' | 'minimal';
|
||||
}
|
||||
|
||||
export const CodeRenderer: React.FC<CodeRendererProps> = ({
|
||||
content,
|
||||
contentType,
|
||||
renderMode = 'full'
|
||||
}) => {
|
||||
const getLanguageFromContentType = (contentType: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'text/x-go': 'go',
|
||||
'text/x-rust': 'rust',
|
||||
'text/x-python': 'python',
|
||||
'text/x-javascript': 'javascript',
|
||||
'text/x-typescript': 'typescript',
|
||||
'application/javascript': 'javascript',
|
||||
'application/typescript': 'typescript',
|
||||
'text/plain': 'text'
|
||||
};
|
||||
return typeMap[contentType] || 'text';
|
||||
};
|
||||
|
||||
const language = getLanguageFromContentType(contentType);
|
||||
|
||||
// Basic syntax highlighting patterns
|
||||
const applySyntaxHighlighting = (code: string, lang: string): string => {
|
||||
let highlighted = escapeHtml(code);
|
||||
|
||||
switch (lang) {
|
||||
case 'go':
|
||||
highlighted = highlightGo(highlighted);
|
||||
break;
|
||||
case 'rust':
|
||||
highlighted = highlightRust(highlighted);
|
||||
break;
|
||||
case 'python':
|
||||
highlighted = highlightPython(highlighted);
|
||||
break;
|
||||
case 'javascript':
|
||||
case 'typescript':
|
||||
highlighted = highlightJavaScript(highlighted);
|
||||
break;
|
||||
default:
|
||||
// For plain text, just highlight URLs and file paths
|
||||
highlighted = highlightGeneric(highlighted);
|
||||
}
|
||||
|
||||
return highlighted;
|
||||
};
|
||||
|
||||
const escapeHtml = (text: string): string => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
const highlightGo = (code: string): string => {
|
||||
// Go keywords
|
||||
code = code.replace(/\b(package|import|func|var|const|type|struct|interface|if|else|for|range|switch|case|default|return|defer|go|chan|select|map|make|new|len|cap|append|copy|delete|close|nil|true|false)\b/g, '<span class="keyword">$1</span>');
|
||||
|
||||
// Go types
|
||||
code = code.replace(/\b(int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|float32|float64|complex64|complex128|bool|byte|rune|string|error)\b/g, '<span class="type">$1</span>');
|
||||
|
||||
// Comments
|
||||
code = code.replace(/(\/\/.*$)/gm, '<span class="comment">$1</span>');
|
||||
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="comment">$1</span>');
|
||||
|
||||
// Strings
|
||||
code = code.replace(/(".*?"|`.*?`)/g, '<span class="string">$1</span>');
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
const highlightRust = (code: string): string => {
|
||||
// Rust keywords
|
||||
code = code.replace(/\b(fn|let|mut|const|static|use|mod|pub|struct|enum|impl|trait|for|if|else|match|while|loop|break|continue|return|true|false|Some|None|Ok|Err|Self|self|super|crate)\b/g, '<span class="keyword">$1</span>');
|
||||
|
||||
// Rust types
|
||||
code = code.replace(/\b(i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize|f32|f64|bool|char|str|String|Vec|Option|Result)\b/g, '<span class="type">$1</span>');
|
||||
|
||||
// Comments
|
||||
code = code.replace(/(\/\/.*$)/gm, '<span class="comment">$1</span>');
|
||||
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="comment">$1</span>');
|
||||
|
||||
// Strings
|
||||
code = code.replace(/(".*?"|'.*?')/g, '<span class="string">$1</span>');
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
const highlightPython = (code: string): string => {
|
||||
// Python keywords
|
||||
code = code.replace(/\b(def|class|import|from|if|elif|else|for|while|try|except|finally|with|as|return|yield|lambda|and|or|not|in|is|True|False|None|pass|break|continue|global|nonlocal)\b/g, '<span class="keyword">$1</span>');
|
||||
|
||||
// Python built-ins
|
||||
code = code.replace(/\b(str|int|float|bool|list|dict|tuple|set|len|range|enumerate|zip|map|filter|print|input|open|type|isinstance|hasattr|getattr|setattr)\b/g, '<span class="type">$1</span>');
|
||||
|
||||
// Comments
|
||||
code = code.replace(/(#.*$)/gm, '<span class="comment">$1</span>');
|
||||
|
||||
// Strings
|
||||
code = code.replace(/("""[\s\S]*?"""|'''[\s\S]*?'''|".*?"|'.*?')/g, '<span class="string">$1</span>');
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
const highlightJavaScript = (code: string): string => {
|
||||
// JavaScript keywords
|
||||
code = code.replace(/\b(function|const|let|var|if|else|for|while|do|switch|case|default|return|break|continue|try|catch|finally|throw|new|this|class|extends|import|export|from|as|async|await|yield|true|false|null|undefined)\b/g, '<span class="keyword">$1</span>');
|
||||
|
||||
// JavaScript types and built-ins
|
||||
code = code.replace(/\b(String|Number|Boolean|Array|Object|Date|RegExp|Promise|Map|Set|Symbol|console|window|document|Math|JSON)\b/g, '<span class="type">$1</span>');
|
||||
|
||||
// Comments
|
||||
code = code.replace(/(\/\/.*$)/gm, '<span class="comment">$1</span>');
|
||||
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="comment">$1</span>');
|
||||
|
||||
// Strings
|
||||
code = code.replace(/(`.*?`|".*?"|'.*?')/g, '<span class="string">$1</span>');
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
const highlightGeneric = (code: string): string => {
|
||||
// URLs
|
||||
code = code.replace(/(https?:\/\/[^\s]+)/g, '<span class="url">$1</span>');
|
||||
|
||||
// File paths
|
||||
code = code.replace(/(\/[^\s]*)/g, '<span class="path">$1</span>');
|
||||
|
||||
// Numbers
|
||||
code = code.replace(/\b(\d+\.?\d*)\b/g, '<span class="number">$1</span>');
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
const processedContent = useMemo(() => {
|
||||
if (renderMode === 'preview') {
|
||||
const lines = content.split('\n');
|
||||
const previewLines = lines.slice(0, 20);
|
||||
return applySyntaxHighlighting(previewLines.join('\n'), language);
|
||||
}
|
||||
return applySyntaxHighlighting(content, language);
|
||||
}, [content, language, renderMode]);
|
||||
|
||||
const getLineNumbers = (text: string): string[] => {
|
||||
const lines = text.split('\n');
|
||||
return lines.map((_, index) => (index + 1).toString());
|
||||
};
|
||||
|
||||
const lineNumbers = getLineNumbers(renderMode === 'preview' ? content.split('\n').slice(0, 20).join('\n') : content);
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
// Could add a toast notification here
|
||||
console.log('Code copied to clipboard');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`code-renderer code-renderer--${renderMode}`}>
|
||||
<div className="code-header">
|
||||
<div className="language-label">
|
||||
<span className="language-badge">{language}</span>
|
||||
<span className="content-type">{contentType}</span>
|
||||
</div>
|
||||
{renderMode === 'full' && (
|
||||
<div className="code-actions">
|
||||
<button
|
||||
className="copy-button"
|
||||
onClick={handleCopyToClipboard}
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
📋 Copy
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="code-container">
|
||||
{renderMode !== 'minimal' && (
|
||||
<div className="line-numbers">
|
||||
{lineNumbers.map((num, index) => (
|
||||
<div key={index} className="line-number">
|
||||
{num}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="code-content">
|
||||
<pre>
|
||||
<code
|
||||
className={`language-${language}`}
|
||||
dangerouslySetInnerHTML={{ __html: processedContent }}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderMode === 'preview' && content.split('\n').length > 20 && (
|
||||
<div className="content-truncated">
|
||||
<em>Showing first 20 lines. Click to view full content.</em>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRenderer;
|
||||
306
rustle/frontend/src/components/renderers/ContextRenderer.css
Normal file
306
rustle/frontend/src/components/renderers/ContextRenderer.css
Normal file
@@ -0,0 +1,306 @@
|
||||
.context-renderer {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.context-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.context-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.format-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.generated-time {
|
||||
font-size: 11px;
|
||||
opacity: 0.9;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.context-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.context-metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.context-type {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
border-left: 3px solid #e9ecef;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.metadata-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.metadata-section p {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.context-tag {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
border: 1px solid #e1bee7;
|
||||
}
|
||||
|
||||
.insight-list,
|
||||
.dependency-list,
|
||||
.file-list,
|
||||
.decision-list {
|
||||
margin: 4px 0 0 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.insight-list li {
|
||||
margin-bottom: 4px;
|
||||
color: #495057;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dependency-item,
|
||||
.decision-item {
|
||||
margin-bottom: 4px;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-item code {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
color: #e83e8c;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.confidence-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.confidence-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.confidence-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: #e9ecef;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.confidence-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.confidence-value {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.content-truncated {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e9ecef;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Context type specific styling */
|
||||
.context-renderer--minimal .metadata-section {
|
||||
border-left-width: 2px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.context-renderer--minimal .metadata-section h4 {
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.context-renderer--minimal .tag {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.context-renderer--preview .context-content {
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Special styling for different context types */
|
||||
.metadata-section:has(.type-badge:contains("file")) {
|
||||
border-left-color: #28a745;
|
||||
}
|
||||
|
||||
.metadata-section:has(.type-badge:contains("directory")) {
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
.metadata-section:has(.type-badge:contains("project")) {
|
||||
border-left-color: #007bff;
|
||||
}
|
||||
|
||||
.metadata-section:has(.type-badge:contains("global")) {
|
||||
border-left-color: #6f42c1;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.context-renderer {
|
||||
background: #1e1e1e;
|
||||
border-color: #404040;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.context-content {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
border-left-color: #555;
|
||||
}
|
||||
|
||||
.metadata-section h4 {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.metadata-section p {
|
||||
color: #ced4da;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
background: #1a365d;
|
||||
color: #90cdf4;
|
||||
border-color: #2a69ac;
|
||||
}
|
||||
|
||||
.context-tag {
|
||||
background: #44337a;
|
||||
color: #d8b4fe;
|
||||
border-color: #6b46c1;
|
||||
}
|
||||
|
||||
.insight-list li {
|
||||
color: #ced4da;
|
||||
}
|
||||
|
||||
.dependency-item,
|
||||
.decision-item {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.file-item code {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
color: #fd7e14;
|
||||
}
|
||||
|
||||
.confidence-indicator {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.confidence-label,
|
||||
.confidence-value {
|
||||
color: #ced4da;
|
||||
}
|
||||
|
||||
.confidence-bar {
|
||||
background: #404040;
|
||||
}
|
||||
|
||||
.content-truncated {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
color: #adb5bd;
|
||||
}
|
||||
}
|
||||
239
rustle/frontend/src/components/renderers/ContextRenderer.tsx
Normal file
239
rustle/frontend/src/components/renderers/ContextRenderer.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Envelope } from '../../types/Envelope';
|
||||
import { MarkdownRenderer } from './MarkdownRenderer';
|
||||
import { JSONRenderer } from './JSONRenderer';
|
||||
import './ContextRenderer.css';
|
||||
|
||||
interface ContextRendererProps {
|
||||
envelope: Envelope;
|
||||
renderMode?: 'full' | 'preview' | 'minimal';
|
||||
}
|
||||
|
||||
interface UCXLContext {
|
||||
summary?: string;
|
||||
purpose?: string;
|
||||
technologies?: string[];
|
||||
tags?: string[];
|
||||
insights?: string[];
|
||||
dependencies?: string[];
|
||||
related_files?: string[];
|
||||
decision_records?: string[];
|
||||
rag_confidence?: number;
|
||||
generated_at?: string;
|
||||
context_type?: 'file' | 'directory' | 'project' | 'global';
|
||||
}
|
||||
|
||||
export const ContextRenderer: React.FC<ContextRendererProps> = ({
|
||||
envelope,
|
||||
renderMode = 'full'
|
||||
}) => {
|
||||
const contextData = useMemo((): UCXLContext | null => {
|
||||
try {
|
||||
const content = envelope.content.raw;
|
||||
|
||||
// Try to parse as JSON first
|
||||
if (envelope.content.content_type === 'application/json' ||
|
||||
envelope.content.content_type === 'application/ucxl-context') {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
// If it's markdown with front matter, try to extract context
|
||||
if (envelope.content.content_type === 'text/markdown') {
|
||||
const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
if (frontMatterMatch) {
|
||||
const frontMatter = frontMatterMatch[1];
|
||||
// Simple YAML-like parsing for common fields
|
||||
const context: UCXLContext = {};
|
||||
|
||||
frontMatter.split('\n').forEach(line => {
|
||||
const match = line.match(/^(\w+):\s*(.+)$/);
|
||||
if (match) {
|
||||
const [, key, value] = match;
|
||||
if (key === 'technologies' || key === 'tags' || key === 'insights') {
|
||||
context[key as keyof UCXLContext] = value.split(',').map(s => s.trim()) as any;
|
||||
} else {
|
||||
(context as any)[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [envelope.content]);
|
||||
|
||||
const renderContextMetadata = (context: UCXLContext) => (
|
||||
<div className="context-metadata">
|
||||
{context.context_type && (
|
||||
<div className="context-type">
|
||||
<span className="type-badge">{context.context_type}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{context.summary && (
|
||||
<div className="metadata-section">
|
||||
<h4>Summary</h4>
|
||||
<p>{context.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{context.purpose && (
|
||||
<div className="metadata-section">
|
||||
<h4>Purpose</h4>
|
||||
<p>{context.purpose}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{context.technologies && context.technologies.length > 0 && (
|
||||
<div className="metadata-section">
|
||||
<h4>Technologies</h4>
|
||||
<div className="tag-list">
|
||||
{context.technologies.map((tech, index) => (
|
||||
<span key={index} className="tag tech-tag">{tech}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{context.tags && context.tags.length > 0 && (
|
||||
<div className="metadata-section">
|
||||
<h4>Tags</h4>
|
||||
<div className="tag-list">
|
||||
{context.tags.map((tag, index) => (
|
||||
<span key={index} className="tag context-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{context.insights && context.insights.length > 0 && (
|
||||
<div className="metadata-section">
|
||||
<h4>Key Insights</h4>
|
||||
<ul className="insight-list">
|
||||
{context.insights.map((insight, index) => (
|
||||
<li key={index}>{insight}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{context.dependencies && context.dependencies.length > 0 && (
|
||||
<div className="metadata-section">
|
||||
<h4>Dependencies</h4>
|
||||
<ul className="dependency-list">
|
||||
{context.dependencies.map((dep, index) => (
|
||||
<li key={index} className="dependency-item">{dep}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{context.related_files && context.related_files.length > 0 && (
|
||||
<div className="metadata-section">
|
||||
<h4>Related Files</h4>
|
||||
<ul className="file-list">
|
||||
{context.related_files.map((file, index) => (
|
||||
<li key={index} className="file-item">
|
||||
<code>{file}</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{context.decision_records && context.decision_records.length > 0 && (
|
||||
<div className="metadata-section">
|
||||
<h4>Decision Records</h4>
|
||||
<ul className="decision-list">
|
||||
{context.decision_records.map((record, index) => (
|
||||
<li key={index} className="decision-item">{record}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderConfidenceIndicator = (confidence?: number) => {
|
||||
if (!confidence) return null;
|
||||
|
||||
const getConfidenceColor = (score: number): string => {
|
||||
if (score >= 0.8) return '#28a745';
|
||||
if (score >= 0.6) return '#ffc107';
|
||||
return '#dc3545';
|
||||
};
|
||||
|
||||
const getConfidenceLabel = (score: number): string => {
|
||||
if (score >= 0.8) return 'High';
|
||||
if (score >= 0.6) return 'Medium';
|
||||
return 'Low';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="confidence-indicator">
|
||||
<span className="confidence-label">RAG Confidence:</span>
|
||||
<div className="confidence-bar">
|
||||
<div
|
||||
className="confidence-fill"
|
||||
style={{
|
||||
width: `${confidence * 100}%`,
|
||||
backgroundColor: getConfidenceColor(confidence)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="confidence-value">
|
||||
{getConfidenceLabel(confidence)} ({(confidence * 100).toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// If we can't parse the context, fall back to other renderers
|
||||
if (!contextData) {
|
||||
if (envelope.content.content_type === 'application/json') {
|
||||
return <JSONRenderer content={envelope.content.raw} renderMode={renderMode} />;
|
||||
} else {
|
||||
return <MarkdownRenderer content={envelope.content.raw} renderMode={renderMode} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`context-renderer context-renderer--${renderMode}`}>
|
||||
{renderMode === 'full' && (
|
||||
<div className="context-header">
|
||||
<div className="context-info">
|
||||
<span className="format-badge">UCXL Context</span>
|
||||
{contextData.generated_at && (
|
||||
<span className="generated-time">
|
||||
Generated: {new Date(contextData.generated_at).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="context-content">
|
||||
{renderContextMetadata(contextData)}
|
||||
|
||||
{contextData.rag_confidence && renderMode !== 'minimal' && (
|
||||
<div className="metadata-section">
|
||||
{renderConfidenceIndicator(contextData.rag_confidence)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderMode === 'preview' && (
|
||||
<div className="content-truncated">
|
||||
<em>Context preview - click to view full details</em>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContextRenderer;
|
||||
259
rustle/frontend/src/components/renderers/ErrorRenderer.css
Normal file
259
rustle/frontend/src/components/renderers/ErrorRenderer.css
Normal file
@@ -0,0 +1,259 @@
|
||||
.error-renderer {
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fed7d7;
|
||||
border-bottom: 1px solid #feb2b2;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 2rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.error-title h3 {
|
||||
margin: 0 0 4px 0;
|
||||
color: #c53030;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.error-title p {
|
||||
margin: 0;
|
||||
color: #744210;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.error-title code {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.error-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.error-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #c53030;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.error-info,
|
||||
.tech-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.error-field,
|
||||
.tech-field {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-weight: 500;
|
||||
color: #744210;
|
||||
min-width: 80px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
color: #2d3748;
|
||||
font-size: 0.85rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.field-value code {
|
||||
background: #f7fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
.stack-trace {
|
||||
background: #f7fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #4a5568;
|
||||
overflow-x: auto;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.suggestion-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
margin-bottom: 6px;
|
||||
color: #4a5568;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.suggestion-item::marker {
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: #f7fafc;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.retry-button,
|
||||
.report-button {
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
background: #38a169;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.retry-button:hover {
|
||||
background: #2f855a;
|
||||
}
|
||||
|
||||
.report-button {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.report-button:hover {
|
||||
background: #3182ce;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.error-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-field,
|
||||
.tech-field {
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
min-width: auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.error-renderer {
|
||||
background: #3d1a1b;
|
||||
border-color: #842029;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
background: #842029;
|
||||
border-color: #6c1f28;
|
||||
}
|
||||
|
||||
.error-title h3 {
|
||||
color: #ea868f;
|
||||
}
|
||||
|
||||
.error-title p {
|
||||
color: #f7d794;
|
||||
}
|
||||
|
||||
.error-title code {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #fd7e14;
|
||||
}
|
||||
|
||||
.error-section h4 {
|
||||
color: #ea868f;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
color: #f7d794;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
color: #e9ecef;
|
||||
}
|
||||
|
||||
.field-value code {
|
||||
background: #495057;
|
||||
border-color: #6c757d;
|
||||
color: #fd7e14;
|
||||
}
|
||||
|
||||
.stack-trace {
|
||||
background: #495057;
|
||||
border-color: #6c757d;
|
||||
color: #ced4da;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.suggestion-item::marker {
|
||||
color: #ea868f;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
background: #495057;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
}
|
||||
168
rustle/frontend/src/components/renderers/ErrorRenderer.tsx
Normal file
168
rustle/frontend/src/components/renderers/ErrorRenderer.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React from 'react';
|
||||
import './ErrorRenderer.css';
|
||||
|
||||
interface ErrorRendererProps {
|
||||
error: any;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
export const ErrorRenderer: React.FC<ErrorRendererProps> = ({
|
||||
error,
|
||||
contentType
|
||||
}) => {
|
||||
const getErrorDetails = (error: any) => {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return {
|
||||
name: 'Rendering Error',
|
||||
message: error,
|
||||
stack: null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Unknown Error',
|
||||
message: 'An unexpected error occurred while rendering this content',
|
||||
stack: null
|
||||
};
|
||||
};
|
||||
|
||||
const errorDetails = getErrorDetails(error);
|
||||
|
||||
const getErrorIcon = (contentType: string): string => {
|
||||
if (contentType.startsWith('image/')) return '🖼️';
|
||||
if (contentType.includes('json')) return '📄';
|
||||
if (contentType.includes('markdown')) return '📝';
|
||||
if (contentType.startsWith('text/')) return '📄';
|
||||
return '⚠️';
|
||||
};
|
||||
|
||||
const getSuggestions = (contentType: string): string[] => {
|
||||
const suggestions: string[] = [];
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
suggestions.push('Check if the JSON syntax is valid');
|
||||
suggestions.push('Verify there are no trailing commas or unescaped quotes');
|
||||
}
|
||||
|
||||
if (contentType.includes('markdown')) {
|
||||
suggestions.push('Check for malformed markdown syntax');
|
||||
suggestions.push('Verify code blocks are properly closed');
|
||||
}
|
||||
|
||||
if (contentType.startsWith('image/')) {
|
||||
suggestions.push('Verify the image data is not corrupted');
|
||||
suggestions.push('Check if the image format is supported');
|
||||
suggestions.push('Ensure base64 encoding is correct if applicable');
|
||||
}
|
||||
|
||||
if (contentType.startsWith('text/')) {
|
||||
suggestions.push('Check for special characters or encoding issues');
|
||||
suggestions.push('Verify the content matches the declared content type');
|
||||
}
|
||||
|
||||
suggestions.push('Try refreshing the content');
|
||||
suggestions.push('Contact support if the problem persists');
|
||||
|
||||
return suggestions;
|
||||
};
|
||||
|
||||
const suggestions = getSuggestions(contentType);
|
||||
|
||||
return (
|
||||
<div className="error-renderer">
|
||||
<div className="error-header">
|
||||
<div className="error-icon">{getErrorIcon(contentType)}</div>
|
||||
<div className="error-title">
|
||||
<h3>Content Rendering Error</h3>
|
||||
<p>Unable to display content of type: <code>{contentType}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="error-details">
|
||||
<div className="error-section">
|
||||
<h4>Error Details</h4>
|
||||
<div className="error-info">
|
||||
<div className="error-field">
|
||||
<span className="field-label">Type:</span>
|
||||
<span className="field-value">{errorDetails.name}</span>
|
||||
</div>
|
||||
<div className="error-field">
|
||||
<span className="field-label">Message:</span>
|
||||
<span className="field-value">{errorDetails.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorDetails.stack && (
|
||||
<div className="error-section">
|
||||
<h4>Stack Trace</h4>
|
||||
<pre className="stack-trace">{errorDetails.stack}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="error-section">
|
||||
<h4>Suggestions</h4>
|
||||
<ul className="suggestion-list">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<li key={index} className="suggestion-item">
|
||||
{suggestion}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="error-section">
|
||||
<h4>Technical Information</h4>
|
||||
<div className="tech-info">
|
||||
<div className="tech-field">
|
||||
<span className="field-label">Content Type:</span>
|
||||
<code className="field-value">{contentType}</code>
|
||||
</div>
|
||||
<div className="tech-field">
|
||||
<span className="field-label">Error Time:</span>
|
||||
<span className="field-value">{new Date().toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="tech-field">
|
||||
<span className="field-label">Browser:</span>
|
||||
<span className="field-value">{navigator.userAgent.split(' ')[0]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="error-actions">
|
||||
<button
|
||||
className="retry-button"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
🔄 Retry
|
||||
</button>
|
||||
<button
|
||||
className="report-button"
|
||||
onClick={() => {
|
||||
const errorReport = {
|
||||
contentType,
|
||||
error: errorDetails,
|
||||
timestamp: new Date().toISOString(),
|
||||
userAgent: navigator.userAgent
|
||||
};
|
||||
console.error('Content rendering error:', errorReport);
|
||||
// Could integrate with error reporting service here
|
||||
}}
|
||||
>
|
||||
📋 Copy Error Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorRenderer;
|
||||
262
rustle/frontend/src/components/renderers/ImageRenderer.css
Normal file
262
rustle/frontend/src/components/renderers/ImageRenderer.css
Normal file
@@ -0,0 +1,262 @@
|
||||
.image-renderer {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.image-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.format-badge {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.content-type,
|
||||
.file-size {
|
||||
font-size: 11px;
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.image-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.zoom-button {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.zoom-button:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.image-renderer--preview .image-container {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.image-renderer--minimal .image-container {
|
||||
min-height: 100px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(248, 249, 250, 0.9);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.image-content {
|
||||
max-width: 100%;
|
||||
max-height: 600px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.image-content:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.image-content.preview {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.image-content.minimal {
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.zoom-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zoomed-image {
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.image-footer {
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e9ecef;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-stats {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
.image-error {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.error-content h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.error-content p {
|
||||
margin: 0.25rem 0;
|
||||
font-size: 14px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
/* Format-specific badge colors */
|
||||
.format-badge:has-text("JPG"),
|
||||
.format-badge:has-text("JPEG") {
|
||||
background: #fd7e14;
|
||||
}
|
||||
|
||||
.format-badge:has-text("PNG") {
|
||||
background: #20c997;
|
||||
}
|
||||
|
||||
.format-badge:has-text("GIF") {
|
||||
background: #e83e8c;
|
||||
}
|
||||
|
||||
.format-badge:has-text("WEBP") {
|
||||
background: #6f42c1;
|
||||
}
|
||||
|
||||
.format-badge:has-text("SVG") {
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.image-renderer {
|
||||
background: #1e1e1e;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.image-header {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.content-type,
|
||||
.file-size {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
background: rgba(45, 45, 45, 0.9);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.image-footer {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.image-stats {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
background: #3d1a1b;
|
||||
border-color: #842029;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
color: #ea868f;
|
||||
}
|
||||
|
||||
.error-content h4 {
|
||||
color: #ea868f;
|
||||
}
|
||||
|
||||
.error-content p {
|
||||
color: #f7d794;
|
||||
}
|
||||
}
|
||||
149
rustle/frontend/src/components/renderers/ImageRenderer.tsx
Normal file
149
rustle/frontend/src/components/renderers/ImageRenderer.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { useState } from 'react';
|
||||
import './ImageRenderer.css';
|
||||
|
||||
interface ImageRendererProps {
|
||||
content: string; // Base64 encoded or URL
|
||||
contentType: string;
|
||||
renderMode?: 'full' | 'preview' | 'minimal';
|
||||
}
|
||||
|
||||
export const ImageRenderer: React.FC<ImageRendererProps> = ({
|
||||
content,
|
||||
contentType,
|
||||
renderMode = 'full'
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [isZoomed, setIsZoomed] = useState(false);
|
||||
|
||||
// Determine if content is base64 or URL
|
||||
const getImageSrc = (): string => {
|
||||
if (content.startsWith('data:')) {
|
||||
return content;
|
||||
} else if (content.startsWith('http')) {
|
||||
return content;
|
||||
} else {
|
||||
// Assume base64 without data prefix
|
||||
return `data:${contentType};base64,${content}`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageLoad = () => {
|
||||
setIsLoading(false);
|
||||
setHasError(false);
|
||||
};
|
||||
|
||||
const handleImageError = () => {
|
||||
setIsLoading(false);
|
||||
setHasError(true);
|
||||
};
|
||||
|
||||
const toggleZoom = () => {
|
||||
if (renderMode === 'full') {
|
||||
setIsZoomed(!isZoomed);
|
||||
}
|
||||
};
|
||||
|
||||
const getFileExtension = (mimeType: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/gif': 'gif',
|
||||
'image/webp': 'webp',
|
||||
'image/svg+xml': 'svg'
|
||||
};
|
||||
return typeMap[mimeType] || 'img';
|
||||
};
|
||||
|
||||
const getImageDimensions = (element: HTMLImageElement): { width: number; height: number } => {
|
||||
return {
|
||||
width: element.naturalWidth,
|
||||
height: element.naturalHeight
|
||||
};
|
||||
};
|
||||
|
||||
const formatFileSize = (base64String: string): string => {
|
||||
// Rough estimation of file size from base64
|
||||
const bytes = (base64String.length * 3) / 4;
|
||||
if (bytes < 1024) return `${bytes.toFixed(0)} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<div className="image-renderer image-error">
|
||||
<div className="error-content">
|
||||
<div className="error-icon">🖼️</div>
|
||||
<h4>Unable to display image</h4>
|
||||
<p>Content Type: {contentType}</p>
|
||||
<p>The image data may be corrupted or in an unsupported format.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`image-renderer image-renderer--${renderMode}`}>
|
||||
{renderMode === 'full' && (
|
||||
<div className="image-header">
|
||||
<div className="image-info">
|
||||
<span className="format-badge">{getFileExtension(contentType).toUpperCase()}</span>
|
||||
<span className="content-type">{contentType}</span>
|
||||
<span className="file-size">{formatFileSize(content)}</span>
|
||||
</div>
|
||||
{!hasError && (
|
||||
<div className="image-actions">
|
||||
<button
|
||||
className="zoom-button"
|
||||
onClick={toggleZoom}
|
||||
title={isZoomed ? "Zoom out" : "Zoom in"}
|
||||
>
|
||||
{isZoomed ? '🔍 Zoom Out' : '🔍 Zoom In'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`image-container ${isZoomed ? 'zoomed' : ''}`}>
|
||||
{isLoading && (
|
||||
<div className="loading-overlay">
|
||||
<div className="loading-spinner">Loading image...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img
|
||||
src={getImageSrc()}
|
||||
alt="UCXL content"
|
||||
className={`image-content ${renderMode}`}
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
onClick={toggleZoom}
|
||||
style={{ cursor: renderMode === 'full' ? 'pointer' : 'default' }}
|
||||
/>
|
||||
|
||||
{isZoomed && (
|
||||
<div className="zoom-overlay" onClick={toggleZoom}>
|
||||
<img
|
||||
src={getImageSrc()}
|
||||
alt="UCXL content (zoomed)"
|
||||
className="zoomed-image"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderMode === 'full' && !hasError && !isLoading && (
|
||||
<div className="image-footer">
|
||||
<div className="image-stats">
|
||||
<span>Click to zoom</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageRenderer;
|
||||
324
rustle/frontend/src/components/renderers/JSONRenderer.css
Normal file
324
rustle/frontend/src/components/renderers/JSONRenderer.css
Normal file
@@ -0,0 +1,324 @@
|
||||
.json-renderer {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.json-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #e9ecef;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.json-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.format-badge {
|
||||
background: #6f42c1;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.size-info {
|
||||
font-size: 11px;
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.json-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.json-content {
|
||||
background: white;
|
||||
padding: 12px;
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.json-renderer--preview .json-content {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.json-renderer--minimal .json-content {
|
||||
max-height: 200px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.json-node {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.json-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.collapse-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
color: #6c757d;
|
||||
margin-right: 6px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.collapse-toggle:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.json-key {
|
||||
color: #0066cc;
|
||||
font-weight: 500;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.json-value {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.json-value--string {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.json-value--number {
|
||||
color: #009999;
|
||||
}
|
||||
|
||||
.json-value--boolean {
|
||||
color: #0066cc;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.json-value--null {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.json-value--object,
|
||||
.json-value--array {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.json-preview {
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.json-children {
|
||||
margin-left: 20px;
|
||||
border-left: 1px solid #e9ecef;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.json-truncated {
|
||||
padding: 4px 0;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.json-ellipsis {
|
||||
background: #f8f9fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* Level-based indentation */
|
||||
.level-0 { }
|
||||
.level-1 { }
|
||||
.level-2 { }
|
||||
.level-3 { opacity: 0.9; }
|
||||
.level-4 { opacity: 0.8; }
|
||||
.level-5 { opacity: 0.7; }
|
||||
|
||||
/* Error state */
|
||||
.json-error {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
background: #f5c6cb;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f1aeb5;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.error-content p {
|
||||
margin: 0 0 12px 0;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.raw-content {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.content-truncated {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e9ecef;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.json-renderer {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.json-header {
|
||||
background: #404040;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.size-info {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.json-content {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.collapse-toggle {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.collapse-toggle:hover {
|
||||
background: #404040;
|
||||
}
|
||||
|
||||
.json-key {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.json-value--string {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.json-value--number {
|
||||
color: #b5cea8;
|
||||
}
|
||||
|
||||
.json-value--boolean {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.json-value--null {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.json-value--object,
|
||||
.json-value--array {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.json-preview {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.json-children {
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.json-truncated {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.json-ellipsis {
|
||||
background: #404040;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.json-error {
|
||||
background: #3d1a1b;
|
||||
border-color: #842029;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
background: #842029;
|
||||
border-color: #6c1f28;
|
||||
}
|
||||
|
||||
.error-content p {
|
||||
color: #ea868f;
|
||||
}
|
||||
|
||||
.raw-content {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.content-truncated {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
212
rustle/frontend/src/components/renderers/JSONRenderer.tsx
Normal file
212
rustle/frontend/src/components/renderers/JSONRenderer.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import './JSONRenderer.css';
|
||||
|
||||
interface JSONRendererProps {
|
||||
content: string;
|
||||
renderMode?: 'full' | 'preview' | 'minimal';
|
||||
}
|
||||
|
||||
interface JSONNode {
|
||||
key?: string;
|
||||
value: any;
|
||||
type: 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null';
|
||||
level: number;
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
export const JSONRenderer: React.FC<JSONRendererProps> = ({
|
||||
content,
|
||||
renderMode = 'full'
|
||||
}) => {
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
||||
|
||||
const parsedJSON = useMemo(() => {
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
const toggleNode = (path: string) => {
|
||||
const newExpanded = new Set(expandedNodes);
|
||||
if (newExpanded.has(path)) {
|
||||
newExpanded.delete(path);
|
||||
} else {
|
||||
newExpanded.add(path);
|
||||
}
|
||||
setExpandedNodes(newExpanded);
|
||||
};
|
||||
|
||||
const getValueType = (value: any): JSONNode['type'] => {
|
||||
if (value === null) return 'null';
|
||||
if (Array.isArray(value)) return 'array';
|
||||
if (typeof value === 'object') return 'object';
|
||||
return typeof value as JSONNode['type'];
|
||||
};
|
||||
|
||||
const formatValue = (value: any, type: JSONNode['type']): string => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return `"${value}"`;
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return String(value);
|
||||
case 'null':
|
||||
return 'null';
|
||||
case 'array':
|
||||
return `Array(${value.length})`;
|
||||
case 'object':
|
||||
const keys = Object.keys(value);
|
||||
return `Object{${keys.length}}`;
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const renderJSONNode = (
|
||||
value: any,
|
||||
key?: string,
|
||||
level: number = 0,
|
||||
path: string = ''
|
||||
): React.ReactNode => {
|
||||
const type = getValueType(value);
|
||||
const currentPath = path ? `${path}.${key}` : key || '';
|
||||
const isExpanded = expandedNodes.has(currentPath);
|
||||
const isCollapsible = type === 'object' || type === 'array';
|
||||
|
||||
// In preview mode, limit depth and number of items
|
||||
if (renderMode === 'preview' && level > 2) {
|
||||
return (
|
||||
<div key={currentPath} className="json-truncated">
|
||||
<span className="json-ellipsis">...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={currentPath} className={`json-node json-node--${type} level-${level}`}>
|
||||
<div className="json-line">
|
||||
{isCollapsible && (
|
||||
<button
|
||||
className={`collapse-toggle ${isExpanded ? 'expanded' : 'collapsed'}`}
|
||||
onClick={() => toggleNode(currentPath)}
|
||||
>
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{key && (
|
||||
<span className="json-key">
|
||||
"{key}":
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className={`json-value json-value--${type}`}>
|
||||
{formatValue(value, type)}
|
||||
</span>
|
||||
|
||||
{isCollapsible && !isExpanded && (
|
||||
<span className="json-preview">
|
||||
{type === 'array' ? `[${value.length} items]` : `{${Object.keys(value).length} keys}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCollapsible && isExpanded && (
|
||||
<div className="json-children">
|
||||
{type === 'array' ? (
|
||||
value.map((item: any, index: number) =>
|
||||
renderJSONNode(item, String(index), level + 1, currentPath)
|
||||
)
|
||||
) : (
|
||||
Object.entries(value).map(([childKey, childValue]) =>
|
||||
renderJSONNode(childValue, childKey, level + 1, currentPath)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
console.log('JSON copied to clipboard');
|
||||
});
|
||||
};
|
||||
|
||||
const handleExpandAll = () => {
|
||||
const getAllPaths = (obj: any, prefix: string = ''): string[] => {
|
||||
const paths: string[] = [];
|
||||
if (typeof obj === 'object' && obj !== null) {
|
||||
Object.keys(obj).forEach(key => {
|
||||
const currentPath = prefix ? `${prefix}.${key}` : key;
|
||||
paths.push(currentPath);
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
paths.push(...getAllPaths(obj[key], currentPath));
|
||||
}
|
||||
});
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
if (parsedJSON) {
|
||||
setExpandedNodes(new Set(getAllPaths(parsedJSON)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollapseAll = () => {
|
||||
setExpandedNodes(new Set());
|
||||
};
|
||||
|
||||
if (!parsedJSON) {
|
||||
return (
|
||||
<div className="json-renderer json-error">
|
||||
<div className="error-header">
|
||||
<span className="error-badge">Invalid JSON</span>
|
||||
</div>
|
||||
<div className="error-content">
|
||||
<p>Unable to parse JSON content. Displaying as raw text:</p>
|
||||
<pre className="raw-content">{content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`json-renderer json-renderer--${renderMode}`}>
|
||||
{renderMode === 'full' && (
|
||||
<div className="json-header">
|
||||
<div className="json-info">
|
||||
<span className="format-badge">JSON</span>
|
||||
<span className="size-info">{content.length} characters</span>
|
||||
</div>
|
||||
<div className="json-actions">
|
||||
<button onClick={handleExpandAll} className="action-button">
|
||||
Expand All
|
||||
</button>
|
||||
<button onClick={handleCollapseAll} className="action-button">
|
||||
Collapse All
|
||||
</button>
|
||||
<button onClick={handleCopyToClipboard} className="action-button">
|
||||
📋 Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="json-content">
|
||||
{renderJSONNode(parsedJSON)}
|
||||
</div>
|
||||
|
||||
{renderMode === 'preview' && (
|
||||
<div className="content-truncated">
|
||||
<em>Preview mode - some nested content may be hidden</em>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JSONRenderer;
|
||||
247
rustle/frontend/src/components/renderers/MarkdownRenderer.css
Normal file
247
rustle/frontend/src/components/renderers/MarkdownRenderer.css
Normal file
@@ -0,0 +1,247 @@
|
||||
.markdown-renderer {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 2rem;
|
||||
margin: 0 0 1rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5rem;
|
||||
margin: 1.5rem 0 1rem 0;
|
||||
color: #34495e;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.25rem;
|
||||
margin: 1rem 0 0.75rem 0;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.markdown-content ul {
|
||||
margin: 0 0 1rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.markdown-content strong {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.markdown-content em {
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.markdown-content a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
border-bottom-color: #3498db;
|
||||
}
|
||||
|
||||
.markdown-content .inline-code {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #e83e8c;
|
||||
}
|
||||
|
||||
.markdown-content .code-block {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-content .code-block::before {
|
||||
content: attr(data-lang);
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
background: #e9ecef;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.markdown-content .code-block code {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Preview mode styling */
|
||||
.markdown-content.preview {
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-content.preview::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2rem;
|
||||
background: linear-gradient(transparent, white);
|
||||
}
|
||||
|
||||
/* Minimal mode styling */
|
||||
.markdown-content.minimal h1,
|
||||
.markdown-content.minimal h2,
|
||||
.markdown-content.minimal h3 {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.markdown-content.minimal .code-block {
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Decision Record specific styling */
|
||||
.markdown-renderer--decision-record {
|
||||
border-left: 4px solid #28a745;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.decision-record .front-matter {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.decision-record .front-matter h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #495057;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.decision-record .front-matter pre {
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.decision-content {
|
||||
border-top: 1px solid #e9ecef;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.content-truncated {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
border-top: 1px solid #e9ecef;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.markdown-renderer {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3 {
|
||||
color: #f8f9fa;
|
||||
border-color: #495057;
|
||||
}
|
||||
|
||||
.markdown-content strong {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.markdown-content em {
|
||||
color: #ced4da;
|
||||
}
|
||||
|
||||
.markdown-content a {
|
||||
color: #74c0fc;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
border-bottom-color: #74c0fc;
|
||||
}
|
||||
|
||||
.markdown-content .inline-code {
|
||||
background: #495057;
|
||||
border-color: #6c757d;
|
||||
color: #fd7e14;
|
||||
}
|
||||
|
||||
.markdown-content .code-block {
|
||||
background: #495057;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.markdown-content .code-block::before {
|
||||
background: #6c757d;
|
||||
color: #ced4da;
|
||||
}
|
||||
|
||||
.markdown-content .code-block code {
|
||||
color: #e9ecef;
|
||||
}
|
||||
|
||||
.decision-record .front-matter {
|
||||
background: #495057;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.decision-record .front-matter h4 {
|
||||
color: #ced4da;
|
||||
}
|
||||
|
||||
.decision-record .front-matter pre {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.decision-content {
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.content-truncated {
|
||||
color: #adb5bd;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
}
|
||||
116
rustle/frontend/src/components/renderers/MarkdownRenderer.tsx
Normal file
116
rustle/frontend/src/components/renderers/MarkdownRenderer.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import './MarkdownRenderer.css';
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string;
|
||||
renderMode?: 'full' | 'preview' | 'minimal';
|
||||
specialType?: 'decision-record' | 'context-metadata' | 'standard';
|
||||
}
|
||||
|
||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
content,
|
||||
renderMode = 'full',
|
||||
specialType = 'standard'
|
||||
}) => {
|
||||
// Simple markdown parsing for common elements
|
||||
const parseMarkdown = (text: string): string => {
|
||||
let html = text;
|
||||
|
||||
// Headers
|
||||
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
|
||||
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
|
||||
|
||||
// Bold and italic
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
|
||||
// Code blocks
|
||||
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
||||
return `<pre class="code-block" data-lang="${lang || 'text'}"><code>${escapeHtml(code.trim())}</code></pre>`;
|
||||
});
|
||||
|
||||
// Inline code
|
||||
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
|
||||
|
||||
// Lists
|
||||
html = html.replace(/^\s*\* (.+)$/gm, '<li>$1</li>');
|
||||
html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
||||
|
||||
// Links
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
|
||||
// Line breaks
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
const escapeHtml = (text: string): string => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
const processedContent = useMemo(() => {
|
||||
if (renderMode === 'preview') {
|
||||
// For preview mode, truncate content
|
||||
const lines = content.split('\n');
|
||||
const previewLines = lines.slice(0, 10);
|
||||
return parseMarkdown(previewLines.join('\n'));
|
||||
}
|
||||
return parseMarkdown(content);
|
||||
}, [content, renderMode]);
|
||||
|
||||
const extractFrontMatter = (text: string) => {
|
||||
const frontMatterMatch = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
if (frontMatterMatch) {
|
||||
try {
|
||||
const frontMatter = frontMatterMatch[1];
|
||||
const content = frontMatterMatch[2];
|
||||
return { frontMatter, content };
|
||||
} catch {
|
||||
return { frontMatter: null, content: text };
|
||||
}
|
||||
}
|
||||
return { frontMatter: null, content: text };
|
||||
};
|
||||
|
||||
const { frontMatter, content: mainContent } = extractFrontMatter(content);
|
||||
|
||||
const renderDecisionRecord = () => (
|
||||
<div className="decision-record">
|
||||
{frontMatter && (
|
||||
<div className="front-matter">
|
||||
<h4>Decision Metadata</h4>
|
||||
<pre>{frontMatter}</pre>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="markdown-content decision-content"
|
||||
dangerouslySetInnerHTML={{ __html: parseMarkdown(mainContent) }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderStandardMarkdown = () => (
|
||||
<div
|
||||
className={`markdown-content ${renderMode}`}
|
||||
dangerouslySetInnerHTML={{ __html: processedContent }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`markdown-renderer markdown-renderer--${specialType}`}>
|
||||
{specialType === 'decision-record' ? renderDecisionRecord() : renderStandardMarkdown()}
|
||||
|
||||
{renderMode === 'preview' && content.split('\n').length > 10 && (
|
||||
<div className="content-truncated">
|
||||
<em>Content truncated for preview...</em>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownRenderer;
|
||||
290
rustle/frontend/src/components/renderers/MetadataRenderer.css
Normal file
290
rustle/frontend/src/components/renderers/MetadataRenderer.css
Normal file
@@ -0,0 +1,290 @@
|
||||
.metadata-renderer {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 8px 8px 0 0;
|
||||
padding: 12px 16px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.metadata-renderer--preview {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metadata-renderer--minimal {
|
||||
background: #f8f9fa;
|
||||
color: #495057;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metadata-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.uri-section h4,
|
||||
.metadata-info h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.uri-breakdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.uri-part {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.uri-label {
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
min-width: 50px;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.uri-value {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.uri-value.agent {
|
||||
background: rgba(52, 152, 219, 0.3);
|
||||
}
|
||||
|
||||
.uri-value.role {
|
||||
background: rgba(155, 89, 182, 0.3);
|
||||
}
|
||||
|
||||
.uri-value.project {
|
||||
background: rgba(46, 204, 113, 0.3);
|
||||
}
|
||||
|
||||
.uri-value.task {
|
||||
background: rgba(230, 126, 34, 0.3);
|
||||
}
|
||||
|
||||
.uri-value.path {
|
||||
background: rgba(241, 196, 15, 0.3);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.metadata-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
min-width: 60px;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.info-value.title {
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.info-value.author {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.info-value.timestamp {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.info-value.version {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.info-value.source {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.tags-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tags-section h5 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.context-data {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.context-data h5 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.context-entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.context-entry {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.context-key {
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.context-value {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
flex: 1;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Minimal mode specific styles */
|
||||
.minimal-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.uri-badge {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.title-minimal {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.author-minimal {
|
||||
color: #6c757d;
|
||||
font-size: 0.75rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.metadata-header {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.uri-part,
|
||||
.info-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.uri-label,
|
||||
.info-label {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.minimal-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode specific adjustments */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.metadata-renderer--minimal {
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.title-minimal {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.author-minimal {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.uri-badge {
|
||||
background: #0d6efd;
|
||||
}
|
||||
}
|
||||
189
rustle/frontend/src/components/renderers/MetadataRenderer.tsx
Normal file
189
rustle/frontend/src/components/renderers/MetadataRenderer.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import React from 'react';
|
||||
import './MetadataRenderer.css';
|
||||
|
||||
interface EnvelopeMetadata {
|
||||
author?: string;
|
||||
title?: string;
|
||||
tags: string[];
|
||||
source?: string;
|
||||
context_data: Record<string, any>;
|
||||
}
|
||||
|
||||
interface UCXLUri {
|
||||
toString(): string;
|
||||
// Add other UCXL URI properties as needed
|
||||
}
|
||||
|
||||
interface MetadataRendererProps {
|
||||
metadata: EnvelopeMetadata;
|
||||
uri: UCXLUri;
|
||||
timestamp: string;
|
||||
version: string;
|
||||
renderMode?: 'full' | 'preview' | 'minimal';
|
||||
}
|
||||
|
||||
export const MetadataRenderer: React.FC<MetadataRendererProps> = ({
|
||||
metadata,
|
||||
uri,
|
||||
timestamp,
|
||||
version,
|
||||
renderMode = 'full'
|
||||
}) => {
|
||||
const formatTimestamp = (timestamp: string): string => {
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
} catch {
|
||||
return timestamp;
|
||||
}
|
||||
};
|
||||
|
||||
const formatURI = (uri: UCXLUri): { agent: string; role: string; project: string; task: string; path: string } => {
|
||||
const uriString = uri.toString();
|
||||
|
||||
// Parse UCXL URI format: ucxl://agent:role@project:task/temporal/path
|
||||
const match = uriString.match(/^ucxl:\/\/([^:]+):([^@]+)@([^:]+):([^\/]+)(.*)$/);
|
||||
|
||||
if (match) {
|
||||
const [, agent, role, project, task, pathPart] = match;
|
||||
// Extract temporal and path parts
|
||||
const pathMatch = pathPart.match(/^\/([^\/]*)(.*)$/);
|
||||
const temporal = pathMatch ? pathMatch[1] : '';
|
||||
const path = pathMatch ? pathMatch[2] : pathPart;
|
||||
|
||||
return {
|
||||
agent,
|
||||
role,
|
||||
project,
|
||||
task,
|
||||
path: temporal ? `${temporal}${path}` : path
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback for non-standard format
|
||||
return {
|
||||
agent: 'unknown',
|
||||
role: 'unknown',
|
||||
project: 'unknown',
|
||||
task: 'unknown',
|
||||
path: uriString
|
||||
};
|
||||
};
|
||||
|
||||
const uriParts = formatURI(uri);
|
||||
|
||||
const renderContextData = (data: Record<string, any>) => {
|
||||
const entries = Object.entries(data);
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="context-data">
|
||||
<h5>Context Data</h5>
|
||||
<div className="context-entries">
|
||||
{entries.map(([key, value]) => (
|
||||
<div key={key} className="context-entry">
|
||||
<span className="context-key">{key}:</span>
|
||||
<span className="context-value">
|
||||
{typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (renderMode === 'minimal') {
|
||||
return (
|
||||
<div className="metadata-renderer metadata-renderer--minimal">
|
||||
<div className="minimal-info">
|
||||
<span className="uri-badge">{uriParts.project}/{uriParts.task}</span>
|
||||
{metadata.title && <span className="title-minimal">{metadata.title}</span>}
|
||||
{metadata.author && <span className="author-minimal">by {metadata.author}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`metadata-renderer metadata-renderer--${renderMode}`}>
|
||||
<div className="metadata-header">
|
||||
<div className="uri-section">
|
||||
<h4>UCXL Address</h4>
|
||||
<div className="uri-breakdown">
|
||||
<div className="uri-part">
|
||||
<span className="uri-label">Agent:</span>
|
||||
<span className="uri-value agent">{uriParts.agent}</span>
|
||||
</div>
|
||||
<div className="uri-part">
|
||||
<span className="uri-label">Role:</span>
|
||||
<span className="uri-value role">{uriParts.role}</span>
|
||||
</div>
|
||||
<div className="uri-part">
|
||||
<span className="uri-label">Project:</span>
|
||||
<span className="uri-value project">{uriParts.project}</span>
|
||||
</div>
|
||||
<div className="uri-part">
|
||||
<span className="uri-label">Task:</span>
|
||||
<span className="uri-value task">{uriParts.task}</span>
|
||||
</div>
|
||||
{uriParts.path && (
|
||||
<div className="uri-part">
|
||||
<span className="uri-label">Path:</span>
|
||||
<span className="uri-value path">{uriParts.path}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metadata-info">
|
||||
{metadata.title && (
|
||||
<div className="info-row">
|
||||
<span className="info-label">Title:</span>
|
||||
<span className="info-value title">{metadata.title}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{metadata.author && (
|
||||
<div className="info-row">
|
||||
<span className="info-label">Author:</span>
|
||||
<span className="info-value author">{metadata.author}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="info-row">
|
||||
<span className="info-label">Created:</span>
|
||||
<span className="info-value timestamp">{formatTimestamp(timestamp)}</span>
|
||||
</div>
|
||||
|
||||
<div className="info-row">
|
||||
<span className="info-label">Version:</span>
|
||||
<span className="info-value version">{version.substring(0, 8)}...</span>
|
||||
</div>
|
||||
|
||||
{metadata.source && (
|
||||
<div className="info-row">
|
||||
<span className="info-label">Source:</span>
|
||||
<span className="info-value source">{metadata.source}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{metadata.tags.length > 0 && (
|
||||
<div className="tags-section">
|
||||
<h5>Tags</h5>
|
||||
<div className="tags-list">
|
||||
{metadata.tags.map((tag, index) => (
|
||||
<span key={index} className="tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderMode === 'full' && renderContextData(metadata.context_data)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetadataRenderer;
|
||||
14
rustle/frontend/src/main.tsx
Normal file
14
rustle/frontend/src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import './style.css'
|
||||
import App from './App'
|
||||
|
||||
const container = document.getElementById('root')
|
||||
|
||||
const root = createRoot(container!)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
)
|
||||
26
rustle/frontend/src/style.css
Normal file
26
rustle/frontend/src/style.css
Normal file
@@ -0,0 +1,26 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
101
rustle/frontend/src/types/Envelope.ts
Normal file
101
rustle/frontend/src/types/Envelope.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// Type definitions for UCXL content rendering
|
||||
|
||||
export interface EnvelopeContent {
|
||||
raw: string;
|
||||
content_type: string;
|
||||
encoding: string;
|
||||
}
|
||||
|
||||
export interface EnvelopeMetadata {
|
||||
author?: string;
|
||||
title?: string;
|
||||
tags: string[];
|
||||
source?: string;
|
||||
context_data: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface UCXLUri {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export interface Envelope {
|
||||
id: string;
|
||||
ucxl_uri: UCXLUri;
|
||||
content: EnvelopeContent;
|
||||
metadata: EnvelopeMetadata;
|
||||
version: string;
|
||||
parent_version?: string;
|
||||
timestamp: string;
|
||||
content_hash: string;
|
||||
}
|
||||
|
||||
// Content type categories for easier handling
|
||||
export type ContentCategory =
|
||||
| 'markdown'
|
||||
| 'code'
|
||||
| 'json'
|
||||
| 'image'
|
||||
| 'context'
|
||||
| 'decision'
|
||||
| 'unknown';
|
||||
|
||||
export const getContentCategory = (contentType: string): ContentCategory => {
|
||||
if (contentType === 'text/markdown' || contentType === 'text/x-markdown') {
|
||||
return 'markdown';
|
||||
}
|
||||
|
||||
if (contentType.startsWith('text/x-') ||
|
||||
contentType === 'text/plain' ||
|
||||
contentType.startsWith('application/javascript') ||
|
||||
contentType.startsWith('application/typescript')) {
|
||||
return 'code';
|
||||
}
|
||||
|
||||
if (contentType === 'application/json' || contentType === 'text/json') {
|
||||
return 'json';
|
||||
}
|
||||
|
||||
if (contentType.startsWith('image/')) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if (contentType === 'application/ucxl-context' ||
|
||||
contentType === 'application/ucxl-metadata') {
|
||||
return 'context';
|
||||
}
|
||||
|
||||
if (contentType === 'application/ucxl-decision' ||
|
||||
contentType === 'application/ucxl-report') {
|
||||
return 'decision';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
// Utility functions for working with envelopes
|
||||
export const createMockEnvelope = (
|
||||
uri: string,
|
||||
content: string,
|
||||
contentType: string,
|
||||
metadata: Partial<EnvelopeMetadata> = {}
|
||||
): Envelope => {
|
||||
return {
|
||||
id: `envelope-${Date.now()}`,
|
||||
ucxl_uri: { toString: () => uri },
|
||||
content: {
|
||||
raw: content,
|
||||
content_type: contentType,
|
||||
encoding: 'utf-8'
|
||||
},
|
||||
metadata: {
|
||||
author: metadata.author,
|
||||
title: metadata.title,
|
||||
tags: metadata.tags || [],
|
||||
source: metadata.source,
|
||||
context_data: metadata.context_data || {}
|
||||
},
|
||||
version: `v${Date.now()}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
content_hash: `hash-${Date.now()}`
|
||||
};
|
||||
};
|
||||
1
rustle/frontend/src/vite-env.d.ts
vendored
Normal file
1
rustle/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
44
rustle/frontend/src/wails/go/main/App.d.ts
vendored
Normal file
44
rustle/frontend/src/wails/go/main/App.d.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
// Type definitions for Wails App bindings
|
||||
|
||||
export interface UCXLValidationResult {
|
||||
valid: boolean;
|
||||
error: string;
|
||||
components: {
|
||||
agent: string;
|
||||
role: string;
|
||||
project: string;
|
||||
task: string;
|
||||
temporal: string;
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BZZZStatus {
|
||||
connected: boolean;
|
||||
mode: string;
|
||||
peers: number;
|
||||
agent: string;
|
||||
role: string;
|
||||
project: string;
|
||||
capabilities: string[];
|
||||
}
|
||||
|
||||
export interface UCXLContent {
|
||||
ucxl_uri: string;
|
||||
content: {
|
||||
raw: string;
|
||||
content_type: string;
|
||||
encoding: string;
|
||||
};
|
||||
metadata: { [key: string]: string };
|
||||
timestamp: string;
|
||||
version: string;
|
||||
content_hash: string;
|
||||
}
|
||||
|
||||
export declare function GetBZZZStatus(): Promise<BZZZStatus>;
|
||||
export declare function GetUCXLContent(uri: string): Promise<UCXLContent>;
|
||||
export declare function Greet(name: string): Promise<string>;
|
||||
export declare function PostUCXLContent(uri: string, content: string, contentType: string, metadata: { [key: string]: string }): Promise<void>;
|
||||
export declare function SearchUCXLContent(query: string, tags: string[], limit: number): Promise<UCXLContent[]>;
|
||||
export declare function ValidateUCXLAddress(uri: string): Promise<UCXLValidationResult>;
|
||||
27
rustle/frontend/src/wails/go/main/App.js
Normal file
27
rustle/frontend/src/wails/go/main/App.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function GetBZZZStatus() {
|
||||
return window['go']['main']['App']['GetBZZZStatus']();
|
||||
}
|
||||
|
||||
export function GetUCXLContent(arg1) {
|
||||
return window['go']['main']['App']['GetUCXLContent'](arg1);
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function PostUCXLContent(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['main']['App']['PostUCXLContent'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
export function SearchUCXLContent(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SearchUCXLContent'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function ValidateUCXLAddress(arg1) {
|
||||
return window['go']['main']['App']['ValidateUCXLAddress'](arg1);
|
||||
}
|
||||
31
rustle/frontend/tsconfig.json
Normal file
31
rustle/frontend/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
rustle/frontend/tsconfig.node.json
Normal file
11
rustle/frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
7
rustle/frontend/vite.config.ts
Normal file
7
rustle/frontend/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})
|
||||
14
rustle/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
14
rustle/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
@@ -0,0 +1,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function GetBZZZStatus():Promise<Record<string, any>>;
|
||||
|
||||
export function GetUCXLContent(arg1:string):Promise<Record<string, any>>;
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
|
||||
export function PostUCXLContent(arg1:string,arg2:string,arg3:string,arg4:Record<string, string>):Promise<void>;
|
||||
|
||||
export function SearchUCXLContent(arg1:string,arg2:Array<string>,arg3:number):Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function ValidateUCXLAddress(arg1:string):Promise<Record<string, any>>;
|
||||
27
rustle/frontend/wailsjs/go/main/App.js
Executable file
27
rustle/frontend/wailsjs/go/main/App.js
Executable file
@@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function GetBZZZStatus() {
|
||||
return window['go']['main']['App']['GetBZZZStatus']();
|
||||
}
|
||||
|
||||
export function GetUCXLContent(arg1) {
|
||||
return window['go']['main']['App']['GetUCXLContent'](arg1);
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function PostUCXLContent(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['main']['App']['PostUCXLContent'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
export function SearchUCXLContent(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SearchUCXLContent'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function ValidateUCXLAddress(arg1) {
|
||||
return window['go']['main']['App']['ValidateUCXLAddress'](arg1);
|
||||
}
|
||||
24
rustle/frontend/wailsjs/runtime/package.json
Normal file
24
rustle/frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
249
rustle/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
rustle/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
238
rustle/frontend/wailsjs/runtime/runtime.js
Normal file
238
rustle/frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
37
rustle/go.mod
Normal file
37
rustle/go.mod
Normal file
@@ -0,0 +1,37 @@
|
||||
module rustle
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.5
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.10.2
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
81
rustle/go.sum
Normal file
81
rustle/go.sum
Normal file
@@ -0,0 +1,81 @@
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.10.2 h1:29U+c5PI4K4hbx8yFbFvwpCuvqK9VgNv8WGobIlKlXk=
|
||||
github.com/wailsapp/wails/v2 v2.10.2/go.mod h1:XuN4IUOPpzBrHUkEd7sCU5ln4T/p1wQedfxP7fKik+4=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
36
rustle/main.go
Normal file
36
rustle/main.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "rustle",
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
13
rustle/wails.json
Normal file
13
rustle/wails.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "rustle",
|
||||
"outputfilename": "rustle",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "anthonyrawlins",
|
||||
"email": "anthonyrawlins@users.noreply.github.com"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user