Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Readme

This Frankenstein of an example shows how well Hono, htmx, and Alpine play together.

  • Hono serves the frameworks, API calls, and functions
  • htmx handles ajax requests, and can very powerfully request html and other content to swap out the front-end
  • alpine handles app-like reactivity without having to always resort to server round trips
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { Hono } from "npm:hono";
import { cors } from 'npm:hono/cors';
import { stream, streamSSE } from "https://deno.land/x/hono@v4.3.11/helper.ts";
import { OpenAI } from "npm:openai";
import { ai } from "https://esm.town/v/yawnxyz/ai";
const app = new Hono();
const openai = new OpenAI();
app.use('*', cors({
origin: '*',
allowMethods: ['GET'],
allowHeaders: ['Content-Type'],
}));
const SOURCE_URL = ""; // leave blank for deno deploy / native
// in this example we use a custom function instead of htmx's custom sse extension, since it never closes!
function getResponse({prompt}) {
const outputDiv = document.getElementById('output');
prompt = prompt ||document.getElementById('prompt').value;
console.log('getting response...', prompt)
const url = "/chat?p=" + encodeURIComponent(prompt);
outputDiv.textContent = 'Loading...';
const eventSource = new EventSource(url);
let isFirstChunk = true;
eventSource.onmessage = (event) => {
if (event.data === "[DONE]") {
eventSource.close();
} else {
const data = JSON.parse(event.data);
if (data.error) {
outputDiv.textContent = "Error: " + data.error;
} else {
if (isFirstChunk) {
outputDiv.textContent = data.token;
isFirstChunk = false;
} else {
outputDiv.textContent += data.token;
}
}
}
};
eventSource.onerror = (error) => {
console.error('EventSource error:', error);
outputDiv.textContent += 'Error occurred.';
};
eventSource.addEventListener('close', () => {
console.log('Connection closed.');
});
}
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script src="https://unpkg.com/htmx.org"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
#output {
white-space: pre-wrap;
border: 1px solid #ccc;
padding: 10px;
min-height: 100px;
}
</style>
</head>
<body x-data="{input: ''}">
<h1>Hono / Htmx / Alpine Streaming + UX Example</h1>
<label for="prompt">Prompt:</label>
<!-- <button hx-ext="sse" sse-connect="#prompt.value && /chat?prompt=#prompt.value" sse-swap="message" hx-trigger="sse:message" hx-target="#output">Submit</button> -->
<!-- <input type="text" id="prompt" value="tell me a joke" /> -->
<!-- alpine code does better for non-ajaxy stuff lol // @input.debounce.850ms="getResponse" -->
<input type="text" id="prompt" value="tell me a joke"
x-model="input"
type="text"
placeholder="Type here..."
>
<!-- uses htmx instead of alpine, but passes alpine's input data value into htmx lol -->
<button id="myButton" hx-trigger="click" hx-on:click="getResponse({prompt: this.input})" >Prompt</button>
<div id="output"></div>
<script>
// instead of htmx sse we pass it a custom function
${getResponse}
yawnxyz-aihonohtmxalpinestreamingexample.web.val.run
June 11, 2024