orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1<!doctype html>
2<html lang="en" data-theme="dark">
3<head>
4<meta charset="utf-8">
5<!-- interactive-widget makes the on screen keyboard shrink the layout viewport
6 rather than sit on top of it, which is what keeps the composer visible. -->
7<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
8<title>chat</title>
9<meta name="description" content="A conversation with a small model running on hardware you own.">
10<link rel="stylesheet" href="{{asset "static/app.css"}}">
11<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' fill='%230e0d0a'/><rect x='3' y='7' width='7' height='2' fill='%236b9e78'/></svg>">
12 <!-- Analytics, first party and on this estate. The id is identity rather than
13 a credential, so it is a constant here like every other site's. -->
14 <script>
15 (function (m, e, t, r, i, c, s) {
16 m.collectorQueue = m.collectorQueue || r;
17 m.collectorServer = c;
18 m.collectorId = s;
19 m.collectorScript = e.createElement(t);
20 m.collectorScript.src = c + i;
21 e.head.appendChild(m.collectorScript);
22 })(window, document, "script", [], "/static/collector.js",
23 "https://analytics.bythewood.me", "73f89553-a3fa-4c4a-b567-9a9a0550240b");
24 </script>
25</head>
26<body>
27<div class="scan" aria-hidden="true"></div>
28<div class="vignette" aria-hidden="true"></div>
29
30<div class="shell">
31 <div class="scrim" id="scrim" hidden></div>
32
33 <aside class="side" id="side">
34 <div class="side-head">
35 <a class="brand" href="/">
36 <span class="key">C<i class="cur"></i></span>
37 <span class="brand-name">chat</span>
38 </a>
39 <button class="icon-btn" id="side-close" title="Hide history" aria-label="Hide history">✕</button>
40 </div>
41
42 <button class="new-chat" id="new-chat">
43 <span class="plus">+</span> New conversation
44 </button>
45
46 <div class="side-label">// History</div>
47 <input class="filter" id="filter" type="search" placeholder="Filter" aria-label="Filter history" hidden>
48 <nav class="convs" id="convs">
49 {{range .Conversations}}
50 <a class="conv" data-id="{{.ID}}" href="/c/{{.ID}}">
51 <span class="conv-title">{{.Title}}</span>
52 <span class="conv-when">{{when .Updated}}</span>
53 <button class="conv-del" data-del="{{.ID}}" title="Delete" aria-label="Delete conversation">✕</button>
54 </a>
55 {{else}}
56 <p class="empty">Nothing yet. Ask something.</p>
57 {{end}}
58 </nav>
59
60 <div class="side-foot">
61 <div class="side-label">// Session</div>
62 <label class="incog" for="incognito">
63 <input type="checkbox" id="incognito">
64 <span class="incog-box" aria-hidden="true"></span>
65 <span class="incog-text">
66 <b>Incognito</b>
67 <small>Nothing is written to disk</small>
68 </span>
69 </label>
70 <div class="side-stats">
71 <span><b id="stat-convs">{{.Stats.Conversations}}</b> saved</span>
72 <span><b id="stat-msgs">{{.Stats.Messages}}</b> messages</span>
73 </div>
74 <button class="wipe" id="wipe">Delete all history</button>
75 </div>
76 </aside>
77
78 <main class="main">
79 <header class="bar">
80 <button class="icon-btn" id="side-open" title="Show history" aria-label="Show history">☰</button>
81 <div class="bar-title" id="bar-title">New conversation</div>
82 <div class="bar-right">
83 <span class="incog-flag" id="incog-flag" hidden>INCOGNITO</span>
84 <span class="warn-flag" id="search-down" hidden title="DuckDuckGo is rate limiting this address, so nothing can be looked up until it clears">NO SEARCH</span>
85 <span class="meter" id="meter" title="Context used by the last turn" hidden>
86 <span class="meter-num" id="meter-num">0</span>
87 <span class="meter-track"><i class="meter-fill" id="meter-fill"></i></span>
88 <span class="meter-cap" id="meter-cap">{{.Ctx}}</span>
89 </span>
90 <span class="tps" id="tps" title="Generation speed of the last turn" hidden></span>
91 <span class="model" title="The model answering">
92 <i class="dot" id="model-dot"></i>{{.Model}}
93 </span>
94 </div>
95 </header>
96
97 <div class="thread" id="thread">
98 <section class="welcome" id="welcome">
99 <div class="eyebrow"><i class="dot on"></i> on this machine <span class="sep">/</span> self-hosted <span class="sep">/</span> no telemetry</div>
100 <h1>A conversation with a model on <span class="hl">hardware you own</span>.</h1>
101 <p>It searches the web, reads pages, checks the weather and the markets, looks up scores and odds, and verifies a song exists before it names one. Nothing leaves this machine except the pages it fetches for you.</p>
102 <p class="welcome-hint">Press <kbd>Ctrl</kbd><kbd>/</kbd> for keyboard shortcuts.</p>
103 <div class="chips">
104 <button class="chip" data-ask="What is the weather looking like this weekend, and how should I pack for camping in it?">Weather this weekend</button>
105 <button class="chip" data-ask="Give me a read on the market right now: the S&P, the Nasdaq, gold and bitcoin.">Market read</button>
106 <button class="chip" data-ask="Build me a 10 song playlist off Turnstile - Blackout, and check every song is real.">Build a playlist</button>
107 <button class="chip" data-ask="Write a Go function that wraps an http.Handler and logs each request as one JSON line, keeping http.Flusher working.">Write some Go</button>
108 </div>
109 </section>
110 </div>
111
112 <div class="composer">
113 <div class="tray" id="tray" hidden></div>
114 <form class="ask" id="ask">
115 <button type="button" class="attach" id="attach" title="Attach files" aria-label="Attach files">+</button>
116 <textarea id="input" rows="1" placeholder="Ask anything" autocomplete="off" spellcheck="true"></textarea>
117 <button type="submit" id="send" title="Send" aria-label="Send">▶</button>
118 <button type="button" id="stop" title="Stop" aria-label="Stop" hidden>■</button>
119 </form>
120 <input type="file" id="picker" multiple hidden>
121 <p class="foot-note">
122 <span id="foot-state">Enter to send, shift and enter for a new line.</span>
123 <span class="foot-right">
124 <button type="button" class="linkish keys-hint" id="keys-open" title="Keyboard shortcuts">
125 Shortcuts <kbd>Ctrl</kbd><kbd>/</kbd>
126 </button>
127 <button type="button" class="linkish" id="mem-open" title="What it remembers about you">Memory</button>
128 <a href="{{.Source}}" target="_blank" rel="noopener">View source</a>
129 </span>
130 </p>
131 </div>
132 </main>
133</div>
134
135<div class="sheet" id="mem-sheet" hidden>
136 <div class="sheet-card mem-card" role="dialog" aria-modal="true" aria-labelledby="mem-title">
137 <div class="sheet-head">
138 <span class="side-label" id="mem-title" style="padding:0">// Memory</span>
139 <button class="icon-btn" id="mem-close" aria-label="Close">✕</button>
140 </div>
141
142 <p class="mem-lede">One sentence each, carried into a turn only when your question touches
143 them. It picks things up as you talk. Deleting is yours, writing goes through the model.</p>
144
145 <div class="mem-msg" id="mem-msg" hidden></div>
146
147 <form class="teach" id="mem-form">
148 <textarea id="mem-said" rows="2" placeholder="Remember that… or forget that…"></textarea>
149 <button type="submit" id="mem-send">Tell it</button>
150 </form>
151
152 <div class="mem-list" id="mem-list"></div>
153
154 <div class="mem-foot">
155 <span id="mem-count"></span>
156 <button type="button" class="danger" id="mem-forget">Forget everything</button>
157 </div>
158 </div>
159</div>
160
161<div class="sheet" id="keys-sheet" hidden>
162 <div class="sheet-card" role="dialog" aria-modal="true" aria-labelledby="keys-title">
163 <div class="sheet-head">
164 <span class="side-label" id="keys-title" style="padding:0">// Keyboard</span>
165 <button class="icon-btn" id="keys-close" aria-label="Close">✕</button>
166 </div>
167 <dl class="keys">
168 <dt><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>O</kbd></dt><dd>New conversation</dd>
169 <dt><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>I</kbd></dt><dd>Toggle incognito</dd>
170 <dt><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>S</kbd></dt><dd>Show or hide history</dd>
171 <dt><kbd>Ctrl</kbd><kbd>K</kbd></dt><dd>Filter history</dd>
172 <dt><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>⌫</kbd></dt><dd>Delete this conversation</dd>
173 <dt><kbd>Shift</kbd><kbd>Esc</kbd></dt><dd>Focus the message box</dd>
174 <dt><kbd>Esc</kbd></dt><dd>Stop generating</dd>
175 <dt><kbd>Enter</kbd></dt><dd>Send</dd>
176 <dt><kbd>Shift</kbd><kbd>Enter</kbd></dt><dd>New line</dd>
177 <dt><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>M</kbd></dt><dd>What it remembers</dd>
178 <dt><kbd>Ctrl</kbd><kbd>/</kbd></dt><dd>This list</dd>
179 </dl>
180 <p class="keys-note">On a Mac use <kbd>⌘</kbd> in place of <kbd>Ctrl</kbd>. Chrome keeps <kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>I</kbd> for its developer tools, so incognito is also on <kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>P</kbd>.</p>
181 </div>
182</div>
183
184<div class="drop" id="drop" hidden><span>Drop files to attach</span></div>
185
186<template id="tpl-msg">
187 <article class="msg">
188 <div class="who"></div>
189 <div class="tools" hidden></div>
190 <div class="work" hidden></div>
191 <div class="files" hidden></div>
192 <div class="widgets" hidden></div>
193 <div class="body"></div>
194 <div class="sources" hidden></div>
195 </article>
196</template>
197
198<script src="{{asset "static/widgets.js"}}" defer></script>
199<script src="{{asset "static/app.js"}}" defer></script>
200</body>
201</html>