<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][README.md]" value="# Stream Processor for RivetKit

Example project demonstrating real-time top-K stream processing with [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-dev/rivetkit)

[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues)

## Getting Started

### Prerequisites

- Node.js 18+

### Installation

```sh
git clone https://github.com/rivet-dev/rivetkit
cd rivetkit/examples/stream
npm install
```

### Development

```sh
npm run dev
```

Open your browser to `http://localhost:3000`

## Features

- **Top-K Processing**: Maintains the top 3 highest values in real-time
- **Real-time Updates**: All connected clients see changes immediately
- **Stream Statistics**: Total count, highest value, and live metrics
- **Interactive Input**: Add custom values or generate random numbers
- **Reset Functionality**: Clear the stream and start fresh
- **Responsive Design**: Clean, modern interface with live statistics

## How it works

This stream processor demonstrates:

1. **Top-K Algorithm**: Efficiently maintains the top 3 values using insertion sort
2. **Real-time Broadcasting**: Updates are instantly sent to all connected clients
3. **State Management**: Persistent tracking of values and statistics
4. **Event-driven Updates**: Live UI updates when new values are processed
5. **Collaborative Experience**: Multiple users can add values simultaneously

## Architecture

- **Backend**: RivetKit actor managing stream state and top-K algorithm
- **Frontend**: React application with real-time stream visualization
- **State Management**: Server-side state with client-side event subscriptions
- **Algorithm**: Insertion-based top-K maintenance with O(k) complexity

## Stream Processing Algorithm

### Value Insertion
```typescript
// Insert new value maintaining sorted order
const insertAt = topValues.findIndex(v =&gt; value &gt; v);
if (insertAt !== -1) {
    topValues.splice(insertAt, 0, value);
}

// Keep only top 3 values
if (topValues.length &gt; 3) {
    topValues.length = 3;
}
```

### Performance Characteristics
- **Time Complexity**: O(k) per insertion where k=3
- **Space Complexity**: O(k) for storing top values
- **Memory Efficient**: Only stores top values, not entire stream
- **Real-time**: Sub-millisecond processing for new values

## Use Cases

This pattern is perfect for:

- **Leaderboards**: Gaming high scores, competition rankings
- **Metrics Monitoring**: Top error rates, highest traffic spikes
- **Social Features**: Most popular posts, trending content
- **Analytics Dashboards**: Key performance indicators
- **Real-time Alerts**: Threshold monitoring and notifications

## Extending

This stream processor can be enhanced with:

- **Configurable K**: Allow different top-K sizes (top 5, top 10, etc.)
- **Time Windows**: Top values within specific time periods
- **Multiple Streams**: Separate processors for different categories
- **Persistence**: Database storage for stream history
- **Complex Events**: Pattern detection and complex event processing
- **Aggregations**: Sum, average, and other statistical operations
- **Filters**: Value range filtering and validation
- **Rate Limiting**: Throttle input to prevent spam

## Stream Processing Concepts

### Top-K Algorithms
- **Heap-based**: Efficient for large K values
- **Sort-based**: Simple implementation for small K
- **Probabilistic**: Approximate results for massive streams

### Real-time Considerations
- **Latency**: Sub-millisecond processing requirements
- **Throughput**: Handling high-volume input streams
- **Memory**: Bounded memory usage regardless of stream size
- **Accuracy**: Exact vs. approximate results trade-offs

## Testing

The example includes basic structural tests. For production use, consider adding:

- **Algorithm correctness**: Verify top-K accuracy
- **Concurrency testing**: Multiple simultaneous inputs
- **Performance testing**: High-volume stream simulation
- **Edge cases**: Duplicate values, negative numbers, overflow handling

## License

Apache 2.0">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;example-stream&quot;,&quot;version&quot;:&quot;2.0.21&quot;,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;concurrently \&quot;tsx --watch src/backend/server.ts\&quot; \&quot;vite\&quot;&quot;,&quot;build&quot;:&quot;vite build&quot;,&quot;preview&quot;:&quot;vite preview&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest&quot;},&quot;dependencies&quot;:{&quot;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/rivetkit@1784d8f4835dfe0620f43e819ff860dd7dd02821&quot;,&quot;@rivetkit/react&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@1784d8f4835dfe0620f43e819ff860dd7dd02821&quot;,&quot;react&quot;:&quot;^18.2.0&quot;,&quot;react-dom&quot;:&quot;^18.2.0&quot;},&quot;devDependencies&quot;:{&quot;@types/node&quot;:&quot;^20.0.0&quot;,&quot;@types/react&quot;:&quot;^18.2.0&quot;,&quot;@types/react-dom&quot;:&quot;^18.2.0&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^4.0.0&quot;,&quot;concurrently&quot;:&quot;^8.2.0&quot;,&quot;tsx&quot;:&quot;^4.0.0&quot;,&quot;typescript&quot;:&quot;^5.0.0&quot;,&quot;vite&quot;:&quot;^5.0.0&quot;,&quot;vitest&quot;:&quot;^3.1.1&quot;}}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
	&quot;compilerOptions&quot;: {
		&quot;target&quot;: &quot;ES2020&quot;,
		&quot;lib&quot;: [&quot;ES2020&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;],
		&quot;module&quot;: &quot;ESNext&quot;,
		&quot;skipLibCheck&quot;: true,
		&quot;moduleResolution&quot;: &quot;bundler&quot;,
		&quot;allowImportingTsExtensions&quot;: true,
		&quot;resolveJsonModule&quot;: true,
		&quot;isolatedModules&quot;: true,
		&quot;noEmit&quot;: true,
		&quot;jsx&quot;: &quot;react-jsx&quot;,
		&quot;strict&quot;: true,
		&quot;noUnusedLocals&quot;: true,
		&quot;noUnusedParameters&quot;: true,
		&quot;noFallthroughCasesInSwitch&quot;: true
	},
	&quot;include&quot;: [&quot;src&quot;, &quot;tests&quot;],
	&quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
	&quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
	&quot;extends&quot;: [&quot;//&quot;]
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import react from &quot;@vitejs/plugin-react&quot;;
import { defineConfig } from &quot;vite&quot;;

export default defineConfig({
	plugins: [react()],
	root: &quot;src/frontend&quot;,
	server: {
		port: 3000,
	},
	build: {
		outDir: &quot;../../dist&quot;,
		emptyOutDir: true,
	},
});
">
<input type="hidden" name="project[files][vitest.config.ts]" value="import { defineConfig } from &quot;vitest/config&quot;;

export default defineConfig({
	test: {
		environment: &quot;node&quot;,
	},
});
">
<input type="hidden" name="project[files][tests/stream.test.ts]" value="import { setupTest } from &quot;rivetkit/test&quot;;
import { expect, test } from &quot;vitest&quot;;
import { registry } from &quot;../src/backend/registry&quot;;

test(&quot;Stream processor maintains top 3 values&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const stream = client.streamProcessor.getOrCreate([&quot;test-top3&quot;]);

	// Initial state should be empty
	const initial = await stream.getTopValues();
	expect(initial).toEqual([]);

	// Add first value
	const result1 = await stream.addValue(10);
	expect(result1).toEqual([10]);

	// Add second value (lower)
	const result2 = await stream.addValue(5);
	expect(result2).toEqual([10, 5]);

	// Add third value (higher)
	const result3 = await stream.addValue(15);
	expect(result3).toEqual([15, 10, 5]);

	// Add fourth value (should replace lowest)
	const result4 = await stream.addValue(8);
	expect(result4).toEqual([15, 10, 8]);

	// Add fifth value (should replace middle)
	const result5 = await stream.addValue(12);
	expect(result5).toEqual([15, 12, 10]);
});

test(&quot;Stream processor tracks statistics correctly&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const stream = client.streamProcessor.getOrCreate([&quot;test-stats&quot;]);

	// Initial stats
	const initialStats = await stream.getStats();
	expect(initialStats).toEqual({
		topValues: [],
		totalCount: 0,
		highestValue: null,
	});

	// Add some values
	await stream.addValue(20);
	await stream.addValue(30);
	await stream.addValue(10);

	const stats = await stream.getStats();
	expect(stats).toEqual({
		topValues: [30, 20, 10],
		totalCount: 3,
		highestValue: 30,
	});

	// Add more values to test count tracking
	await stream.addValue(5);
	await stream.addValue(25);

	const finalStats = await stream.getStats();
	expect(finalStats.totalCount).toBe(5);
	expect(finalStats.topValues).toEqual([30, 25, 20]);
	expect(finalStats.highestValue).toBe(30);
});

test(&quot;Stream processor handles duplicate values&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const stream = client.streamProcessor.getOrCreate([&quot;test-duplicates&quot;]);

	// Add duplicate values
	await stream.addValue(10);
	await stream.addValue(10);
	await stream.addValue(10);

	const result = await stream.getTopValues();
	expect(result).toEqual([10, 10, 10]);

	const stats = await stream.getStats();
	expect(stats.totalCount).toBe(3);
	expect(stats.highestValue).toBe(10);
});

test(&quot;Stream processor reset functionality&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const stream = client.streamProcessor.getOrCreate([&quot;test-reset&quot;]);

	// Add some values
	await stream.addValue(100);
	await stream.addValue(200);
	await stream.addValue(50);

	// Verify state before reset
	const beforeReset = await stream.getStats();
	expect(beforeReset.totalCount).toBe(3);
	expect(beforeReset.topValues).toEqual([200, 100, 50]);

	// Reset the stream
	const resetResult = await stream.reset();
	expect(resetResult).toEqual({
		topValues: [],
		totalCount: 0,
		highestValue: null,
	});

	// Verify state after reset
	const afterReset = await stream.getStats();
	expect(afterReset).toEqual({
		topValues: [],
		totalCount: 0,
		highestValue: null,
	});
});

test(&quot;Stream processor handles edge case values&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const stream = client.streamProcessor.getOrCreate([&quot;test-edge-cases&quot;]);

	// Test with zero
	await stream.addValue(0);
	expect(await stream.getTopValues()).toEqual([0]);

	// Test with negative numbers
	await stream.addValue(-5);
	await stream.addValue(-1);
	expect(await stream.getTopValues()).toEqual([0, -1, -5]);

	// Test with very large numbers
	await stream.addValue(1000000);
	expect(await stream.getTopValues()).toEqual([1000000, 0, -1]);

	const stats = await stream.getStats();
	expect(stats.totalCount).toBe(4);
	expect(stats.highestValue).toBe(1000000);
});
">
<input type="hidden" name="project[files][src/backend/registry.ts]" value="import { actor, setup } from &quot;rivetkit&quot;;

export type StreamState = {
	topValues: number[];
};

const streamProcessor = actor({
	// Persistent state that survives restarts: https://rivet.dev/docs/actors/state
	state: {
		topValues: [] as number[],
		totalValues: 0,
	},

	actions: {
		// Callable functions from clients: https://rivet.dev/docs/actors/actions
		getTopValues: (c) =&gt; c.state.topValues,

		getStats: (c) =&gt; ({
			topValues: c.state.topValues,
			totalCount: c.state.totalValues,
			highestValue:
				c.state.topValues.length &gt; 0 ? c.state.topValues[0] : null,
		}),

		addValue: (c, value: number) =&gt; {
			// State changes are automatically persisted
			c.state.totalValues++;

			// Insert new value if needed
			const insertAt = c.state.topValues.findIndex((v) =&gt; value &gt; v);
			if (insertAt === -1 &amp;&amp; c.state.topValues.length &lt; 3) {
				// Add to end if not better than existing values but we have space
				c.state.topValues.push(value);
			} else if (insertAt !== -1) {
				// Insert at the correct position
				c.state.topValues.splice(insertAt, 0, value);
			}

			// Keep only top 3
			if (c.state.topValues.length &gt; 3) {
				c.state.topValues.length = 3;
			}

			// Sort descending to ensure correct order
			c.state.topValues.sort((a, b) =&gt; b - a);

			const result = {
				topValues: c.state.topValues,
				totalCount: c.state.totalValues,
				highestValue:
					c.state.topValues.length &gt; 0 ? c.state.topValues[0] : null,
			};

			// Send events to all connected clients: https://rivet.dev/docs/actors/events
			c.broadcast(&quot;updated&quot;, result);

			return c.state.topValues;
		},

		reset: (c) =&gt; {
			c.state.topValues = [];
			c.state.totalValues = 0;

			const result = {
				topValues: c.state.topValues,
				totalCount: c.state.totalValues,
				highestValue: null,
			};

			c.broadcast(&quot;updated&quot;, result);

			return result;
		},
	},
});

// Register actors for use: https://rivet.dev/docs/setup
export const registry = setup({
	use: { streamProcessor },
});
">
<input type="hidden" name="project[files][src/backend/server.ts]" value="import { registry } from &quot;./registry&quot;;

registry.start();
">
<input type="hidden" name="project[files][src/frontend/App.tsx]" value="import { createRivetKit } from &quot;@rivetkit/react&quot;;
import { useEffect, useState } from &quot;react&quot;;
import type { registry } from &quot;../backend/registry&quot;;

const { useActor } = createRivetKit&lt;typeof registry&gt;(&quot;http://localhost:8080&quot;);

export function App() {
	const [topValues, setTopValues] = useState&lt;number[]&gt;([]);
	const [newValue, setNewValue] = useState&lt;number&gt;(0);
	const [totalCount, setTotalCount] = useState&lt;number&gt;(0);
	const [highestValue, setHighestValue] = useState&lt;number | null&gt;(null);

	const streamProcessor = useActor({
		name: &quot;streamProcessor&quot;,
		key: [&quot;global&quot;],
	});

	// Load initial stats
	useEffect(() =&gt; {
		if (streamProcessor.connection) {
			streamProcessor.connection.getStats().then((stats) =&gt; {
				setTopValues(stats.topValues);
				setTotalCount(stats.totalCount);
				setHighestValue(stats.highestValue);
			});
		}
	}, [streamProcessor.connection]);

	// Listen for updates from other clients
	streamProcessor.useEvent(&quot;updated&quot;, ({ topValues, totalCount, highestValue }: {
		topValues: number[];
		totalCount: number;
		highestValue: number | null;
	}) =&gt; {
		setTopValues(topValues);
		setTotalCount(totalCount);
		setHighestValue(highestValue);
	});

	// Add a new value to the stream
	const handleAddValue = async () =&gt; {
		if (streamProcessor.connection &amp;&amp; !isNaN(newValue)) {
			await streamProcessor.connection.addValue(newValue);
			setNewValue(0);
		}
	};

	// Reset the stream
	const handleReset = async () =&gt; {
		if (streamProcessor.connection) {
			const result = await streamProcessor.connection.reset();
			setTopValues(result.topValues);
			setTotalCount(result.totalCount);
			setHighestValue(result.highestValue);
		}
	};

	// Handle form submission
	const handleSubmit = (e: React.FormEvent) =&gt; {
		e.preventDefault();
		handleAddValue();
	};

	// Handle random value generation
	const handleRandomValue = () =&gt; {
		const randomValue = Math.floor(Math.random() * 1000) + 1;
		setNewValue(randomValue);
	};

	return (
		&lt;div className=&quot;app-container&quot;&gt;
			&lt;div className=&quot;header&quot;&gt;
				&lt;h1&gt;Stream Processor&lt;/h1&gt;
				&lt;p&gt;Real-time top-3 value tracking with RivetKit&lt;/p&gt;
			&lt;/div&gt;

			&lt;div className=&quot;info-box&quot;&gt;
				&lt;h3&gt;How it works&lt;/h3&gt;
				&lt;p&gt;
					This stream processor maintains the top 3 highest values in real-time. 
					Add numbers and watch as the system automatically keeps track of the highest values. 
					All connected clients see updates immediately when new values are added.
				&lt;/p&gt;
			&lt;/div&gt;

			&lt;div className=&quot;content&quot;&gt;
				&lt;div className=&quot;top-values-section&quot;&gt;
					&lt;div className=&quot;top-values-list&quot;&gt;
						&lt;h3&gt;🏆 Top 3 Values&lt;/h3&gt;
						{topValues.length === 0 ? (
							&lt;div className=&quot;empty-state&quot;&gt;
								No values added yet.&lt;br /&gt;
								Add some numbers to get started!
							&lt;/div&gt;
						) : (
							topValues.map((value, index) =&gt; (
								&lt;div key={`${value}-${index}`} className=&quot;value-item&quot;&gt;
									&lt;span className=&quot;value-rank&quot;&gt;#{index + 1}&lt;/span&gt;
									&lt;span className=&quot;value-number&quot;&gt;{value.toLocaleString()}&lt;/span&gt;
								&lt;/div&gt;
							))
						)}
					&lt;/div&gt;
				&lt;/div&gt;

				&lt;div className=&quot;input-section&quot;&gt;
					&lt;form onSubmit={handleSubmit} className=&quot;input-form&quot;&gt;
						&lt;h3&gt;Add New Value&lt;/h3&gt;
						
						&lt;div className=&quot;input-group&quot;&gt;
							&lt;label htmlFor=&quot;value-input&quot;&gt;Number:&lt;/label&gt;
							&lt;input
								id=&quot;value-input&quot;
								type=&quot;number&quot;
								value={newValue || &quot;&quot;}
								onChange={(e) =&gt; setNewValue(Number(e.target.value))}
								placeholder=&quot;Enter any number...&quot;
								disabled={!streamProcessor.connection}
							/&gt;
						&lt;/div&gt;

						&lt;button 
							type=&quot;submit&quot; 
							className=&quot;submit-button&quot;
							disabled={!streamProcessor.connection || isNaN(newValue)}
						&gt;
							Add to Stream
						&lt;/button&gt;
					&lt;/form&gt;

					&lt;div style={{ marginTop: &quot;15px&quot;, display: &quot;flex&quot;, gap: &quot;10px&quot; }}&gt;
						&lt;button 
							onClick={handleRandomValue}
							style={{
								flex: 1,
								padding: &quot;8px&quot;,
								backgroundColor: &quot;#28a745&quot;,
								color: &quot;white&quot;,
								border: &quot;none&quot;,
								borderRadius: &quot;4px&quot;,
								cursor: &quot;pointer&quot;
							}}
						&gt;
							Random Value
						&lt;/button&gt;
						&lt;button 
							onClick={handleReset}
							disabled={!streamProcessor.connection}
							style={{
								flex: 1,
								padding: &quot;8px&quot;,
								backgroundColor: &quot;#dc3545&quot;,
								color: &quot;white&quot;,
								border: &quot;none&quot;,
								borderRadius: &quot;4px&quot;,
								cursor: &quot;pointer&quot;
							}}
						&gt;
							Reset Stream
						&lt;/button&gt;
					&lt;/div&gt;
				&lt;/div&gt;
			&lt;/div&gt;

			&lt;div className=&quot;stats&quot;&gt;
				&lt;div className=&quot;stat-item&quot;&gt;
					&lt;div className=&quot;stat-value&quot;&gt;{totalCount}&lt;/div&gt;
					&lt;div className=&quot;stat-label&quot;&gt;Total Values&lt;/div&gt;
				&lt;/div&gt;
				&lt;div className=&quot;stat-item&quot;&gt;
					&lt;div className=&quot;stat-value&quot;&gt;{highestValue?.toLocaleString() || &quot;—&quot;}&lt;/div&gt;
					&lt;div className=&quot;stat-label&quot;&gt;Highest Value&lt;/div&gt;
				&lt;/div&gt;
				&lt;div className=&quot;stat-item&quot;&gt;
					&lt;div className=&quot;stat-value&quot;&gt;{topValues.length}&lt;/div&gt;
					&lt;div className=&quot;stat-label&quot;&gt;Top Values Count&lt;/div&gt;
				&lt;/div&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
}
">
<input type="hidden" name="project[files][src/frontend/index.html]" value="&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Stream Processor - RivetKit&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f0f0f0;
        }
        .app-container {
            max-width: 800px;
            margin: 0 auto;
            background-color: white;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .header {
            text-align: center;
            margin-bottom: 30px;
        }
        .header h1 {
            color: #333;
            margin: 0;
        }
        .header p {
            color: #666;
            margin: 10px 0;
        }
        .content {
            display: flex;
            gap: 30px;
        }
        .top-values-section {
            flex: 1;
        }
        .input-section {
            flex: 1;
        }
        .top-values-list {
            background-color: #f8f9fa;
            border: 2px solid #e9ecef;
            border-radius: 8px;
            padding: 20px;
            min-height: 150px;
        }
        .top-values-list h3 {
            margin: 0 0 15px 0;
            color: #495057;
            text-align: center;
        }
        .value-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 12px 15px;
            margin: 8px 0;
            background-color: white;
            border: 1px solid #dee2e6;
            border-radius: 6px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }
        .value-rank {
            font-weight: bold;
            color: #6c757d;
            font-size: 14px;
        }
        .value-number {
            font-size: 18px;
            font-weight: bold;
            color: #495057;
        }
        .empty-state {
            text-align: center;
            color: #6c757d;
            font-style: italic;
            padding: 40px 20px;
        }
        .input-form {
            background-color: #f8f9fa;
            border: 2px solid #e9ecef;
            border-radius: 8px;
            padding: 20px;
        }
        .input-form h3 {
            margin: 0 0 15px 0;
            color: #495057;
            text-align: center;
        }
        .input-group {
            margin-bottom: 15px;
        }
        .input-group label {
            display: block;
            margin-bottom: 8px;
            color: #495057;
            font-weight: bold;
        }
        .input-group input {
            width: 100%;
            padding: 12px;
            border: 1px solid #ced4da;
            border-radius: 4px;
            font-size: 16px;
            box-sizing: border-box;
        }
        .input-group input:focus {
            outline: none;
            border-color: #80bdff;
            box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
        }
        .submit-button {
            width: 100%;
            padding: 12px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: background-color 0.2s;
        }
        .submit-button:hover {
            background-color: #0056b3;
        }
        .submit-button:disabled {
            background-color: #6c757d;
            cursor: not-allowed;
        }
        .info-box {
            background-color: #e8f4f8;
            border: 1px solid #b8d4da;
            border-radius: 6px;
            padding: 15px;
            margin-bottom: 20px;
        }
        .info-box h3 {
            margin: 0 0 10px 0;
            color: #2c5aa0;
        }
        .info-box p {
            margin: 0;
            color: #555;
            line-height: 1.5;
        }
        .stats {
            display: flex;
            justify-content: space-around;
            margin-top: 20px;
            padding: 15px;
            background-color: #f8f9fa;
            border-radius: 6px;
        }
        .stat-item {
            text-align: center;
        }
        .stat-value {
            font-size: 24px;
            font-weight: bold;
            color: #007bff;
        }
        .stat-label {
            color: #6c757d;
            font-size: 14px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot; src=&quot;/main.tsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;">
<input type="hidden" name="project[files][src/frontend/main.tsx]" value="import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import { App } from &quot;./App&quot;;

const root = document.getElementById(&quot;root&quot;);
if (!root) throw new Error(&quot;Root element not found&quot;);

createRoot(root).render(
	&lt;StrictMode&gt;
		&lt;App /&gt;
	&lt;/StrictMode&gt;
);">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="example-stream">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>