Compare commits

...
Sign in to create a new pull request.

717 commits

Author SHA1 Message Date
889c02fccb
chat: lazy-render reasoning_content into a collapsible thinking block
Qwen3.x / Deepseek-R1 / o1 stream model thinking via
delta.reasoning_content during SSE; the final answer arrives later via
delta.content. Previously chat.js parsed only delta.content, so any
thinking time appeared to the user as a long empty pause followed by a
sudden answer.

Both generator functions (sendMessage, sendMessageWithCustomHistory)
gain an opts.onThinking(text) callback that fires only when the server
actually streams reasoning_content. Backward compatible: existing
callers ignore the option, see no behaviour change.

handleUserInput now passes an onThinking callback that lazy-creates a
<details class="chat-thinking"> block above the answer span on the
first reasoning delta. When the first content delta arrives, the
thinking block auto-collapses into a "thinking (click to expand)"
summary so the answer is the visible focus.

If a response never thinks (thinking off, simple prompt, etc.), no
thinking DOM element is ever created — no visual artefact at all.

Existing enable_thinking:false request bodies kept for now; this change
is rendering-only and activates automatically the moment any caller
flips thinking on.
2026-05-25 14:49:33 -04:00
a7f71b5a8b
tts: dropdown voice picker, sticky Play footer, default voice foxhop 2026-05-25 08:17:32 -04:00
535f4ce027
text-to-speech.html: F5-TTS is the live engine, not Qwen3-TTS
Updates marketing copy to reflect actual production state: F5-TTS replaced
Qwen3-TTS as the default engine after head-to-head benchmarking showed
faster inference and cleaner clones on the same reference clips. Engine
count goes from 5 to 6 (F5-TTS additive). Qwen3-TTS remains wired up but
marked Self-host. Updates Makefile snippets to F5 targets, and the sidebar
nav now links F5-TTS at the top of the engine list.
2026-05-25 06:57:40 -04:00
bbfeff4b60
text-to-speech.html: announce voice 42 (foxhop, self-recorded)
Adds a third demo block for the self-recorded voice 42 alongside aria
and atlas. Updates the "what's live" copy to acknowledge that 41 voices
come from LibriSpeech public-domain and 1 is the operator's own voice.
Slot 41 left intentionally empty as a Hitchhiker tribute.
2026-05-25 06:29:47 -04:00
cbc4596315
tts: raise chunk caps to 500/2500 (sidecar phantom fix landed, single-chunk MSE streaming back) 2026-05-24 13:34:16 -04:00
0c024fa899
tts: complete clara->atlas remap in normalizeModelVoice fallback 2026-05-24 11:50:37 -04:00
ddc895de4c
tts: switch default voice clara -> atlas (clean male voice per transcription test) 2026-05-24 11:50:18 -04:00
378662b62f
tts: switch default voice aria -> clara (aria has loud F5 phantom bleed) 2026-05-24 11:14:53 -04:00
ad3568ff3c
tts: cap chunks at 120 chars to prevent F5 internal-chunking ref_audio bleed 2026-05-24 10:56:22 -04:00
7a5f545c00
tts: trim leading silence per chunk via Web Audio decode to skip F5 phantom-phoneme artifacts 2026-05-24 10:32:43 -04:00
075db9154a
tts: punctuate unbounded chunks, strip URLs and paths, log chunk text 2026-05-24 10:20:52 -04:00
29c400bdb4
tts: hotfix qwen 503s and primer noise (5 stale defaults, AudioContext primer, normalizeModelVoice) 2026-05-24 10:16:37 -04:00
9e36a1b6c2
tts: harden Safari freeze paths (#003) with timeouts, audio priming, defensive guards 2026-05-24 10:08:15 -04:00
d8949d0b6c
tts: ship F5 backend by default with pipelined playlist streaming 2026-05-24 10:03:50 -04:00
a4818b2afb
docs: record BM25 relevance ranking in tickets 006/009, reconcile statuses 2026-05-22 11:54:22 -04:00
2834e990bd
rag: rank cross-page context by BM25 relevance, drop unrelated pages 2026-05-22 06:31:41 -04:00
21edbe7d81
translate: add open-in-tab button, move controls above preview, fix blob lifetime 2026-05-21 21:26:38 -04:00
bd30ee1fab
chat: disable Qwen3 thinking via chat_template_kwargs (8x faster) 2026-05-21 21:26:37 -04:00
1099535827
syntax error
modified:   public/src/config.js
2026-05-04 15:23:41 -04:00
f5b9244747
disable thinking by default.
modified:   CLAUDE.md
	modified:   public/src/chat.js
	modified:   public/src/config.js
2026-05-04 15:12:47 -04:00
a04de221ee
license: 'The permacomputer' -> 'Our permacomputer'
Permacomputer Preamble wording fix per CLAUDE.md style rule:
prefer 'our' for community-owned things; 'the' implies fixed
singular ownership.
2026-05-04 09:28:21 -04:00
8f65d5651a style: avoid "the", use "our" — writing style rule + sweep 2026-03-31 13:20:22 -04:00
3a1d4cde52 skip TTS endpoint fetch when TTS is disabled 2026-03-28 12:38:58 -04:00
d55d84655e bump firefox zip to 4.2.1 2026-03-19 18:45:58 -04:00
6da9a9f597 rebuild all extension zips with unminified bundle, update size labels 2026-03-19 18:44:30 -04:00
e2d84178c7 disable bundle minification for AMO compliance, update firefox zip 2026-03-19 18:41:16 -04:00
30fe5f3120 update Firefox extension zip: add data_collection_permissions, bump to 4.2.0 2026-03-19 18:08:57 -04:00
0ee4a1623d document manifest version sync requirement 2026-03-19 17:22:41 -04:00
ca4eac93ce bump browser toys release link to v4.2.0 2026-03-19 17:19:54 -04:00
133542ae2c document Firefox AMO distribution and manifest requirements 2026-03-19 17:06:12 -04:00
3b8b7eb640 add Firefox Add-ons marketplace link, collapse manual install into details 2026-03-19 15:49:15 -04:00
5596565759 gate TTS buttons on isTTSEnabled(), update deploy policy in CLAUDE.md 2026-03-17 07:05:40 -04:00
00ef650b0e Replace translate preview popup with inline iframe
Firefox (and other browsers) block window.open popups from the
translate modal. Render translated HTML in an iframe via blob URL
instead. Fix translate modal header layout by removing negative
margin hack, use proper article padding structure. Add CSS for
iframe preview and raw HTML toggle.
2026-03-14 11:46:35 -04:00
dfc1cb2a36 add Reverse RAG whitepaper PDF with full keyword slug, link GitLab source repo 2026-03-07 20:06:53 -05:00
377f245e0d fix vault gate display cleanup, add GitLab source link to Reverse RAG whitepaper 2026-03-07 20:06:16 -05:00
44e6e337cd re-enable bundle minification: 1138KB exceeded 1MB store limit, minified is 730KB
The unminified bundle caused store-ready test failures in browser-toys
(all three browsers). Minification brings it well under the 1MB per-file
limit while the CDN copy remains readable source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:39:17 -05:00
93258f3ae8 remove greeting from system prompts: introduce material directly instead of welcoming users 2026-03-04 16:41:48 -05:00
a1923d0476 disable minification for extension bundle: readable code recruits developers 2026-03-04 07:57:36 -05:00
6d867260a6 add sprint mode, bundle-extension scripts, fix context bloat in DOM extraction 2026-03-03 18:49:08 -05:00
cd6a432e3f fix page styles bleeding into chatbot input (#013): CSS isolation for form elements 2026-03-03 18:26:37 -05:00
a0a2327af6 implement reverse RAG, journey-aware greetings, vault viewer, crawl mode, unfirehose sharing (#004-#012) 2026-03-03 17:46:55 -05:00
fa0ad2782d CLAUDE.md: add uncloseai-browser-toys repo reference 2026-03-03 16:58:53 -05:00
018aff20e4 CLAUDE.md: add no-flexbox rule, css grid only 2026-03-03 16:51:29 -05:00
b76c902c07 replace all flexbox with css grid, checkbox labels inline 2026-03-03 16:50:54 -05:00
f5fe6f4aee fix checkbox layout: flex container, 18px checkbox, label inline with gap 2026-03-03 16:42:08 -05:00
940d9c849f fix fitPageContent: proportional overhead, never drop all content on small contexts 2026-03-03 16:38:32 -05:00
d0d65f8ae4 floating button font-size 18px 2026-03-03 16:35:17 -05:00
f71d00295f floating button font-size 14px 2026-03-03 16:34:36 -05:00
433fb137f5 floating button border-radius 21px 2026-03-03 16:34:22 -05:00
ed7993712d floating button height 69px 2026-03-03 16:33:51 -05:00
1d0582bfc9 floating button width 121px, perfect square label 2026-03-03 16:33:29 -05:00
45c630f17a floating button right 42px 2026-03-03 16:32:48 -05:00
9e711d344d floating button bottom 69px 2026-03-03 16:32:15 -05:00
5003c89684 floating button bottom 141px to 89px (20 + 69) 2026-03-03 16:31:10 -05:00
93926bfbfc smart content collapsing for context overflow, spinner CSS, ticket updates
fitPageContent() in content.js: 6 progressive collapse stages (dedup,
nav links, references, lists, code blocks, sidebar debris) before hard
truncation. No summarization, real content structurally compressed.
Wired into both page extraction points in embed modal.
Added .working pulse and .uncloseai-spinner CSS animations for #004.
Updated tickets #001 (CSP root cause), #004 (CSS ready), #005 (fixed).
2026-03-03 16:26:01 -05:00
12e9e79f25 disable TTS via feature flag, move floating button up 121px, add bug tickets
TTS disabled by default for all users (tickets #002 freeze, #003 truncation).
Embedders re-enable with window.UNCLOSEAI_ENABLE_TTS = true.
Floating button bottom 20px to 141px to avoid overlapping page buttons.
Added docs/tickets/ with 5 issues from cthegray.
Bundle extension script and biome config updates.
2026-03-03 15:56:09 -05:00
5687fa1c22 add AGPL-3.0-only licence preamble to Reverse RAG page 2026-02-26 07:14:24 -05:00
95d6baeebb add canonical permacomputer preamble to all source files 2026-02-25 17:39:59 -05:00
255f0089f6 consolidate LICENSE preamble: canonical quadrivium, remove cthegray, normalize URIs 2026-02-25 15:01:09 -05:00
3af52ce181 update LICENSE preamble: add russell.ballestrini.net, remove dashes, and to & 2026-02-25 14:45:03 -05:00
4cbfacfa95 rename reverse-rag to reverse-retrieval-augmented-generations-rag.html, update all sidebar links 2026-02-24 23:21:29 -05:00
c2d6d6337c update Reverse RAG PDF with modular architecture description 2026-02-24 23:19:22 -05:00
cda3bb8527 add Reverse RAG landing page with abstract and PDF link, redirect old slug 2026-02-24 23:14:57 -05:00
a85ffb7f76 reverse-rag.html redirects to PDF whitepaper 2026-02-24 22:13:25 -05:00
bbcd69dea7 switch all source files to AGPL-3.0-only, full GNU AGPL v3 + permacomputer preamble in LICENSE 2026-02-24 19:42:46 -05:00
d47e8900e7 add Reverse RAG whitepaper, link from all site pages and uncloseai.js docs 2026-02-24 18:41:25 -05:00
b7ac2cbf3a fix TTS cutoff: lower chunk limit to 1500 chars, fix code block regex for backtick content 2026-02-24 18:15:07 -05:00
8845d22ce9 document Reverse Retrieval Augmented Generation as the core technique in uncloseai.js 2026-02-24 18:07:21 -05:00
9163f88ce5 add dark background to ai-message pre blocks for hljs syntax highlighting 2026-02-24 17:51:14 -05:00
7edba8ab24 fix double border on code blocks in chat messages 2026-02-24 17:48:52 -05:00
d8d8472460 fix modal crash: defer settings state apply until controls element exists 2026-02-24 17:43:47 -05:00
7aad5d154c replace absolute overlay settings panel with normal grid flow, hide chat when settings open 2026-02-24 17:30:57 -05:00
b1c2ecd9d0 use solid white background on settings panel instead of inherit to prevent bleed-through 2026-02-24 17:24:31 -05:00
fcec5b92dc keep header visible above settings panel with z-index layering 2026-02-24 17:14:25 -05:00
2e2b52e69e upgrade highlight.js from 11.8.0 to 11.10.0 for full language grammar support in code blocks 2026-02-24 16:42:08 -05:00
a70c94e837 add highlight.js CSS auto-loading to embed modal and code highlighting demos to styleguide 2026-02-24 16:39:18 -05:00
97eb0454ca settings panel covers full modal with inherited background, no chat bleed-through 2026-02-24 16:36:00 -05:00
4763c53c40 chunk long TTS text at sentence boundaries to handle 4096 char server limit 2026-02-24 16:24:00 -05:00
9fbd7f5649 remove overly aggressive div/header reset that destroyed all padding 2026-02-24 16:07:14 -05:00
854df0d2da reset host page header/div/nav styles inside modal, force header full width 2026-02-24 15:55:52 -05:00
615c84c15e aggressive inline dialog reset for host page CSS, 42+ voices, per-subdomain vault 2026-02-24 15:22:58 -05:00
ca8df46ddc fix modal header on host pages, button reset, remove emoji, add all 3 headers to styleguide, 42+ voices 2026-02-24 15:16:14 -05:00
31328aeddc rewrite uncloseai-js.html docs with all features, config vars, JS API, and widget library 2026-02-24 14:40:28 -05:00
8c9330b216 widen vault card max-width to 420px to prevent orphaned words 2026-02-24 14:34:57 -05:00
318b9f3ace fix vault lock screen showing when already unlocked, tighten vault card design 2026-02-24 14:33:04 -05:00
01439ed29a revert vault session to localStorage (per-origin, shared across tabs)
sessionStorage was per-tab which forced re-entering the password on
every new tab to the same site. localStorage is scoped per-origin
which is the right granularity. The lock button clears the session
from localStorage when the user wants to end their session.
2026-02-24 13:56:47 -05:00
ec0897de2c vault session per-tab: use sessionStorage instead of localStorage
Each tab now requires its own password unlock. Navigating within a tab
preserves the session (sessionStorage survives navigation). Closing the
tab or opening a new tab requires re-entering the password. Previously
the session was in localStorage, shared across all tabs indefinitely.
2026-02-24 12:58:54 -05:00
650f9eaadc fix vault lock: clear decrypted DOM content and add opaque gate background
Lock button was not properly securing the UI:
- decrypted chat messages stayed in DOM behind the vault gate
- vault gate had no background in base CSS (transparent when no theme set)
- settings content and input area remained visible after locking
- container lacked position:relative for proper gate overlay scoping
2026-02-24 12:40:27 -05:00
58f9642d6f update browser-toys portal for v1.0.0 release with mandatory vault docs
- add v1.0.0 release link and inline SHA-256 checksums
- update storage/privacy section to reflect mandatory vault encryption
- replace optional vault setup steps with first launch and returning flows
- add per-site vault and releases page link
2026-02-24 12:07:29 -05:00
5d117e50b9 Namespace globals under window.uncloseai to stop clobbering host pages
window.sendMessage, window.handleUserInput, window.speakText etc
were overwriting host page functions with the same names. This broke
OpenCompletion chat (sendMessage clobbered). Now all exports live
under window.uncloseai namespace. Only uniquely-named globals
(UncloseVault, toggleUncloseaiEmbeddedModal, openTTSModal, etc)
remain on window directly for internal cross-module compat.
2026-02-23 19:55:28 -05:00
310853195b add styleguide page, redesign vault gate UI with proper CSS classes 2026-02-23 17:37:57 -05:00
9d191f24d1 mandatory vault gate: encrypt all localStorage with password before chat
Vault password prompt is now the first thing shown when opening
uncloseai. No chat, no greeting, no data written until the user
creates or unlocks their vault. Each site has its own vault with
AES-256 encryption using password + device-specific salt.

- Add vault gate overlay blocking modal until password entered
- Remove plaintext conversation fallback from storage.js
- Route TTS mode, selected voice, language through vault
- Route custom API config reads through vault in models.js
- Add vault gate i18n strings (26 languages) explaining per-site
  encryption of chat history, API keys, and settings
- Add missing keys to migration (voice, TTS mode)
- Re-show vault gate on lock from settings
- Permacomputer headers across all source files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:38:54 -05:00
c38cae3c13 add computed page intelligence: 11 analyzers for deterministic LLM context augmentation 2026-02-23 14:24:11 -05:00
4555e8bf98 add vault encryption docs to browser-toys portal, computed page intelligence
browser-toys.html: new "Encrypting your chats" section documenting UncloseVault
AES-256 encryption of conversations, settings, and API keys. How to enable,
what gets encrypted, how it works, automatic plaintext migration.

page-intelligence.js: deterministic page analysis (zero API calls) extracting
structured data, heading outline, reading metrics, readability score, code
detection, link topology, media inventory, forms, tables, entity patterns.
Integrated into greeting flow alongside LLM classification.
2026-02-23 14:23:24 -05:00
a898a759f4 license ago.js under AGPL v3 2026-02-23 12:58:18 -05:00
80e20a4d85 add page pre-inference and ago.js port for superhuman greeting context 2026-02-23 12:57:46 -05:00
6417d3853f add duplicate load guard so extension defers to page native uncloseai.js 2026-02-23 10:57:02 -05:00
3f190723b4 encrypt conversation history with vault password, add vault and unsandbox translations to all 26 languages 2026-02-23 10:12:19 -05:00
f0db6d9589 add public domain license and headers to all source files 2026-02-22 22:27:09 -05:00
2fd0432701 remove attribution claim and security audit report link from public pages 2026-02-22 22:24:02 -05:00
e0fb466c36 use URI not URL in content extraction and CLAUDE.md style guide 2026-02-22 21:47:10 -05:00
4f589f2449 add URL translation for content extraction, fetch raw over DOM scraping
for known dynamic pages (GitLab CI jobs), fetch the raw/plain text
endpoint instead of scraping a half-loaded DOM. falls back to DOM
extraction with a MutationObserver settle wait when raw isn't accessible.
extractWebpageContent is now async, all callers updated.
2026-02-22 21:42:46 -05:00
1a5b576a7d update browser toys portal: AGPL-3.0-only license, 2026 footer, checksums in TOC 2026-02-22 21:08:35 -05:00
366f7fc71f update browser toy downloads with load event timing fix 2026-02-22 20:53:11 -05:00
b1bc9bc718 update browser toy downloads with bootstrap injection fix 2026-02-22 20:20:57 -05:00
f4bb87eba8 add X-Uncloseai-Client header for browser-toy identification 2026-02-22 18:46:18 -05:00
3c5efd29c4 deploy to /opt/www/uncloseai/ subdir, stop nuking neighbors 2026-02-22 18:00:01 -05:00
5281c1b54f add download zips with checksums, fix repo links to git.unturf.com 2026-02-22 11:51:17 -05:00
59fd8a8bf5 add security section and audit link to browser toys portal page 2026-02-22 11:06:25 -05:00
c6e47df7c2 add browser toys portal page and sidebar links
new browser-toys.html with install instructions, storage/privacy
docs, and per-browser guides. sidebar link added to all 26 pages.
browser toys section added to index.html and uncloseai-js.html.
2026-02-22 10:51:53 -05:00
3289b9b7be Add qwen and qwen-vl endpoints to inference page, remove ai.unturf.com block 2026-02-16 18:03:35 -05:00
3258cac2a3 Add uncloseai-cli page, fix Discord invite, ban em dashes in writing 2026-02-16 15:52:41 -05:00
timehexon
a6eb90ce7d Replace "AI" with "machine learning" in CLAUDE.md
Machine learning is what we grow. "AI" is forbidden in all
permacomputer discourse, marketing, & documentation.
2026-02-02 19:56:27 +00:00
e0630221e4 Add TTS play buttons for voice demos (aria, atlas) 2026-01-28 07:59:12 -05:00
df040d4f70 Add make voices-qwen to self-hosting instructions
Explain how to download the 42+ cloned voice samples from LibriSpeech
using the new Makefile target. Updated on both the main TTS page and
the Qwen3-TTS subpage.
2026-01-27 17:31:17 -05:00
363dde30f0 Use real model names on TTS pages, add upstream HuggingFace links
Replace generic engine names (Voice Cloning, Fast Synthesis, etc.)
with actual model names (Qwen3-TTS, Piper TTS, XTTS v2, Silero TTS,
Kokoro TTS) across all 6 TTS pages including titles, headers, and
sidebar nav. Add upstream HuggingFace/GitHub links in technical
details section of each engine subpage.
2026-01-27 17:12:48 -05:00
5226d0be04 Restructure TTS docs into main page + 5 engine subpages
Rewrite text-to-speech.html to focus on the live service (Qwen3-TTS
voice cloning with 42+ voices). Each of the 5 engines now has its own
page under /tts/ with detailed docs, examples, and self-hosting
instructions. Engine cards on the main page link out with Live/Self-host
badges. Sidebar nav updated with TTS subsection on all 6 pages.

New pages:
- /tts/voice-cloning.html (Live - Qwen3-TTS)
- /tts/fast-synthesis.html (Self-host - Piper)
- /tts/hd-cloning.html (Self-host - XTTS v2)
- /tts/multilingual-cpu.html (Self-host - Silero)
- /tts/lightweight.html (Self-host - Kokoro)
2026-01-27 17:06:58 -05:00
4df8f92349 Rewrite all example text with raccoon mission voice 2026-01-27 16:50:55 -05:00
c455b4ed43 Rewrite engine descriptions: lead with capabilities, not model names 2026-01-27 16:48:07 -05:00
42eccafcac Update TTS page: 42+ voices, new voice names, API endpoint links
- Update all examples from "alloy" to real voice names (aria, atlas)
- Add clickable links to /v1/voices and /v1/models endpoints
- Update voice count from 20+ to 42+ built-in cloned voices
- Bump TTS modal cache version to 4
- Update deployment instructions (remove vars.sh, add deploy-cpu)
- Add in-memory TTS cache for replay without regeneration
2026-01-27 16:45:24 -05:00
017201b5c9 Fix MSE garbling: use sequence mode for SourceBuffer 2026-01-27 10:22:10 -05:00
460bfd6b2a Fix MSE garbling: copy chunk data to avoid shared ArrayBuffer 2026-01-27 10:17:22 -05:00
092b3c72b2 Restore streaming mode, remove debug override 2026-01-27 10:16:56 -05:00
a6c30be5b7 Debug: force buffered mode to test webm without MSE 2026-01-27 10:07:21 -05:00
8dffac24f9 Add diagnostic logging to streaming TTS 2026-01-27 09:53:37 -05:00
46048a3251 Fix Firefox streaming: use browser-detected format instead of hardcoded mp3 2026-01-27 09:51:24 -05:00
4ae6b4fbbd Use webm+opus for Firefox TTS streaming via MediaSource
Firefox supports audio/webm;codecs=opus in MediaSource API,
enabling true streaming playback. Remove broken progressive
playback (partial blobs don't stream). Simplified fallback
to full buffer for any browser without MSE support.
2026-01-27 09:29:02 -05:00
03147f028d Fix progressive playback: add error handling, response.body check
Progressive playback was silently failing if response.body was null
or if the reader threw. Now falls back to full buffer if body isn't
streamable, logs chunk progress, and properly unblocks callers on
error instead of hanging forever.
2026-01-27 09:23:37 -05:00
cc1d21ade0 Progressive playback for Firefox, add Qwen3-TTS to TTS page
Firefox can't use MediaSource for streaming. Instead of buffering
the entire response before playing, progressively read chunks and
start playback after ~8KB arrives. Replaces audio src with complete
blob when download finishes for seeking support.

Update text-to-speech.html to document Qwen3-TTS as the default
5th engine with 20 cloned voices and 10-language support.
2026-01-27 09:20:39 -05:00
1a2a85c797 Use UA detection for audio format instead of canPlayType
Firefox on Linux reports canPlayType('audio/mpeg') as supported
but then fails to decode mp3. Use user-agent detection instead:
Firefox always gets opus (ogg), Chromium browsers get mp3.
2026-01-27 08:23:06 -05:00
74a1ea581c Detect browser audio codec support, use opus for Firefox
Firefox on Linux lacks mp3 decoders. Detect browser capabilities
at load time: use mp3 for Chrome, opus (ogg) for Firefox.
MediaSource streaming only works with mp3 (Chrome); Firefox
falls back to buffered playback with opus format.
Sends response_format parameter to TTS server.
2026-01-27 08:19:24 -05:00
3019fd0e15 Route all TTS through handleTTS for streaming support
- speakChatText now uses handleTTS instead of speakTextDirect
  so embed modal TTS (intro, chat messages) respects streaming mode
- Streaming result updates blobUrl on completion so existing
  download click handlers work without changes
2026-01-27 08:12:47 -05:00
ab13f2c19f Disable gpt-oss.ai.unturf.com endpoint (returns 404) 2026-01-27 08:12:07 -05:00
519e281623 Fix streaming TTS to return audio immediately, collect stream in background
- speakTextStreaming now returns immediately with the audio element
  already playing, plus a done promise for when the stream completes
- Modal wires up controls immediately using the live audio element
- Pause/play works during streaming (stream continues in background)
- Button shows "Streaming..." while receiving, switches to "Pause" when done
- Download button enables only after stream completes
- All TTS functions now return consistent done promise interface
2026-01-27 07:36:00 -05:00
81dbae3c7a Add streaming TTS playback mode with MediaSource API
Default to streaming mode which plays audio as chunks arrive
from the server instead of buffering the entire response.
Adds a Buffered/Streaming toggle in the TTS modal.
Falls back to buffered if MediaSource doesn't support audio/mpeg.
2026-01-27 07:32:18 -05:00
10c67ae4e2 Fix TTS model default: use tts-1-qwen instead of tts-1
The backend only has tts-1-qwen registered. Requests with model=tts-1
were failing with KeyError: 'tts-1'.
2026-01-26 18:41:38 -05:00
7018490249 Fix TTS URLs to use speech.ai.unturf.com 2026-01-26 18:13:02 -05:00
ed4b599895 Switch TTS to 3090-ai server (20 voices) 2026-01-26 17:32:51 -05:00
15e7e4c7c7 Update TTS modal to support 20 Qwen3-TTS voices
- Fix API response format parsing (data.data[].voices -> flat array)
- Update fallback voices from 6 to 20
- Bump cache version to v3 to force refresh
2026-01-26 17:25:20 -05:00
3d1c06c1c1 Add sitemap.xml for Google indexing 2026-01-26 13:04:14 -05:00
bfc03de5ef Add Google site verification 2026-01-26 13:02:46 -05:00
0ac9ba7430 Update C# examples with .NET 10 and Mono sections 2026-01-26 12:48:07 -05:00
6c0dfcb4ca Add C examples page, fix nav links to use correct paths 2026-01-26 12:44:30 -05:00
cce700e705 Add C and C# Implementations links to all page navs 2026-01-26 12:25:02 -05:00
c40f5e0bc0 Add C and C# portal pages, dotnet10/mono variants, c# symlink 2026-01-26 11:08:14 -05:00
44a3876db6 Fix model refresh to show partial success when TTS is down 2026-01-26 10:29:15 -05:00
83c5066948 new file: docs/VAULT_SYNC_DESIGN.md 2026-01-26 10:07:02 -05:00
2fbefde281 Fix: make vaultSection and settingsContent span full width of parent grid 2026-01-23 12:00:22 -05:00
ae22b1a906 Fix layout: explicit grid styles, full width elements 2026-01-23 11:44:24 -05:00
cd1a6ea190 Fix: call buildVaultUI after settingsContent is set up 2026-01-23 11:31:11 -05:00
011ecef046 Fix settings layout: align content to start, reduce gaps 2026-01-23 11:27:10 -05:00
5d3d7ed18c Fix settings layout: balanced two-column grid 2026-01-23 11:19:05 -05:00
f64cc45d59 Make unlocked vault bar thinner, full width at top 2026-01-23 11:17:04 -05:00
69a069701e Fix unlocked vault state - reset styles, compact layout 2026-01-23 11:14:08 -05:00
0966857cc6 Center vault form vertically with CSS grid place-content 2026-01-23 10:49:59 -05:00
f7bb6fecae Remove width constraints on vault form for mobile compatibility 2026-01-23 10:45:36 -05:00
b14e43f200 Center vault form with CSS grid, split text, button says Unlock Settings 2026-01-23 10:41:57 -05:00
5c37bf35b8 Fix: hide settings until vault unlocked 2026-01-23 10:38:48 -05:00
25d27be681 Remove vault settings dialog (no password change/delete) 2026-01-23 10:33:19 -05:00
2ff6c6b1a3 Simplify vault UI: remove confirm password, settings hidden until unlocked 2026-01-23 10:31:41 -05:00
5159355c98 Change to unlimited session persistence until explicitly locked 2026-01-23 09:53:12 -05:00
9944d8f757 Initialize vault in main entry point
- Import and export UncloseVault
- Load CryptoJS dynamically for vault encryption
- Initialize vault on page load (auto-restores session if valid)
- Expose vault globally via window.UncloseVault
2026-01-23 09:47:18 -05:00
3d07909854 Add vault system for encrypted settings storage
- Add vault.js: encrypted settings with AES-256 + 7-day session persistence
- Integrate vault UI in settings modal with friendly messaging
- Update config.js and un.js to use vault for credentials
- Add translation strings for vault UI (en.js)
- Show "Logged in" status with Lock button instead of session info
- Vault password only asked when opening settings
2026-01-23 09:46:56 -05:00
637064bf99 Rename browser SDK to un.js to match official naming 2026-01-23 08:36:08 -05:00
52b08894c6 Add unsandbox.com API key settings to modal
- Add public/src/unsandbox.js browser SDK with HMAC-SHA256 auth
- Add settings UI for unsandbox public/secret keys in modal
- Refactor code-execution.js to use SDK when keys configured
- Falls back to unauthenticated endpoint when not configured
- Add English translations for new settings
2026-01-23 08:34:18 -05:00
a00c7fd9e2 Add official unsandbox.com JavaScript SDK
- Add lib/un.js (official async SDK from unsandbox.com)
- Add update-unsandbox-sdk Makefile target for SDK updates
- Note: SDK is Node.js-based, browser code-execution.js remains separate
2026-01-23 08:29:07 -05:00
070e3ae591 Use uncloseai.com tag for runner 2026-01-12 11:16:28 -05:00
0aa20fd8b8 Remove tag requirement - runner is project-specific 2026-01-12 11:14:42 -05:00
2784aeda13 Deploy to new proxy server 2026-01-12 11:13:38 -05:00
53a84734ea Trigger deploy to new proxy 2026-01-12 11:09:33 -05:00
4b434b0222 Retrigger deployment 2026-01-12 11:07:08 -05:00
e82591cd75 Update runner tag to proxy.uncloseai.com for new proxy server 2026-01-12 11:03:10 -05:00
e1366fe2c7 Add explicit CRITICAL language instruction for intro generation 2026-01-03 14:06:37 -05:00
2fecc5848a Fix getUserLanguagePreference to check UNCLOSEAI_LANGUAGE for intro generation 2026-01-03 13:46:45 -05:00
0aaba4c7c0 Add UI translations for Italian, Dutch, Polish, Vietnamese, Thai, Ukrainian, Swedish 2026-01-03 13:42:40 -05:00
b70b3e0844 Add Italian, Dutch, Polish, Vietnamese, Thai, Ukrainian, Swedish language support 2026-01-03 13:33:30 -05:00
60c3bbcf9f Add language code normalizer for UNCLOSEAI_LANGUAGE 2026-01-03 13:31:23 -05:00
78d526098a Add debug logging for language detection 2026-01-03 13:19:56 -05:00
15decd2ab6 Force UNCLOSEAI_LANGUAGE to override user setting and disable selector 2026-01-03 12:56:33 -05:00
f7adaa1d74 Fix UNCLOSEAI_LANGUAGE to return actual value not boolean 2026-01-03 12:53:10 -05:00
1912076b5b Add UNCLOSEAI_LANGUAGE config for embed default language 2026-01-03 12:43:37 -05:00
6e2ad4e196 Add echo only to LLM request, not stored chat history 2026-01-03 11:17:01 -05:00
cdada47eab Remove auto-focus on textarea when modal opens 2026-01-03 11:10:36 -05:00
e55d7d414c Fix echo to only use custom prompt, not full system message with page content 2026-01-01 12:24:50 -05:00
1ec196275f Fix token estimation: use 3 chars/token instead of 4 2026-01-01 12:22:11 -05:00
bd312f0979 Echo system prompt as final assistant message for attention reinforcement
- Add system prompt as last assistant message in chat history
- Support UNCLOSEAI_SYSTEM_PROMPT_REPLACE flag for custom prompts
- Use configured voice preference in page-reader TTS
- Disable code execution button (sandbox not running)
2026-01-01 12:14:24 -05:00
9730fff7f4 Remove redundant TTS Setup section 2025-12-02 15:53:12 -05:00
6590f9eb4c Remove non-existent Qwen Swagger docs link 2025-12-02 15:51:59 -05:00
3d8ee26a23 Fix duplicate TTS link in sidebar nav 2025-12-02 15:50:31 -05:00
e90ed3c14f Add model discovery docs and cross-link inference/TTS pages 2025-12-02 07:30:31 -05:00
eea1c51ce8 Add crawler page to navigation on all pages
- Add 'Our Crawler' link to left sidebar navigation after Text-to-Speech on all HTML pages
- Fix missing Text-to-Speech link in inference.html navigation
- Update Discord bot link in crawler.html to point to Git repo instead of domain
2025-11-27 13:03:58 -05:00
bfee917049 docs: Add ethical web crawler documentation page
Created /crawler.html page explaining:
- Our user agent (uncloseai.com/1.42)
- What we use the crawler for (research, Q&A, content aggregation)
- Ethical practices (robots.txt, crawl delays, caching, smart depth)
- How to block the crawler
- Link to Discord bot

Added crawler link to sidebar navigation in inference.html

This provides transparency for webmasters and explains our ethical
crawling practices in human-friendly terms.
2025-11-27 10:11:10 -05:00
baa0a1fd7c docs: Add tool calling flags for Hermes 3 vLLM setup
Add --enable-auto-tool-choice and --tool-call-parser hermes flags to the
vLLM server command. These flags are required for Hermes 3 to support
function calling via the OpenAI-compatible API.
2025-11-27 09:08:53 -05:00
911c4281da Fix Go code style and module import issues
- Remove redundant newlines in fmt.Println calls (go/http/uncloseai.go)
- Update Go version to 1.19 for compatibility across all modules
- Add go.mod for examples directory with proper module replacement
- Add go.mod for uncloseai library package
- Run go mod tidy to resolve openai SDK dependencies
2025-11-20 19:32:53 -05:00
063b2bf960 Fix code block buttons visibility by allowing vertical overflow on pre elements 2025-11-10 16:46:33 -05:00
4ff5740559 Revert inline styles approach - wrong solution 2025-11-10 16:43:41 -05:00
4de5b27700 Add defensive inline styles to code buttons for cross-site compatibility 2025-11-10 16:41:35 -05:00
8bc83789df Add CSS best practices to CLAUDE.md and fix button visibility with proper specificity instead of !important 2025-11-10 16:36:20 -05:00
117fee0d30 Force visibility of code execution buttons with defensive CSS and improved z-index 2025-11-10 16:33:59 -05:00
b40ff56b23 Match code execution output background to code block styling and remove debug logs 2025-11-10 16:07:50 -05:00
1803ed9030 Add debug logging to diagnose code block button visibility issue 2025-11-10 15:43:30 -05:00
df7b47bc61 Fix code block copy/run buttons not showing - remove conflicting CSS absolute positioning 2025-11-10 15:37:46 -05:00
0dcf1eb6e7 Fix code block buttons not rendering - correct duplicate detection class name 2025-11-10 15:34:56 -05:00
5426425e3a Add code execution sandbox support to uncloseai.js
Integrated code.ai.unturf.com code execution service into uncloseai.js modal:
- Added CODE_EXEC_URL to config.js
- Created code-execution.js module with executeCode() and result display
- Modified addCodeBlockCopyButtons() to add ▶ Run button next to 📋 Copy
- Auto-detects language from code block class (defaults to Python)
- Uses async job polling with exponential backoff
- Shows execution output, errors, and allows cancellation for long-running jobs
- Results displayed inline below code blocks with monospace formatting

Code blocks in AI responses now have both Copy and Run buttons.
2025-11-10 14:47:25 -05:00
9e5bc89d93 Remove TTS download button debugging logs
Download button functionality is now confirmed working - removed all debugging console logs.
2025-11-10 06:19:15 -05:00
b52776b6fd Add comprehensive TTS download button debugging logs
Added detailed console logging to help diagnose why download buttons don't appear after TTS generation. Logs include:
- TTS result object inspection
- Download button DOM query verification
- Display style changes before/after
- Computed style verification
- Error logging when button not found

This will help identify whether the issue is:
1. TTS not completing successfully
2. Download button not being found in DOM
3. Display style not being applied correctly
4. CSS overriding the display property
2025-11-10 06:11:44 -05:00
7459c08c1b Remove title casing from voice names - display as-is from API 2025-11-10 05:53:08 -05:00
d1749209a0 Fix voices API parsing - use correct data.data format with model.voices arrays 2025-11-10 05:51:20 -05:00
7a658f7bdf Remove fallback voices - fail properly if API doesn't respond 2025-11-10 05:48:18 -05:00
4e5ec776a8 Fix voice count display to show actual API voices not fallback count 2025-11-10 05:47:39 -05:00
c135bcd6c5 Refresh voices when clicking refresh models button 2025-11-10 05:43:47 -05:00
f3d825278a Add version-based cache invalidation for voice data
Users with old cached voice data need to see the new models (tts-1-silero,
tts-1-kokoro). Added a cache version system that automatically invalidates
old cache when we update the voice models.

Changes:
- Added cacheVersionKey to track cache format version
- Set currentVersion to '2' (increment when voice models change)
- Cache is invalidated if version doesn't match
- Old cache entries are cleaned up automatically
- Users will automatically fetch fresh voice data on next modal open
2025-11-10 04:07:53 -05:00
0db28f7984 Remove Quick Links section from text-to-speech page 2025-11-10 03:52:45 -05:00
3332f5d07e Update chat modal voice selection to use dynamic model:voice format with backward compatibility 2025-11-10 03:47:18 -05:00
190ae74625 Add curl example showing model parameter usage 2025-11-10 03:46:06 -05:00
d5088b6978 Introduce project as uncloseai-speech on text-to-speech page 2025-11-10 03:45:32 -05:00
6bd6a12f83 Make voice dropdown dynamic in Hermes modal 2025-11-10 03:43:11 -05:00
12b10b01cd Simplify text-to-speech page content
- Add /v1/models endpoint example with jq pretty printing
- Fix /v1/voices endpoint to use jq . for pretty printing
- Remove 'Learn ML By Doing' section, link to repo README instead
- Remove 'For Machine Learning Learners' section
- Remove 'Contribute Voices' section
- Remove 'Why AGPL v3' section
- Remove 'What's Next' roadmap section
- Remove MinIO infrastructure goals reference
2025-11-10 03:39:47 -05:00
91859d1807 Replace corporations with orgs in text-to-speech page 2025-11-10 03:34:21 -05:00
caeeb85c63 Add text-to-speech navigation link to all public pages
- Added text-to-speech link in navigation after Inference Setup link
- Updated inference.html TTS section to link to text-to-speech page
- Removed redundant openedai-speech references from inference page
- Applied changes to 18 public HTML pages for consistent navigation
2025-11-09 18:12:44 -05:00
bc70558961 Integrate dynamic voices API endpoint with model:voice selection and localStorage caching 2025-11-09 15:48:27 -05:00
caacc05e63 Change AI to machine learning in legal pages 2025-11-07 21:02:40 -05:00
b28e72d4de Remove issue tracker links from legal pages - private repo for devs only 2025-11-07 20:38:01 -05:00
49ca462431 Update privacy policy and terms of use pages
- Added uncloseai.js button to both legal pages
- Added all language examples to navbar navigation
- Removed email addresses (no support inbox)
- Updated "Your Rights" to "Data Rights" - clarifies we don't collect data
- Removed data request processes (nothing to request/delete)
- Updated third-party services section to clarify Remarkbox is part of unturf umbrella
- Changed "Contact Us" to "Questions" - explains we don't respond to policy inquiries
- Emphasizes open source code transparency over traditional support
2025-11-07 20:36:42 -05:00
5b4c27247f Add privacy policy, terms of use, and copyright notices to all pages
- Created privacy-policy.html with data collection transparency
- Created terms-of-use.html with service terms and liability limits
- Added copyright notice (© uncloseai. 2025) to all 18 HTML pages
- Added Privacy Policy and Terms of Use links to all footers
- Emphasizes IP logging for rate limiting only, not user tracking
- Notes console-only logging (stdout, no disk storage)
- Highlights HTTPS encryption for all communications
2025-11-07 19:17:01 -05:00
b41c10d2e6 modified: .gitignore
deleted:    add_remote_url_keys.js
	deleted:    build-all.log
	deleted:    modal_tests.js
	deleted:    simple_tests.js
	deleted:    uncloseai.js.orig
2025-10-26 11:29:52 -04:00
4c07f47f47 Changes to be committed:
modified:   public/uncloseai-js.html
2025-10-26 11:24:39 -04:00
71101019ab Fix Scheme implementation: Add GnuTLS support and fix model discovery
- Add guile-gnutls package to Dockerfile for HTTPS support
- Fix unbound variables by ensuring proper module imports (rnrs bytevectors, srfi srfi-43)
- Fix vector/list handling in model discovery with vector->list conversion
- All tests now pass: model discovery, non-streaming chat, and streaming chat work correctly
2025-10-24 20:41:24 -04:00
157078415c Fix Scheme Dockerfile to install guile-json module 2025-10-24 20:18:42 -04:00
390e028769 Fix Common Lisp to handle both vector and list responses from /models API 2025-10-24 14:27:38 -04:00
9aa834852c Update Makefile.languages paths and add commonlisp/scheme targets, fix Common Lisp timeout slot name conflict 2025-10-24 14:24:16 -04:00
dad7943c54 Consolidate Scheme and Common Lisp into single uncloseai files with environment-based discovery 2025-10-24 14:03:01 -04:00
c67d1b13da Remove committed MP3 files and update gitignore to exclude audio files 2025-10-24 13:45:00 -04:00
39f47da6f3 Remove legacy HTML documentation files from languages directory 2025-10-24 13:41:37 -04:00
845e747d1c Update links to /languages and add brief READMEs to all language directories 2025-10-24 13:36:27 -04:00
38545721a0 Reorganize project structure: move public files to public/ directory 2025-10-24 13:23:42 -04:00
8de043bfef modified: src/config.js 2025-10-22 19:44:37 -04:00
45ccee4b71 modified: languages/awk/Dockerfile
modified:   languages/c/curl/Dockerfile
	modified:   languages/c/libsoup/Dockerfile
	modified:   languages/c/nghttp2/Dockerfile
	modified:   languages/c/nghttp2/uncloseai.c
	modified:   languages/cpp/boost-beast/Dockerfile
	modified:   languages/cpp/cpp-httplib/Dockerfile
	modified:   languages/cpp/libcurl/Dockerfile
	modified:   languages/csharp/openai/Dockerfile
	modified:   languages/go/http/Dockerfile
	modified:   languages/go/openai/Dockerfile
	modified:   languages/lua/Dockerfile
	modified:   languages/ruby/openai/Dockerfile
	modified:   languages/vbnet/Dockerfile
2025-10-17 09:02:19 -04:00
fc4b021b7f fix elixir httpoison SSE parsing (String.replace_prefix), document openai_ex streaming blocks for full response 2025-10-15 20:16:33 -04:00
b5bead65cb modified: csharp-examples.html
modified:   elixir-examples.html
	modified:   go-examples.html
	modified:   java-examples.html
	modified:   nodejs-examples.html
	modified:   python-examples.html
	modified:   ruby-examples.html
2025-10-15 20:06:05 -04:00
7fc375a598 disable elixir streaming - not compatible with vLLM endpoints in openai_ex 0.9.18 (returns full response instead of SSE chunks) 2025-10-15 19:43:23 -04:00
c30b3af2de fix elixir openai_ex 0.9.18 API: use Chat.Completions module and string keys for response parsing 2025-10-15 19:31:53 -04:00
b4148cbaf6 update elixir openai_ex to 0.9.18 (latest, published Oct 12 2025) 2025-10-15 19:21:38 -04:00
60b61b5a7e fix build failures: haskell streaming with withResponse, java-openai 4.3.0 with proper API, go-openai fixes, mojo real container 2025-10-15 18:09:13 -04:00
d5c443fbdb fix rust Dockerfile (remove unused binary copy) and create mojo using python:3.13-alpine with Python equivalents 2025-10-15 17:16:06 -04:00
e765dadd34 fix java-openai: use baseUrl instead of baseURL, call .stream() on StreamResponse, skip TTS (not in SDK 0.8.1) 2025-10-15 17:09:47 -04:00
c6a09db0eb fix build failures: D consolidation, csharp-openai ApiKeyCredential, go-http/go-openai, haskell imports 2025-10-15 17:06:33 -04:00
323294dd57 consolidate D language into single uncloseai.d file matching other languages 2025-10-15 16:56:43 -04:00
6318db3d1f fix Docker builds: go-http missing files, go-openai go.sum, csharp-openai SDK API, D language JSONValue syntax 2025-10-15 16:53:15 -04:00
381bd5f739 update all OpenAI SDK implementations to use environment variables MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, TTS_ENDPOINT_1 with dynamic model discovery 2025-10-15 16:18:05 -04:00
f7e1e6b17f move raw HTTP implementations to http subdirectories for Go, Ruby, Java, and C# 2025-10-15 15:57:30 -04:00
8841bacb0b update JavaScript openai client to demonstrate both Hermes and Qwen with both streaming and non-streaming modes 2025-10-15 15:51:11 -04:00
df3d76738e move Elixir HTTPoison client to httpoison subdirectory 2025-10-15 15:50:12 -04:00
bdf7875f31 reorganize openai clients: rename python/openai-client to python/openai, add javascript/openai/nodejs and elixir/openai 2025-10-15 15:47:23 -04:00
0cf61ea676 add official OpenAI SDK client examples for Go, Ruby, Java, and C# 2025-10-15 15:25:32 -04:00
bfc41a22a0 add link to unturf fork of openedai-speech 2025-10-15 12:24:48 -04:00
4c5960a6be clarify vLLM setup: f16 safetensors generally, FP8 quant for Hermes (optimized for 4090/3090) 2025-10-15 12:23:15 -04:00
f7c5d6d553 change TTS examples from build to grow something people love 2025-10-15 11:40:08 -04:00
66a82f6b0b update rate limiting documentation to reflect current configuration 2025-10-15 11:24:57 -04:00
a2175d3f7d remove navigation heading from index page 2025-10-15 11:12:17 -04:00
2b977c4560 add all language links to navigation in alphabetical order across all pages 2025-10-15 11:06:43 -04:00
d917f22042 add 10 language example pages (Go, Ruby, Java, C#, Rust, PHP, Elixir, Kotlin, Swift, Dart) and update navigation with book and all-languages links 2025-10-15 10:44:48 -04:00
3484a17ebd modified: css/sidebar-theme.css
modified:   index.html
	new file:   inference.html
	new file:   nodejs-examples.html
	new file:   python-examples.html
	new file:   uncloseai-js.html
2025-10-15 09:50:24 -04:00
17717cf3e3 remove emoji
modified:   CLAUDE.md
	modified:   languages/awk/uncloseai.awk
	modified:   languages/bash/index.html
	modified:   languages/bash/uncloseai.sh
	modified:   languages/clojure/uncloseai.clj
	modified:   languages/cpp/libcurl/uncloseai.cpp
	modified:   languages/go/examples/basic.go
	modified:   languages/go/uncloseai.go
	modified:   languages/java/UncloseAI.java
	modified:   languages/javascript/vanilla/uncloseai.html
	modified:   languages/kotlin/src/main/kotlin/UncloseAI.kt
	modified:   languages/kotlin/uncloseai.kt
	modified:   languages/lua/uncloseai.lua
	modified:   languages/nim/uncloseai.nim
	modified:   languages/ocaml/uncloseai.ml
	modified:   languages/odin/uncloseai.odin
	modified:   languages/perl/uncloseai.pl
	modified:   languages/php/uncloseai.php
	modified:   languages/powershell/uncloseai.ps1
	modified:   languages/prolog/uncloseai.pl
	modified:   languages/python/aiohttp/uncloseai.py
	modified:   languages/python/httpx-async/uncloseai.py
	modified:   languages/python/openai-client/uncloseai.py
	modified:   languages/python/requests/uncloseai.py
	modified:   languages/r/uncloseai.R
	modified:   languages/ruby/uncloseai.rb
	modified:   languages/rust/examples/basic.rs
	modified:   languages/rust/src/uncloseai.rs
	modified:   languages/scala/src/main/scala/UncloseAI.scala
	modified:   languages/tcl/uncloseai.tcl
2025-10-14 15:39:30 -04:00
47ccc09992 fix c/nghttp2, cpp/boost-beast, cpp/cpp-httplib to use actual libraries, rename libh2o to libsoup 2025-10-14 14:56:26 -04:00
9e3eccb1b9 modified: .gitignore
modified:   CLAUDE.md
	modified:   Makefile
	new file:   languages/awk/Dockerfile
	new file:   languages/awk/uncloseai.awk
	modified:   languages/bash/Dockerfile
	deleted:    languages/bash/examples.sh
	new file:   languages/bash/uncloseai.sh
	new file:   languages/c/curl/Dockerfile
	new file:   languages/c/curl/Makefile
	new file:   languages/c/curl/index.html
	new file:   languages/c/curl/uncloseai.c
	new file:   languages/c/libh2o/Dockerfile
	new file:   languages/c/libh2o/Makefile
	new file:   languages/c/libh2o/uncloseai.c
	new file:   languages/c/nghttp2/Dockerfile
	new file:   languages/c/nghttp2/Makefile
	new file:   languages/c/nghttp2/uncloseai.c
	new file:   languages/clojure/Dockerfile
	new file:   languages/clojure/deps.edn
	new file:   languages/clojure/uncloseai.clj
	new file:   languages/cobol/Dockerfile
	new file:   languages/cobol/discover.sh
	new file:   languages/cobol/hermes.sh
	new file:   languages/cobol/qwen.sh
	new file:   languages/cobol/tts.sh
	new file:   languages/cobol/uncloseai.cob
	new file:   languages/cpp/boost-beast/Dockerfile
	new file:   languages/cpp/boost-beast/Makefile
	new file:   languages/cpp/boost-beast/uncloseai.cpp
	new file:   languages/cpp/cpp-httplib/Dockerfile
	new file:   languages/cpp/cpp-httplib/Makefile
	new file:   languages/cpp/cpp-httplib/uncloseai.cpp
	new file:   languages/cpp/libcurl/Dockerfile
	new file:   languages/cpp/libcurl/Makefile
	new file:   languages/cpp/libcurl/uncloseai.cpp
	new file:   languages/crystal/Dockerfile
	new file:   languages/crystal/uncloseai.cr
	new file:   languages/csharp/Dockerfile
	new file:   languages/csharp/Uncloseai.cs
	new file:   languages/csharp/csharp.csproj
	new file:   languages/dart/Dockerfile
	new file:   languages/dart/bin/uncloseai.dart
	new file:   languages/dart/pubspec.yaml
	new file:   languages/deno/Dockerfile
	new file:   languages/deno/uncloseai.ts
	new file:   languages/elixir/Dockerfile
	new file:   languages/elixir/lib/uncloseai.ex
	new file:   languages/elixir/mix.exs
	new file:   languages/elixir/run.exs
	new file:   languages/erlang/Dockerfile
	new file:   languages/erlang/rebar.config
	new file:   languages/erlang/src/uncloseai.app.src
	new file:   languages/erlang/src/uncloseai.erl
	new file:   languages/fortran/Dockerfile
	new file:   languages/fortran/uncloseai.f90
	new file:   languages/fsharp/Dockerfile
	new file:   languages/fsharp/Uncloseai.fs
	new file:   languages/fsharp/fsharp.fsproj
	new file:   languages/go/Dockerfile
	new file:   languages/go/README.md
	new file:   languages/go/examples/basic.go
	new file:   languages/go/go.mod
	new file:   languages/go/uncloseai.go
	new file:   languages/go/uncloseai/uncloseai.go
	new file:   languages/haskell/Dockerfile
	new file:   languages/haskell/UncloseAI.hs
	new file:   languages/haskell/uncloseai.cabal
	new file:   languages/java/Dockerfile
	new file:   languages/java/UncloseAI.java
	new file:   languages/javascript/bun/Dockerfile
	new file:   languages/javascript/bun/uncloseai.ts
	new file:   languages/javascript/nodejs/Dockerfile
	new file:   languages/javascript/nodejs/README.md
	new file:   languages/javascript/nodejs/package.json
	new file:   languages/javascript/nodejs/uncloseai.js
	new file:   languages/javascript/typescript/Dockerfile
	new file:   languages/javascript/typescript/package.json
	new file:   languages/javascript/typescript/tsconfig.json
	new file:   languages/javascript/typescript/uncloseai.ts
	new file:   languages/javascript/vanilla/Dockerfile
	new file:   languages/javascript/vanilla/uncloseai.html
	new file:   languages/julia/Dockerfile
	new file:   languages/julia/Project.toml
	new file:   languages/julia/src/uncloseai.jl
	new file:   languages/kotlin/Dockerfile
	new file:   languages/kotlin/build.gradle.kts
	new file:   languages/kotlin/src/main/kotlin/UncloseAI.kt
	new file:   languages/kotlin/uncloseai.kt
	new file:   languages/lua/Dockerfile
	new file:   languages/lua/uncloseai.lua
	new file:   languages/nim/Dockerfile
	new file:   languages/nim/uncloseai.nim
	new file:   languages/ocaml/Dockerfile
	new file:   languages/ocaml/dune
	new file:   languages/ocaml/dune-project
	new file:   languages/ocaml/uncloseai.ml
	new file:   languages/odin/Dockerfile
	new file:   languages/odin/uncloseai.odin
	new file:   languages/perl/Dockerfile
	new file:   languages/perl/uncloseai.pl
	new file:   languages/php/Dockerfile
	new file:   languages/php/uncloseai.php
	new file:   languages/powershell/Dockerfile
	new file:   languages/powershell/uncloseai.ps1
	new file:   languages/prolog/Dockerfile
	new file:   languages/prolog/uncloseai.pl
	new file:   languages/python/aiohttp/Dockerfile
	new file:   languages/python/aiohttp/requirements.txt
	new file:   languages/python/aiohttp/uncloseai.py
	modified:   languages/python/httpx-async/Dockerfile
	deleted:    languages/python/httpx-async/examples.py
	new file:   languages/python/httpx-async/uncloseai.py
	modified:   languages/python/openai-client/Dockerfile
	deleted:    languages/python/openai-client/examples.py
	new file:   languages/python/openai-client/uncloseai.py
	modified:   languages/python/requests/Dockerfile
	deleted:    languages/python/requests/examples.py
	new file:   languages/python/requests/uncloseai.py
	new file:   languages/r/Dockerfile
	new file:   languages/r/uncloseai.R
	new file:   languages/ruby/Dockerfile
	new file:   languages/ruby/README.md
	new file:   languages/ruby/uncloseai.rb
	new file:   languages/rust/Cargo.toml
	new file:   languages/rust/Dockerfile
	new file:   languages/rust/README.md
	new file:   languages/rust/examples/basic.rs
	new file:   languages/rust/src/lib.rs
	new file:   languages/rust/src/uncloseai.rs
	new file:   languages/scala/Dockerfile
	new file:   languages/scala/build.sbt
	new file:   languages/scala/project/build.properties
	new file:   languages/scala/project/plugins.sbt
	new file:   languages/scala/src/main/scala/UncloseAI.scala
	new file:   languages/tcl/Dockerfile
	new file:   languages/tcl/uncloseai.tcl
	new file:   languages/v/Dockerfile
	new file:   languages/v/uncloseai.v
	new file:   languages/v/v.mod
	new file:   languages/vbnet/Dockerfile
	new file:   languages/vbnet/UncloseAI.vb
	new file:   languages/vbnet/UncloseAI.vbproj
	modified:   languages/zig/Dockerfile
	modified:   languages/zig/build.zig
	deleted:    languages/zig/src/main.zig
	new file:   languages/zig/src/uncloseai.zig
2025-10-14 10:24:16 -04:00
bf3f8cfac6 fix read page functionality by importing missing getSelectedModelMaxCompletionTokens in tts.js 2025-10-13 13:13:51 -04:00
19ff284403 add Python httpx-async example with httpx==0.28.1 and python:3.13-alpine 2025-10-12 15:21:35 -04:00
4941cce333 reorganize Python with openai-client and requests subdirs, pin to python:3.13-alpine, add Docker image consistency standards 2025-10-12 15:17:12 -04:00
39502bcac3 add Python example with openai==2.3.0 (latest) and dependency pinning standards to CLAUDE.md 2025-10-12 15:10:07 -04:00
973c045af6 add bash language example - tested working with Docker 2025-10-12 14:58:53 -04:00
332a64825d add webwords Docker image reference guide for workers 2025-10-12 14:43:03 -04:00
1efa96a837 add languages directory structure, Makefile.languages, and code quality guidelines from webwords 2025-10-12 14:39:02 -04:00
b340c80868 reorganize client installation sections into language model code sections 2025-10-12 14:26:38 -04:00
8fb4cfb807 add web client-only solution description to uncloseai button section 2025-10-12 14:22:39 -04:00
1c5b9ef035 simplify uncloseai.js section - focus on button, remove detailed internals 2025-10-12 14:20:56 -04:00
1a210d48e9 add iframe theme reload on theme toggle
modified:   index.html
2025-10-12 08:31:50 -04:00
c9bd3ff7d2 add Qwen 3 Coder model to homepage with code examples for both Hermes and Qwen endpoints 2025-10-12 08:26:54 -04:00
14f3699ab2 disable hermes2 endpoint (returns 404) - keep hermes and qwen only 2025-10-11 15:19:05 -04:00
54d7f548e9 document tmux remote access capabilities 2025-10-11 15:09:05 -04:00
ca03398bb3 add detailed console logging for model fetching and processing 2025-10-11 09:41:11 -04:00
9ba6ef8522 set qwen context window to 32K (practical limit for 24GB VRAM) 2025-10-11 09:39:41 -04:00
30b7e6649a update qwen model context window to correct value: 262144 tokens (256K) 2025-10-11 09:38:52 -04:00
5fc300ecc2 move ollama model context windows to config.js endpoint configuration 2025-10-11 09:37:27 -04:00
39db3f1004 add qwen.ai.unturf.com endpoint to model registry 2025-10-11 09:30:07 -04:00
90645cc899 fix code block width - use min(72vw, 900px) to handle both narrow and wide viewports 2025-10-11 09:19:25 -04:00
202e877b34 Revert "fix code block width - use 90% for desktop (relative to modal), 85vw for mobile"
This reverts commit 7406950040.
2025-10-11 09:18:58 -04:00
7406950040 fix code block width - use 90% for desktop (relative to modal), 85vw for mobile 2025-10-11 09:15:35 -04:00
0ede23a4db fix code block width - use 72vw for desktop, 85vw for mobile 2025-10-11 09:11:43 -04:00
86ef139a1d simplify code block width to 80vw for all screen sizes 2025-10-11 09:01:05 -04:00
ac0ff18308 make code block widths responsive using min() function
Changed code block max-width to use min() with both pixel and viewport units:
- Mobile: min(400px, 85vw)
- Desktop: min(850px, 70vw)

This ensures code blocks scale appropriately at all resolutions and never
cause horizontal overflow when resizing the browser window.
2025-10-11 08:46:04 -04:00
8f2395f7ce Revert "change code blocks to use 100% width with box-sizing"
This reverts commit 3192a9a75b.
2025-10-11 08:35:51 -04:00
3192a9a75b change code blocks to use 100% width with box-sizing
Replaced fixed pixel max-width values with 100% for pre and code elements.
Added box-sizing border-box to ensure padding is included in width calculation.
Code blocks now adapt to message bubble size while still scrolling when content is too wide.
2025-10-11 08:23:22 -04:00
1762f8cf20 fix message overflow - use fit-content and auto margins instead of justify-self
Replaced justify-self with margin-left/right auto and width fit-content
to prevent messages from expanding the grid column and causing horizontal
scrollbars. User messages now align right and AI messages align left
without affecting each other's positioning.
2025-10-11 08:13:15 -04:00
a082f8dbf6 fix modal code block overflow - add responsive max-width to prevent horizontal scrollbars
Added max-width constraints to code blocks in AI messages:
- Mobile (< 768px): 340px max-width
- Desktop (≥ 768px): 720px max-width

Code blocks now get their own horizontal scrollbar when content is too wide,
preventing the modal from getting horizontal scrollbars. All code formatting
and whitespace is preserved.
2025-10-11 07:55:00 -04:00
23dc8dffd9 modified: index.html 2025-09-12 18:44:49 -04:00
8b29615fcb adjust sidebar spacing for large monitors - center layout and reduce gap 2025-09-12 10:01:41 -04:00
91d2ed9170 modified: index.html 2025-09-09 08:49:28 -04:00
04b49530b6 fix mobile zoom issue - remove overflow-x hidden and improve viewport constraints 2025-09-06 19:50:23 -04:00
3ac6386234 modified: css/sidebar-theme.css 2025-09-01 18:58:59 -04:00
35193b1c09 modified: css/sidebar-theme.css 2025-09-01 18:43:52 -04:00
17a19d53ea new file: css/sidebar-theme.css
modified:   index.html
2025-09-01 18:41:48 -04:00
88dece974a remove 2000 token hard cap for introduction messages - allow full model completion limits 2025-07-28 10:49:29 -04:00
eff2eee69b create centralized Groq TPM rate limit handling for all requests
- new groq-fetch.js provides groqAwareStreamingFetch for all API calls
- automatically detects Groq endpoints and handles TPM rate limits
- works for chat, translation, TTS - any request to Groq
- waits 60 seconds for TPM window reset, retries up to 3 times
- other platforms (Hermes, OpenAI, etc.) unaffected
- cleaner abstraction replaces inline retry logic in chat.js
2025-07-12 13:50:01 -04:00
bd10045c4a make TPM rate limit retry Groq-specific only - other platforms use normal error handling 2025-07-12 13:45:36 -04:00
6335b651e6 implement Groq TPM rate limit handling - wait 60s for token budget reset 2025-07-12 13:44:36 -04:00
04fa0fb05f remove artificial response caps - let models use their full completion token limits 2025-07-12 13:32:56 -04:00
471d61176d allow chat responses to use full available space for coherent answers - cap at 4096 tokens max 2025-07-12 13:31:39 -04:00
8467f58d6b optimize chat token allocation for maximum input/output space
- replace aggressive 1.5x buffer with smart model-based allocation
- 128k+ models: only 2k buffer (was wasting 10k+ tokens)
- 32k models: 1.5k buffer max
- 8k models: conservative buffer
- follow-up responses now have maximum room for both history and output
- especially important for long conversations on large context models
2025-07-12 13:29:58 -04:00
539121a165 increase intro generation tokens to prevent cutoffs - min 500, max 2000 2025-07-12 13:28:13 -04:00
c014adde73 fix intro generation getting cut off by being more generous with output tokens
- reduce aggressive buffering that was leaving too little room for intro output
- reserve up to 1000 tokens or 30% of model capacity for intro generation
- increase minimum output tokens from 100 to 200
- should fix truncated intros especially on Groq models with 8K completion limits
2025-07-12 13:27:08 -04:00
6645061372 make active tab even lighter green - very subtle background with dark green text 2025-07-12 11:46:00 -04:00
5198805155 replace blue active tab with lighter green - much better visual harmony 2025-07-12 11:42:04 -04:00
97af1d9b1a change tab colors from green to blue/gray to reduce visual competition
- tabs now use neutral gray (inactive) and blue (active) instead of green
- prevents distraction from green translate button (call-to-action)
- inactive tabs: light gray background with dark gray text
- active tab: blue background with white text
- hover: slightly darker gray
- updated both builtin and pico CSS versions
2025-07-12 11:37:22 -04:00
3bd518ff20 adjust progress timing: keep Hermes as default, speed up for Groq only 2025-07-12 11:26:05 -04:00
34e5e25bfb replace URL constructor with regex validation for remote URLs
- URL constructor was too strict and failing on valid domains
- new regex pattern validates common URL formats more permissively
- ensures scheme prepending works in both main logic and fetchRemotePageHTML
- fixes issues with domains like www.inrupt.com
2025-07-12 11:13:40 -04:00
c4c216316f auto-add https:// scheme to remote URLs when missing 2025-07-12 11:06:53 -04:00
e6e7a77fa3 use selected model name in translation progress messages
- replace hardcoded "Hermes AI" with actual selected model
- progress now shows "Translating with <model-name>..."
- makes progress messages accurate for all model selections
2025-07-12 11:03:43 -04:00
79377af49a switch tab order: remote URL tab now comes before custom text tab 2025-07-12 11:02:27 -04:00
e87e27b2a2 show both English and native language names in translation dropdown 2025-07-12 11:01:00 -04:00
7851c5747c fix remote URL input height - make it single line instead of textarea height 2025-07-12 08:35:18 -04:00
4c75f7cd6e fix chat and ui max_tokens errors with Groq models
- cap all max_tokens to model's max completion tokens limit
- fixes sendMessageWithCustomHistory in chat.js
- fixes intro generation in ui.js
- fixes TTS processing token calculation
- prevents "max_tokens must be less than or equal to 8192" errors
2025-07-12 07:51:31 -04:00
337f9970a8 improve translation prompt to prevent explanatory text prefixes 2025-07-12 07:40:38 -04:00
41ba59db1b use API-provided max completion tokens instead of hardcoding
Fetch and store maxCompletionTokens from models endpoint to respect
each model's specific output limits. This fixes 400 errors from
Groq models that have different completion limits than context windows.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 07:36:31 -04:00
a864366c80 add hardcoded context windows for Groq models 2025-07-12 07:28:13 -04:00
fd71442331 remove chunking complexity, use simple fit-or-error approach 2025-07-12 07:19:00 -04:00
50a0487bcd fix chunking to work directly with body children and handle edge cases 2025-07-12 07:14:51 -04:00
d4aa30e454 implement sibling-level chunking to prevent DOM duplication 2025-07-12 07:10:14 -04:00
e2c211b9c6 add chunk content debugging to identify duplication source 2025-07-11 22:10:46 -04:00
60743c5bfb add better error logging for fragment translation debugging 2025-07-11 22:04:44 -04:00
b7563d0f2b create separate translation functions for full pages vs fragments 2025-07-11 22:00:07 -04:00
e975e97ea3 fix chunk reassembly stacking and increase batch size for speed 2025-07-11 21:54:54 -04:00
a8a4359e96 auto-detect and cache endpoints without tokenization support
When upstream tokenization fails (404, errors), cache the endpoint
and automatically use JS tokenizer for future requests. This fixes
chunking for APIs like Groq that don't have tokenize endpoints.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-11 21:52:24 -04:00
e50b0b8118 improve token allocation for chunked translations
Changed from broken buffer calculation to 40% output reserve which gives
individual chunks much more room for translation output. Reverted
preservation mode changes to keep original behavior.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-11 21:47:06 -04:00
8dbf607c03 fix duplicate const declaration of getSelectedModel
Consolidated duplicate imports in translate-modal.js that were causing
SyntaxError: redeclaration of const getSelectedModel.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-11 21:42:46 -04:00
04f0ac766b fix token buffer calculation causing chunking to fail
Previous buffer calculation used 1.5x input tokens which exceeded model
context window for large content. Changed to fixed 1000 token reserve
and improved chunk sizing to use reasonable portions of model capacity.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-11 21:38:47 -04:00
7b34e60f63 fix getSelectedModelMaxTokens import in progress calculation 2025-07-11 21:23:51 -04:00
854d22ee79 fix progress calculation to show actual tokens including preservation and chunking 2025-07-11 21:21:58 -04:00
cfc6dec1f4 remove unnecessary preservation in progress calculation that was creating placeholder confusion 2025-07-11 21:18:59 -04:00
a7dddf24d8 fix small model cutoffs by accounting for translation output expansion in chunking logic 2025-07-11 21:02:23 -04:00
a43714f82d fix tab content visibility and add proper styling to URL input 2025-07-11 20:59:43 -04:00
9550fb8e94 re-enable relative to absolute URL conversion for remote translations 2025-07-11 20:54:56 -04:00
e0a784c073 fix remote translation double processing by calling translateHTML directly 2025-07-11 20:53:54 -04:00
004764a05b add translation ID tracking to debug repeated page issues 2025-07-11 20:50:07 -04:00
84b06202dd fix countTokens returning object instead of number causing incorrect chunking 2025-07-11 20:48:40 -04:00
af6108e988 add debug logging to see why chunking is being triggered when it shouldn't 2025-07-11 20:46:44 -04:00
f476b6e476 disable URL conversion during chunking and add debug logging for chunk content 2025-07-11 20:43:09 -04:00
8e78df00c0 fix DOM duplication by chunking only body content and reassembling properly 2025-07-11 20:39:15 -04:00
bbac26b487 fix chunking to work on original HTML before preservation, not placeholder soup 2025-07-11 20:29:05 -04:00
a2a786040e remove preservations references from translateTextRaw function 2025-07-11 20:24:50 -04:00
99bdb1c6f8 clean up preservations scope in translation functions 2025-07-11 20:22:30 -04:00
7d3e7ec111 complete translateTextRaw function to properly separate raw and preserved translation 2025-07-11 20:19:39 -04:00
36828be6ae fix double HTML preservation in chunked translations by using raw translate function 2025-07-11 20:18:46 -04:00
5ddf7d36d7 fix HTML placeholder restoration in parallel chunk translation 2025-07-11 20:16:27 -04:00
eed55b922a add parallel chunk translation for massive speed improvement on large pages 2025-07-11 20:13:38 -04:00
0b02a3d75e implement intelligent chunking for large page translations based on actual token limits 2025-07-11 20:11:31 -04:00
0f51d07eb1 add translateHTML function to unify translation logic between current page and remote page translations 2025-07-11 19:47:04 -04:00
a340ea4f75 convert all relative URLs to absolute URLs in remote translations 2025-07-11 19:42:11 -04:00
46a7a8c92e fix base URL resolution for remote translations using proper URL constructor 2025-07-11 19:31:21 -04:00
d09b204f60 fix preservations variable name in translateRemoteHTML 2025-07-11 19:28:09 -04:00
3d06f28954 fix remote translation to preserve full DOM structure like page translation 2025-07-11 19:25:23 -04:00
4d53518087 fix translate modal: 3 columns layout and always open remote translations in new window 2025-07-11 19:19:38 -04:00
a89a45e177 update CORS proxy to use cors-proxy.uncloseai.com endpoint 2025-07-11 19:05:47 -04:00
2b69a4d17d update CORS proxy URL to use query parameter format 2025-07-11 17:46:23 -04:00
196527c674 use uncloseai.com CORS proxy at /api/fetch/ 2025-07-11 17:05:44 -04:00
695049ff90 use public CORS proxy for remote URL fetching 2025-07-11 17:04:03 -04:00
aa1285ba6c update remote URL fetching to use uncloseai.com CORS proxy 2025-07-11 17:01:33 -04:00
b050bbc6bf fix syntax errors: remove double commas from translation files 2025-07-11 16:50:57 -04:00
928f658328 add remote URL translation tab: fetch and translate any website just-in-time 2025-07-11 16:44:02 -04:00
2587fcf050 fix translation title: add original title + translated into language to new window 2025-07-11 16:35:49 -04:00
5477c2d6a5 fix ui.js buffer variable conflict: rename stream buffer to avoid redeclaration 2025-07-11 16:32:44 -04:00
10de872c6c fix variable conflict: rename stream buffer to avoid const redeclaration error 2025-07-11 16:31:17 -04:00
54f05b0af0 implement proportional token buffer: use 1.5x input tokens instead of fixed 2048 2025-07-11 16:29:11 -04:00
21349b9fcc fix openTranslateModal error: use window.openTranslateModal and remove duplicate import 2025-07-11 16:25:54 -04:00
a991ca4626 improve UX: close button closes settings panel first, then modal on second click 2025-07-11 16:16:48 -04:00
f304b6f634 standardize modal dropdown ID: use 'modal-model-selection' consistently 2025-07-11 15:56:44 -04:00
f939d1c782 fix model endpoint routing: properly route custom endpoint models to custom API with correct credentials 2025-07-11 15:52:08 -04:00
047d1e7856 add debug logging to track custom endpoint detection issue 2025-07-11 15:41:49 -04:00
559dd59522 fix CI pipeline: handle JSDOM dependency gracefully for modal tests 2025-07-11 15:39:08 -04:00
04e7e71e2e fix model refresh: include custom endpoint in model fetching when checkbox is checked regardless of current model routing 2025-07-11 15:36:23 -04:00
8b0fe6d2ac apply 2048 token buffer to all upstream API requests: translation and TTS now calculate available tokens properly 2025-07-11 15:25:02 -04:00
3a4952087d increase token buffer to 2048 for safer margin 2025-07-11 15:15:23 -04:00
96f8edc2e7 fix token limit calculation: use available tokens instead of model max to prevent context overflow 2025-07-11 15:04:29 -04:00
f7ddf32fff track package-lock.json for reproducible builds 2025-07-11 14:59:05 -04:00
cc03367fb1 update gitignore: add node_modules and package-lock.json 2025-07-11 14:58:29 -04:00
527fbfad1b remove jest dependencies and old test files: clean migration to custom test framework 2025-07-11 14:57:22 -04:00
133daabd77 add jest-free testing framework: comprehensive integration, modal, and simple test suites with improved error handling 2025-07-11 14:56:57 -04:00
ca9c994bfe fix model endpoint routing: use model's registered endpoint over custom when available, add page title to translate modal tab 2025-07-11 14:56:02 -04:00
2363944fa6 update src files with latest changes 2025-07-11 14:30:44 -04:00
affa6f5ca8 modified: journal.rst 2025-07-11 07:28:18 -04:00
595ac0a597 add model count feedback to refresh button: shows loading state and final count 2025-07-11 01:24:01 -04:00
cd218cc4a1 fix refresh models button: include custom API config in cache key to properly bust cache 2025-07-11 01:08:57 -04:00
02e614d733 add detailed logging for custom API model fetching to debug blog site issue 2025-07-11 01:05:35 -04:00
0af7efc863 fix model selection: add missing ID to modal model dropdown 2025-07-11 00:37:02 -04:00
525c4c315f add debug logging to getSelectedModel to trace model selection issue 2025-07-11 00:34:03 -04:00
98592efb8d Revert "fix model mismatch: validate selected model exists on custom API, fallback to default if not"
This reverts commit f4f6ae6a9c.
2025-07-11 00:32:45 -04:00
f4f6ae6a9c fix model mismatch: validate selected model exists on custom API, fallback to default if not 2025-07-11 00:31:52 -04:00
fd4cf9684b add dynamic max_tokens to all chat functions: regular chat, custom history, and intro generation 2025-07-11 00:27:28 -04:00
1b571b4ed6 add dynamic max_tokens support: capture from /v1/models API and use in translation 2025-07-11 00:24:33 -04:00
461194ee83 use base URL as endpoint ID and remove model name input field - models now selected from dropdown 2025-07-11 00:14:51 -04:00
81a4062b19 fix custom API models registry: use endpointsToFetch instead of VLLM_ENDPOINTS for lookup 2025-07-11 00:11:14 -04:00
c96a09c1f1 add custom API models to refresh: fetch /v1/models from custom endpoint when configured 2025-07-11 00:09:05 -04:00
dc95e4c535 add !important to grid styles in PicoCSS override to force 2-column layout 2025-07-11 00:05:48 -04:00
35157b052b revert dynamic import: put getAPIConfig back to static import 2025-07-11 00:03:57 -04:00
be508bcfb1 fix module caching issue: dynamically import getAPIConfig in ui.js to prevent blog site button failure 2025-07-11 00:02:45 -04:00
e9a1acf546 add detailed error logging for API failures to help debug custom endpoint issues 2025-07-10 23:52:25 -04:00
83c6ac9a2c patch all API calls: translate and intro generation now use custom API when configured 2025-07-10 23:50:20 -04:00
4259e6774c add console logging to verify custom API configuration is working 2025-07-10 23:42:23 -04:00
96082cf750 implement custom API endpoint support: centralized config function intercepts all chat API calls 2025-07-10 23:38:05 -04:00
8560ea43ce update useCustomAPI translations across all languages 2025-07-10 23:34:32 -04:00
d557ffcfbe remove labels from API configuration inputs and update placeholders 2025-07-10 23:28:48 -04:00
ebc574db25 remove unused translation keys from all language files 2025-07-10 23:21:03 -04:00
2284c4e7cb remove section labels from settings panel for cleaner UI 2025-07-10 23:16:44 -04:00
0c6277536c add mobile responsive override for settings grid to stack on small screens 2025-07-10 23:12:37 -04:00
d12428c267 restructure settings panel with two columns and add model name field to API config 2025-07-10 23:09:47 -04:00
10c5352739 move quick actions to top of settings panel 2025-07-10 23:06:51 -04:00
68b2c85966 add API configuration UI to settings panel with grid layout 2025-07-10 23:04:55 -04:00
988349b67e fix: remove hardcoded English from alerts and pass errors verbatim
- Replace English wrapper text in error alerts with direct error.message
- Use existing translation keys where available (pleaseEnterTranslateText, pleaseSelectFile)
- Remove unnecessary alerts for modal failures and operation errors
- All error messages now passed through verbatim without English text
2025-07-10 22:45:19 -04:00
f87f59c194 fix: reset heading styles to prevent blog site CSS interference
- Add CSS rules to reset all padding and margin for h1-h6 inside our modals
- Prevents blog site heading styles like 'padding-top: 30px' from breaking modal layouts
- Applied to all three modals: uncloseai-embedded-modal, tts-modal, translate-modal
2025-07-10 22:20:50 -04:00
78e4a80f36 fix: remove light background from chat area
- Change .uncloseai-modal-chat-area background from #f8f9fa to transparent
- Prevents white background showing behind short AI messages on blog sites
2025-07-10 22:15:01 -04:00
e02fb10b11 fix: remove PicoCSS modal overlay effects for TTS and translate modals
- Override --pico-modal-overlay-backdrop-filter and --pico-modal-overlay-background-color
- Set both variables to none/transparent for dialog#tts-modal and dialog#translate-modal
- Prevents upstream PicoCSS from applying default modal backdrop effects
2025-07-10 22:10:17 -04:00
4f4993b605 feat: add translations for chat message button tooltips
- Add deleteMessage, copyRawMarkdown, copyHTML translation keys to all 19 language files
- Update all chat bubble button tooltips to use translation functions
- Provides proper localization for Delete message, Copy raw (Markdown), and Copy HTML buttons
2025-07-10 22:01:43 -04:00
48c5146e65 fix: resolve chat bubble download button issues
- Fix multiple download button creation by using correct selector
- Add download buttons to historical messages during creation
- Ensure download buttons appear next to play buttons consistently
- Show/hide download buttons instead of creating them dynamically
2025-07-10 21:54:32 -04:00
e58303f971 fix: update pico.css modal controls grid template to match builtin 2025-07-10 21:46:53 -04:00
d2a2683ea2 fix: correct modal controls grid template and adjust spacing to 18px 2025-07-10 21:43:52 -04:00
0c64b7c137 fix: add downloadChatAudio translations and improve chat bubble UX
- Add downloadChatAudio translation key to all 19 language files
- Update chat bubble download buttons to use correct translation
- Fix download button duplication by creating buttons initially
- Position download buttons next to play buttons in chat bubbles
- Adjust action toolbelt spacing to 24px for better layout
2025-07-10 21:38:11 -04:00
c3d336806f modified: src/languages/ar.js
modified:   src/languages/bn.js
	modified:   src/languages/de.js
	modified:   src/languages/en.js
	modified:   src/languages/es.js
	modified:   src/languages/fr.js
	modified:   src/languages/hi.js
	modified:   src/languages/id.js
	modified:   src/languages/ja.js
	modified:   src/languages/ko.js
	modified:   src/languages/mr.js
	modified:   src/languages/pt.js
	modified:   src/languages/ru.js
	modified:   src/languages/sw.js
	modified:   src/languages/te.js
	modified:   src/languages/tr.js
	modified:   src/languages/ur.js
	modified:   src/languages/zh-tw.js
	modified:   src/languages/zh.js
	modified:   src/uncloseai-embed-modal.js
2025-07-10 20:57:59 -04:00
86ce18f181 fix: implement tooltip-based read page status with persistent download button
- Convert read page button to use tooltips for status updates
- Add download button that appears after audio generation
- Keep download button persistent after audio ends for continued access
- Remove DOM status container creation, use tooltips instead

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-10 20:21:07 -04:00
30e80b98b3 fix: complete tooltip implementation and remove DOM status element
- Update all readPageWithHermes calls to pass button parameter
- Remove CSS styling for page reader status DOM element
- Ensure all Read Page buttons use tooltips instead of separate status div

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-10 20:04:25 -04:00
40a95fa7a6 fix: convert read page status messages to tooltips with translations
- Replace DOM status element with tooltip functionality in page-reader.js
- Add translation keys for read page status messages to all 18 languages
- Update page-reader.js to use button tooltips instead of separate status div
- Fix TTS download button duplication guard with consistent CSS classes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-10 19:54:13 -04:00
28db40609d fix: redesign read page status as bottom-right tooltip 2025-07-10 19:19:00 -04:00
db5a243228 fix: remove chat area padding and unify copy button styling
- Set chat area padding to 0px
- Remove light mode specific copy button styling for consistency

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-10 19:10:01 -04:00
6419528ae1 fix: theme-aware styling for copy button, logo, and chat box
- Remove copy button background, add light mode styling with white bg
- Ensure modal title stays white across themes
- Set chat box to off-white (#f8f9fa) for light mode, dark (#1a1a1a) for dark mode

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-10 19:05:53 -04:00
b9ee32e518 fix: apply syntax highlighting to code blocks on page reload
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-10 18:17:54 -04:00
e8066e29f9 fix: light mode styling for copy button and modal title
- Add light mode specific styling for copy code button (white bg, dark text)
- Ensure modal title stays white in both builtin and pico CSS versions
- Prevent PicoCSS from overriding title color in light mode

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-10 18:15:26 -04:00
bbbd24503f fix: set chat bubble font size to 14px explicitly
- Add font-size: 14px to .user-message and .ai-message classes
- Prevents inheritance of 18px font from emoji-only styles
- Ensures chat bubbles maintain proper 14px text size
- Applied to both builtin and PicoCSS styles
2025-07-10 18:06:11 -04:00
d0096e6943 fix: lower copy code button z-index to stay below settings panel
- Reduce z-index from 10 to 5 for copy code buttons
- Prevents buttons from appearing on top of settings panel
- Settings panel has z-index 10 and should stay on top when open
- Applied to both builtin and PicoCSS styles
2025-07-10 18:03:06 -04:00
46def6655b fix: move copy code button to bottom-right and increase modal width
- Move copy code button from bottom-left to bottom-right of code blocks
- Increase main modal width from 70vw to 80vw
- Increase max-width from 800px to 1000px
- Prevents horizontal scrolling in chat area
- Applied to both builtin and PicoCSS override styles
2025-07-10 17:32:12 -04:00
884364a934 style: change action buttons from pills to square emoji buttons
- Change border-radius from 20px to 6px for square corners
- Make emoji-only buttons fixed 36x36px squares
- Use grid layout with place-content: center for emoji centering
- Matches the style of emoji buttons used in chat messages
- Reduced padding to 8px for consistent square appearance
2025-07-10 17:28:15 -04:00
9407f157bc fix: convert markdown button to emoji format
- Change fullPageRaw button from text to emoji + tooltip format
- Use 📝 emoji for markdown conversion functionality
- Follows same pattern as other action buttons
2025-07-10 17:25:30 -04:00
a5e135eca6 fix: convert action buttons to emoji-only with tooltips and remove modal backdrops
- Remove smart translate button functionality (non-working)
- Convert all action buttons to emoji + tooltip format:
  📖 Read Page, 🔊 TTS Anything, 🌐 Translate, 📋 Copy Raw, 📄 Copy HTML
- Add emoji-only CSS class with 18px font size for emojis
- Add backdrop removal for TTS and translate modals in builtin CSS
- Update button creation to use title attribute for tooltips
- Reduce padding for emoji-only buttons (8px 12px vs 8px 16px)
2025-07-10 17:14:15 -04:00
85415e2221 fix: make action buttons expand to fit text content
- Remove overflow hidden and text-overflow ellipsis from buttons
- Change grid-auto-columns from minmax(0, max-content) to max-content
- Allow buttons to stretch to accommodate full text without truncation
- Keep white-space: nowrap to prevent text wrapping
2025-07-10 17:01:42 -04:00
a8be7730a3 fix: improve settings panel positioning and button centering
- Lower settings panel by 15px more (80px to 95px top offset)
- Use place-content instead of place-items for proper grid centering
- Keep all layouts using CSS Grid as requested
- Fix gear and close button centering in both CSS files
2025-07-10 16:55:33 -04:00
7f64ff0d51 fix: remove distracting PicoCSS backdrop filters from modals
- Add ::backdrop overrides for all dialog modals
- Set backdrop-filter to none and background-color to transparent
- Applied to uncloseai-embedded-modal, translate-modal, and tts-modal
- Removes the blurry backdrop effect from PicoCSS theme
2025-07-10 16:39:30 -04:00
e2b413917f fix: improve settings panel and button overflow issues
- Increase settings panel top offset from 60px to 80px to avoid header
- Remove max-height: 40vh restriction on settings panel
- Add overflow hidden and text-overflow ellipsis to buttons
- Change grid-auto-columns to minmax(0, max-content) for proper sizing
- Ensure buttons don't expand beyond their content width
2025-07-10 16:34:33 -04:00
e7f90a266d fix: repair grid layout issues from conversion
- Use grid-auto-flow: column for horizontal button layouts
- Fix controls and message actions to display horizontally
- Add theme-aware background colors for settings panel
- Move settings panel top position to avoid covering header
- Ensure all button text stays within button boundaries
2025-07-10 16:27:31 -04:00
a7323e68cd refactor: convert all flexbox layouts to CSS Grid in modal styles
- Convert modal container from flex column to grid with explicit rows
- Replace flex centering with grid place-items: center
- Convert header layouts from flex space-between to grid 1fr auto
- Transform input areas from flex to grid column layouts
- Update chat messages from flex column to grid single column
- Convert TTS controls, voice selection, and audio controls to grid
- Replace flex-wrap patterns with grid auto-fit minmax columns
- Remove all flex-shrink, flex-direction, and space-between properties
- Update align-self flex-start/end to justify-self start/end for grid
- Apply changes to both builtin and PicoCSS override stylesheets

This completes the systematic conversion from Flexbox to CSS Grid
layouts throughout the modal interface as requested.
2025-07-10 13:49:37 -04:00
8c61e7e8e4 fix: settings panel now fills entire modal using CSS Grid
- Changed settings panel from flex to CSS Grid layout
- When open, settings panel covers the entire modal with absolute positioning
- Uses grid-template-columns: 1fr with 20px gap and align-content: start
- Added background color and z-index for proper overlay behavior
- Updated both PicoCSS and built-in CSS versions
2025-07-10 13:15:32 -04:00
4517e280fa fix: improve button and message visibility
- Increased font-size from 13px to 18px for .uncloseai-btn-control (toolbelt buttons)
- Moved code copy button to bottom-left and increased size to 18px
- Increased background opacity from 0.7 to 0.9 for code copy button
- Changed message backgrounds from solid colors to 95% opacity for better visibility
- Updated both PicoCSS and built-in CSS versions
2025-07-10 08:35:14 -04:00
b515efac53 fix: increase emoji button font size from 12px to 18px
- Updated .uncloseai-btn-small font-size from 12px to 18px in both CSS files
- Makes emoji icons more visible and appropriately sized
- Improves button usability and visual hierarchy
2025-07-10 08:25:51 -04:00
54d98679ff fix: increase floating button width to prevent text wrapping
- Increased width from 110px to 120px to accommodate 'uncloseai.' text
- Added white-space: nowrap to prevent any text wrapping
- Updated both PicoCSS and built-in CSS versions
2025-07-10 08:23:56 -04:00
d9ee717d13 fix: ensure CSS loads before creating floating button
- Wait for CSS to load via onload event before creating button
- Prevents button appearing unstyled on initial page load
- Fallback to direct modal opening if toggleUncloseaiEmbeddedModal not available
- Refactored button creation into separate function for better flow control
2025-07-10 07:03:48 -04:00
675b8fefd1 fix: add delay for CSS loading to ensure floating button styles are applied
- Added 100ms delay when CSS needs to be loaded for floating button
- This ensures CSS rules are parsed before button is displayed
- Fixes issue where button appeared unstyled in top-left corner
2025-07-10 07:00:50 -04:00
a1a53fcf07 fix: load CSS for floating button on blog sites to restore position and styling 2025-07-10 06:56:56 -04:00
3f9cb2c40f refactor: replace all inline styles with CSS classes for better maintainability
- Moved 115+ inline styles from uncloseai-embed-modal.js to CSS classes
- Cleaned all inline styles from page-reader.js, widget-library.js, chat.js, models.js, file-upload.js
- Added CSS custom properties for theming support
- Created reusable CSS classes for buttons, containers, and UI elements
- Only retained legitimate dynamic styles (height calculations, position adjustments)
- Improved consistency with PicoCSS overrides
2025-07-09 21:32:55 -04:00
4561b9edcc fix: implement proper theming with CSS custom properties
- Fixed the inline style replacement to maintain dynamic theming
- Added CSS custom properties (variables) for theme-aware styling
- CSS classes now use var() with fallback values for light theme
- JavaScript sets CSS custom properties on theme changes
- Maintains full light/dark theme compatibility for all button types

This ensures the replaced CSS classes work correctly with the existing theming system instead of using static colors.
2025-07-09 20:51:45 -04:00
c7dbb4afcc feat: major inline style cleanup progress
- Replaced TTS, delete, and download buttons with utility classes
- Converted message action containers to CSS classes
- Replaced form labels and sections with semantic classes
- Reduced inline styles from 131 to 99 in main modal (32 styles removed)
- Updated CSS classes to match exact styling requirements
2025-07-09 20:43:31 -04:00
5d175fb72e feat: continue inline style cleanup with control button utility class
- Added uncloseai-btn-control class for standard modal action buttons
- Replaced control button inline styles with CSS class
- Reduced inline styles from 131 to 118 in main modal file
- Established pattern for systematic cleanup of remaining 200+ inline styles
2025-07-09 20:38:00 -04:00
be1b22e2f0 feat: add utility CSS classes and start replacing inline styles
- Added comprehensive utility CSS classes to both builtin and pico stylesheets
- Replaced copy button, close button, title container, and header right styling
- 208 total inline styles identified across codebase
- Started systematic replacement with reusable CSS classes
2025-07-09 20:36:03 -04:00
a49c9973ae fix: make settings gear icon white with text-shadow for better visibility 2025-07-09 20:29:07 -04:00
99856600c0 docs: rewrite journal entry for modal CSS overhaul with complete solution 2025-07-09 18:34:12 -04:00
44917ceffd docs: update journal entry with mobile experience fix 2025-07-09 18:32:07 -04:00
f24219634f fix: mobile modal now fills 100% height/width up to 768px breakpoint 2025-07-09 17:01:22 -04:00
d48b9d623f docs: add journal entry for modal CSS architecture overhaul 2025-07-09 16:46:37 -04:00
5157e37de8 fix: use consistent CSS loading pattern with PicoCSS detection and absolute URLs 2025-07-09 16:43:46 -04:00
bf9d7da770 fix: use base tag to resolve relative URLs in translated pages 2025-07-09 16:30:14 -04:00
275f9e02fa fix: convert ALL relative URLs to absolute in translated pages
- Convert CSS links, script sources, image sources, and navigation links
- Use current page URL as base for proper relative URL resolution
- Skip data: URLs, fragments, and protocol-relative URLs
- Ensure all assets load properly in new window context
2025-07-09 16:26:24 -04:00
ba7885afe6 fix: convert relative CSS URLs to absolute in translated pages
- Convert relative CSS URLs to absolute URLs using current origin
- Ensure translated pages can load original blog CSS in new window
- Maintain modal functionality with external CSS files
- Skip URLs that are already absolute (http/https or protocol-relative)
2025-07-09 16:23:38 -04:00
761c4ec259 debug: add console logging to CSS inlining for translated pages
- Add logging to see how many CSS links are found
- Log each CSS file being inlined and success/failure
- Help diagnose why translated pages may not have modal styling
2025-07-09 16:21:40 -04:00
c65bde327c fix: inline external CSS in translated pages for new window compatibility
- Fetch external uncloseai CSS files and inline them as <style> tags
- Replace body content with translated version while preserving page structure
- Ensure new window is self-contained and doesn't need external CSS requests
- Maintain new window approach to avoid breaking current page
2025-07-09 16:18:36 -04:00
1b7597501a revert: restore original translateCurrentPage function
The CSS refactoring broke translated page rendering. The original function
was working correctly by translating the complete HTML document structure.
Translated pages don't need modal CSS, just the original page styling.
2025-07-09 16:14:44 -04:00
fbe81a8298 fix: preserve HTML structure and styling in translated pages
- Update translateCurrentPage() to preserve original page HTML structure
- Extract only text content for translation while keeping head/styles
- Create properly formatted translated page with clean typography
- Fix issue where translated pages opened in new windows lacked CSS styling
2025-07-09 16:13:25 -04:00
f3bd60e578 fix: correct CSS file domain from ai.unturf.com to uncloseai.com 2025-07-09 16:07:47 -04:00
20a95224fd fix: use actual PicoCSS presence detection instead of USE_CUSTOM_STYLING flag
- Change CSS loading logic to detect actual PicoCSS link tags on page
- Blog sites without PicoCSS now correctly load uncloseai-modal-builtin.css
- PicoCSS sites load uncloseai-modal-pico.css with proper overrides
- Fix modal appearing full-screen on blog sites that don't use PicoCSS
2025-07-09 16:04:49 -04:00
121d79d838 fix: add !important to PicoCSS overrides to force proper modal sizing 2025-07-09 16:00:25 -04:00
b46aedcdf3 fix: restore isMobile variable definition for responsive text sizing 2025-07-09 15:23:06 -04:00
e3e6e08449 feat: complete CSS refactoring - remove all inline styles
- Remove all inline CSS from translate-modal.js, tts-modal.js, and widget-library.js
- Replace inline styles with CSS classes in both builtin and pico CSS files
- Eliminate USE_CUSTOM_STYLING conditionals from styling code
- Add proper CSS classes: translate-modal-*, tts-modal-*, widget-*
- Maintain all functionality while achieving clean separation of concerns
- Fix modal sizing issues on large displays through proper PicoCSS overrides
- Support responsive design with mobile-first approach (≤480px full screen)
- Pass all tests: formatting, exports, translations, and integration
2025-07-09 15:20:06 -04:00
adfbdc395b feat: refactor all modal styles to dedicated CSS files
- Create uncloseai-modal-builtin.css for blog versions
- Create uncloseai-modal-pico.css for PicoCSS override
- Add conditional CSS loading to all modal JavaScript files
- Remove inline styles from main modal, translate modal, TTS modal, and widget library
- Fix modal full-screen issue on large displays with proper PicoCSS overrides
- Maintain mobile-first responsive design (≤480px full screen)
- Support both USE_CUSTOM_STYLING modes for maximum compatibility
2025-07-09 14:57:30 -04:00
f0d27bbbef fix: reduce modal width and enforce max-width constraint
Changed desktop modal width from 90vw to 70vw and added \!important
to max-width to ensure modal doesn't fill entire screen on large displays.
2025-07-09 14:26:03 -04:00
de9eeffe70 fix: prevent modal from filling full screen on laptops
Changed mobile detection threshold from 768px to 480px to ensure
centered modal behavior on desktop/laptop screens while preserving
full-screen experience on actual mobile devices.
2025-07-09 10:00:45 -04:00
72c73b9a99 fix: reorganize Python and Node.js examples under Hermes AI Model section
- Changed Node.js Examples from h2 to h3 to be subsection under Using the Hermes AI Model
- Updated table of contents to group all Python and Node.js examples under main Hermes section
- Improved logical document structure with consistent hierarchy
- All AI model examples now properly organized under single main section

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 15:59:20 -04:00
98996476a8 fix: enhance table of contents visual hierarchy with custom CSS
- Added distinct bullet styles (disc for main, circle for sub-items)
- Implemented proper indentation with padding and margins
- Added subtle left border for nested sections using theme color
- Differentiated font sizes between main sections and subsections
- Improved line height and spacing for better readability
- Maintains PicoCSS compatibility while clearly showing hierarchy

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 15:56:28 -04:00
d9569c77fe feat: complete table of contents and header structure overhaul
- Added comprehensive hierarchical table of contents with proper h2/h3 nesting
- Added missing section IDs to all headers for proper anchor navigation
- Restructured headers to use only h2 (main sections) and h3 (subsections)
- Organized content with logical grouping: Web Client, OpenAI Client, Hermes Model, Node.js, TTS
- Enhanced navigation with complete coverage of all sections and subsections
- Improved document structure and accessibility with semantic HTML

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 15:53:57 -04:00
38054643d4 fix: use semantic HTML and PicoCSS styling for table of contents
- Changed from <section> to <aside> for proper semantic navigation
- Removed custom CSS overrides that conflicted with PicoCSS
- Simplified structure from nested ordered/unordered lists to flat unordered list
- Removed inline styling to let PicoCSS handle navigation appearance
- Improved accessibility and framework compatibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 15:41:25 -04:00
d9b9b8a5c1 feat: add comprehensive table of contents to index page
- Added navigational table of contents with hierarchical structure
- Added missing section IDs for proper anchor linking
- Organized content into logical sections with subsections
- Included prominent link to demo page for interactive examples
- Improved page navigation and user experience

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 15:25:02 -04:00
b9be6e512a fix: streamline quick start section and ensure consistent code-first demo structure
- Streamlined index.html quick start section to focus on one-line installation
- Fixed demo.html section ordering to consistently show code examples before live demos
- Corrected section 12 numbering (was duplicate section 11)
- Enhanced demo page user experience with proper code-then-demo flow

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 15:11:17 -04:00
f79a66ddf0 feat: add comprehensive code documentation to Custom API Examples section 2025-07-04 14:43:51 -04:00
af7019f91b fix: make all code examples visible by default
- Add 'open' attribute to all <details> sections
- Ensure code examples are immediately visible to users
- No need to click to expand - documentation should be accessible
2025-07-04 14:36:30 -04:00
1bf4d26a8a feat: comprehensive demo page documentation improvement
- Add TTS modal button example with openTTSModal() function
- Add translation modal button example with openTranslateModal() function
- Add standalone read page button example with readPageWithHermes() function
- Reorganize all sections to show live examples first, then code examples
- Improve documentation flow: demo first, then implementation details
- Add detailed feature lists and best-use-case descriptions
- Provide multiple implementation options for each feature (widget vs modal vs API)
- Update table of contents to reflect new organization
- Make it easy for people to add widgets to any webpage with clear examples
2025-07-04 14:32:13 -04:00
c57d86c27f fix: properly load all widget functions for demo page
- Restore translate modal demo - it works fine on main app
- Add manual imports for openTranslateModal, openTTSModal, readPageWithHermes
- Ensure all widget functions are available globally for demo widgets
- Fix module loading issues that were causing "is not a function" errors
- Make translation modal work as isolated tool in demo
2025-07-04 14:24:52 -04:00
9148d26496 fix: completely remove smart translate dropdown from demo
- Comment out entire smart dropdown section - no point advertising broken features
- Simplify translation section to only show working translation modal
- Update section title from "Translation Options" to "Translation Modal"
- Change "Live Examples" to "Live Example" (singular)
- Clean up code examples to only show working translate feature
- Remove confusing "two ways to translate" messaging
2025-07-04 14:17:52 -04:00
61b4cb0d5c fix: disable non-working features in demo.html
- Remove file upload references from system message
- Comment out smart-translate feature examples (not working yet)
- Disable smart translate demo with "Under Development" styling
- Keep traditional translate modal working
- Style disabled features with greyed-out appearance
- Fix demo page to only show working functionality
2025-07-04 12:49:53 -04:00
ce7014e83a fix: resolve handleFileUpload ReferenceError while preserving upload code for future
- Restore handleFileUpload import in widget-library.js to fix ReferenceError
- Keep all upload functionality code intact for future development
- Disable upload features in demo.html with "Under Development" messaging
- Remove upload from feature button examples but preserve the code
- Style disabled upload demo with greyed-out appearance
- Fix demo page functionality while maintaining upload code for future fixes
2025-07-04 12:43:04 -04:00
38fdedb6a6 docs: enhance language support documentation and remove auto-detection claims
- Expand README language section with complete list of 19 supported languages
- Add native language names and language codes for each supported language
- Detail language features: manual selection, complete coverage, AI responses
- List comprehensive translation coverage areas (modals, buttons, messages, etc.)
- Update project structure to show language files with descriptions
- Remove auto-detection claims since feature is not working properly
- Change to accurate "manual selection" and "AI-powered translation"
- Fix demo.html to remove false auto-detection advertising

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 12:31:53 -04:00
8c99a0316e fix: correct demo.html issues and update language count to 19
- Fix demo.html language count from 18 to 19 (verified by counting files)
- Remove "omni-directional" translation claims and use standard "Translation"
- Fix custom TTS example to use speakTextDirect instead of speakText (no Hermes preprocessing)
- Fix all demo functions to properly import modules dynamically
- Add missing imports for sendMessage, uploadFile, speakTextDirect, translateText
- Fix broken custom chat, TTS, file upload, page analysis, and translation examples
- Update README.rst and index.html language counts to accurate 19 languages
- Fix typo "transation" to "translation" in index.html

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 12:28:15 -04:00
d7cc5531b8 feat: improve streaming completion detection and add syntax highlighting with copy buttons
- Detect when model streaming is complete in all streaming locations
- Add syntax highlighting (hljs) to code blocks after streaming finishes
- Add copy buttons to code blocks after highlighting is applied
- Prevent duplicate copy buttons with CSS class detection
- Optimize performance by doing syntax highlighting and copy buttons only once per message
- Apply improvements to both modal streaming and standalone chat
- Add copy buttons to intro message generation as well

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 12:22:18 -04:00
41818a202b docs: add comprehensive README.rst and documentation sections to index.html and demo.html
- Created extensive README.rst with full project documentation
- Added brief documentation sections to index.html and demo.html
- Emphasized public domain licensing and open web philosophy
- Fixed branding to use lowercase uncloseai. and unturf.
- Points users to git repository for complete technical details

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 12:13:15 -04:00
91f446f5ed Revert "fix: explicitly set white color for uncloseai logo text in PicoCSS light mode"
This reverts commit f50ccabcd1.
2025-07-04 12:03:57 -04:00
f50ccabcd1 fix: explicitly set white color for uncloseai logo text in PicoCSS light mode 2025-07-04 11:59:32 -04:00
b49e52678b Revert "fix: use original page content for title generation instead of processed TTS content"
This reverts commit b178e039b2.
2025-07-03 22:40:55 -04:00
b178e039b2 fix: use original page content for title generation instead of processed TTS content 2025-07-03 22:39:55 -04:00
7bb43c7613 fix: make dropdown text color conditional for PicoCSS and fix download button positioning to be next to play buttons 2025-07-03 22:32:44 -04:00
2e655a206b fix: make tab styling conditional - only apply custom CSS for blog version, let PicoCSS handle native styling 2025-07-03 22:20:55 -04:00
f4c39823b1 fix: add proper tab styling to replace bullet points with visual tab design 2025-07-03 22:17:40 -04:00
d8642675df update translate modal title from 'Translate Page' to 'Translate' across all 19 languages 2025-07-03 22:17:01 -04:00
375471e81a feat: replace button-based tabs with PicoCSS tab navigation in translate modal 2025-07-03 22:11:20 -04:00
d24b6fa1bb fix: use correct variable name targetLanguage instead of targetLanguageCode 2025-07-03 22:04:43 -04:00
28d0b516f1 fix: use exact tokenization in translate modal UI instead of rough estimate 2025-07-03 22:01:01 -04:00
e300a93545 fix: replace tokenInfo reference with roughTokenEstimate in translate modal 2025-07-03 21:49:44 -04:00
f53742a9d5 fix: tokenize actual prompt sent to AI instead of raw HTML and remove duplicate tokenization from modal 2025-07-03 21:46:22 -04:00
cda3455a22 fix: remove enforceModalTextColors function and set black text directly on textarea and dropdowns 2025-07-03 21:36:13 -04:00
a8de98a56a fix: improve tokenizer response parsing and force black text on all modal elements 2025-07-03 21:30:41 -04:00
04b062f3f8 fix: strip v1 from tokenize endpoint since it uses base URL not v1 API 2025-07-03 21:24:37 -04:00
5364b6c446 fix: revert v1 prefix from API endpoints to restore Read Page functionality 2025-07-03 21:20:14 -04:00
c2aa8f1fbd fix: ensure form elements have black text on white backgrounds 2025-07-03 21:18:58 -04:00
969dcbc31e fix: simplify modal text colors and add v1 prefix to API endpoints 2025-07-03 21:16:05 -04:00
29a6a82fa1 fix: enforce modal text colors to prevent white-on-white text issues
- Add enforceModalTextColors() function with high CSS specificity
- Use !important declarations to override blog CSS
- Force dark text (#212529) on inputs, buttons, and general content
- Keep white text on user messages for proper contrast
- Add user-message class to user messages for targeted styling
- Prevents unreadable white text on white backgrounds on blog
2025-07-03 21:11:05 -04:00
401d245fe4 CRITICAL FIX: trash buttons now properly update localStorage
- All delete buttons now call loadConversationHistory/saveConversationHistory
- Update both localStorage and chat.js module for consistency
- User message delete: filters by content match
- AI message delete: removes both user and AI messages in pair
- Historical message delete: removes by array index
- Intro message delete: filters by assistant role and content
- No more localStorage sync issues with deleted messages
2025-07-03 21:02:12 -04:00
933ad570ac feat: major TTS improvements for chat messages and Read Page
- Chat TTS now uses speakTextDirect (no Hermes preprocessing, faster)
- Add download buttons (💾) next to all TTS buttons after processing
- Download buttons use generateTitleForTTS for smart filenames
- Read Page download now uses title generator instead of document.title
- Download buttons appear for user messages, AI responses, and historical messages
- Fallback filenames if title generation fails
- All TTS processing is now direct without Hermes delay
2025-07-03 20:58:40 -04:00
cb88b2da38 debug: add error logging to sendMessage wrapper
- Add try/catch with detailed error logging
- Check if sendMessage exists in imported module
- Log module contents if function not found
- Helps diagnose 'sendMessage(...) is not iterable' error
2025-07-03 20:51:50 -04:00
0213a7e632 fix: improve TTS filename generation with strict prompt and fallback
- Enhanced system prompt: MUST generate title, NEVER refuse
- Clear instructions for edge cases (empty, inappropriate, nonsensical text)
- Added fallback logic if Hermes returns empty or refusal messages
- Fallback uses first 30 chars of text or 'audio-file' as last resort
- Ensures downloads always have a valid filename
2025-07-03 20:48:47 -04:00
46f74eab59 CRITICAL FIX: restore chat functionality - sendMessage must be generator
- Change sendMessage to async generator function with yield*
- Fixes 'Error: sendMessage(...) is not iterable' error
- Restores chat functionality in Hermes modal
2025-07-03 20:45:28 -04:00
20ed410ff0 fix: multiple improvements to TTS modal and language switching
- Fix TTS speed control: change default to 0.9, step by 0.05, max 2.0
- Apply playbackRate to audio element so speed actually works
- Add refreshModalUI() function to update UI when language changes
- Re-add upstream tokenizer API with simplified token counting
- Format token counts with commas (e.g., 8,432 tokens)
- Add getTokenCount() for simple integer token counts
- Language switching now refreshes modal UI without hard refresh
2025-07-03 20:42:11 -04:00
beab5d01a8 feat: reuse markdown extraction logic for TTS spoken tokens
- extractSpokenTokens now detects if input is already markdown format
- Updated prompt to handle both markdown and HTML input
- Added markdown to plain text conversion in prompt (bold, links)
- processContentWithHermes now delegates to extractSpokenTokens
- Consistent processing pipeline whether from Batman button or Read Page
- Better reuse of extractWebpageContent markdown format
2025-07-03 20:28:18 -04:00
4dc9568079 feat: improve extractSpokenTokens with JavaScript pre and post-processing
- Pre-clean HTML before sending to Hermes (remove scripts, styles, ads, iframes)
- Decode HTML entities (&nbsp; &amp; etc.) before Hermes processing
- Simplified, clearer system prompt focused on extraction tasks
- Post-process Hermes output with cleanTextForTTS() for final cleanup
- Reduces Hermes workload and improves extraction quality
- Three-stage pipeline: JS pre-clean → Hermes extraction → JS post-clean
2025-07-03 20:23:52 -04:00
17925537a6 feat: optimize TTS with direct processing for modal and Hermes for Read Page
- TTS modal now uses speakTextDirect() with simple JavaScript text cleaning
- No more Hermes preprocessing for modal text (faster, simpler)
- Read Page button still uses speakText() with Hermes for article extraction
- Reduced textarea height from 240px to 100px with vertical resize
- Added cleanTextForTTS() to remove markdown, HTML, and excess whitespace
- Modal gets instant TTS while Read Page gets intelligent content extraction
2025-07-03 20:20:59 -04:00
78ab413e8e fix: remove manual positioning from TTS modal for proper centering
- Remove top, left, and transform properties
- Let dialog element use native browser centering
- Fixes positioning issues across different viewports
- Cleaner solution that works better with dialog element
2025-07-03 20:14:35 -04:00
6cd16664f7 modified: src/tts-modal.js 2025-07-03 20:03:12 -04:00
67408f0b1a feat: improve TTS filename generation with better system prompt
- Enhanced system prompt to generate 5-10 word descriptive filenames
- Added robust text cleaning to remove quotes, special characters
- Limit filename length to 50 chars for filesystem compatibility
- Better dash-separated filename format like 'javascript-array-methods-guide.mp3'
2025-07-03 19:33:49 -04:00
da4ff80655 feat: improve TTS filename generation with better system prompt
- Enhanced system prompt to generate 3-5 word descriptive filenames
- Added robust text cleaning to remove quotes, special characters
- Limit filename length to 50 chars for filesystem compatibility
- Better dash-separated filename format like 'javascript-tutorial.mp3'
2025-07-03 19:32:53 -04:00
76bd27bea8 feat: generate smart filenames for TTS downloads using Hermes
- Use generateTitleForTTS to create dash-separated filenames based on textarea content
- Hermes analyzes the text and generates descriptive filenames like 'hello-world-tts.mp3'
- Fallback to default filename if generation fails
2025-07-03 19:31:52 -04:00
69fd142004 fix: TTS modal audio playback and download functionality
- Add missing blobUrl to speakText return value
- TTS modal now correctly receives blobUrl for audio playback and download
- Fixes missing MP3 download and autoplay functionality
2025-07-03 19:08:21 -04:00
f42ea86c7b fix: reduce console spam and improve audio error handling
- Limit translation.js console output to 100 chars for long text (was showing 22KB)
- Add content filtering to TTS to prevent inappropriate content processing
- Enhance audio error handling with abort, stalled, and specific error types
- Add user-friendly error messages for audio playback failures
2025-07-03 19:05:51 -04:00
ade7f4a35e feat: replace TTS modal audio embed with custom player controls
## Enhanced TTS Audio Experience:
- **Remove browser default audio controls** (was showing embed instead of custom player)
- **Add custom play/pause button** with emoji icons (⏸️ Pause / ▶️ Play / ▶️ Replay)
- **Add interactive progress bar** with click-to-seek functionality
- **Add time display** showing current time / total duration (e.g., "1:23 / 3:45")
- **Prominent download button** (💾 Download) for saving MP3 files
- **Auto-play with fallback** - attempts auto-play, gracefully handles browser restrictions

## UI Improvements:
- **Clean layout** with flexbox controls container
- **Visual progress indicator** with smooth transitions
- **Responsive design** supporting both custom and default styling modes
- **Proper spacing and styling** for professional appearance

## Functionality:
- **Click progress bar to seek** to any position in audio
- **Real-time progress updates** during playback
- **Proper state management** (play/pause/ended states)
- **Error handling** for audio playback issues
- **Accessibility** with proper button labels and time displays

## Technical Details:
- Hidden audio element (no browser controls)
- Custom event listeners for timeupdate, ended, error events
- Proper time formatting (MM:SS format)
- Progress calculation with percentage-based width updates
- Browser auto-play policy compliance with user interaction fallback

**Result: Professional audio player experience instead of basic browser embed\!** 🎵
2025-07-03 19:01:37 -04:00
03e402fdf9 feat: add comprehensive Jest test suite for all 3 modals with complete coverage
## Jest Configuration & Setup:
- Configure Jest with jsdom environment for DOM testing
- Set up test mocks for external dependencies (marked, hljs)
- Create test setup with localStorage, fetch, and dialog element mocks
- Add npm scripts for testing: test, test:watch, test:coverage, test:modals
- Update Makefile with Jest test targets and CI integration

## TTS Modal Tests (89 test cases):
- Modal creation, display, and structure validation
- Voice selection dropdown with 6+ voice options
- Speed control slider (0.5x-2.0x range)
- Audio generation with API integration testing
- Download functionality with blob handling
- Modal closing and cleanup
- Accessibility (ARIA, keyboard navigation, focus management)
- Error handling (API failures, malformed responses, network issues)

## Translate Modal Tests (85 test cases):
- Modal creation with source/target language selection
- Language detection and auto-population
- Translation API integration with streaming responses
- Copy functionality with clipboard API
- Text editing and retranslation
- Language swap functionality
- Modal state management
- Accessibility compliance
- Comprehensive error handling

## Embed Modal Tests (78 test cases):
- Main chat modal with conversation management
- Model loading and selection from endpoints
- Conversation history persistence with localStorage
- Real-time chat with streaming AI responses
- Message actions (copy, delete, TTS buttons)
- Action buttons with fallback modal opening
- Toggle functionality and state management
- User input handling (Enter key, send button)
- Accessibility and keyboard navigation
- Error handling for chat API, storage, and network issues

## Total: 252 comprehensive test cases covering:
- UI component creation and rendering
- User interaction and event handling
- API integration and streaming responses
- State management and persistence
- Accessibility compliance
- Error boundary testing
- Modal lifecycle management
- Cross-browser compatibility patterns

All tests include proper mocking, async handling, and cleanup for reliable CI execution.
2025-07-03 18:55:49 -04:00
c721b88473 fix: add fallback handling for window modal functions in embed modal
- Add safety checks for window.openTTSModal and window.openTranslateModal availability
- Implement fallback dynamic imports if window functions not available
- Prevents "window.openTTSModal is not a function" and "window.openTranslateModal is not a function" errors
- Provides graceful error handling with user feedback if modal imports fail
- Resolves loading order dependency issues between ui.js and embed modal

This ensures modal buttons work regardless of script loading order.
2025-07-03 18:51:48 -04:00
f3531d1591 fix: resolve localStorage Promise handling in modal dynamic import wrappers
- Fix loadConversationHistory() calls missing await (was returning Promise instead of data)
- Fix saveConversationHistory() calls missing await in 4 locations
- Fix clearConversationHistory() calls missing await
- Fix getChatHistory() calls missing await in 6 locations
- Fix updateChatHistory() calls missing await in 5 locations

All dynamic import wrapper functions are now properly awaited, resolving:
- "History length: undefined" error (Promise.length was undefined)
- "Raw history from localStorage: Promise { <state>: "pending" }" console output
- localStorage operations not completing before UI updates

This fixes the localStorage breakage caused by async wrapper functions not being awaited.
2025-07-03 18:44:59 -04:00
2315e476c8 feat: add comprehensive session error regression tests to prevent modal failures
Add 10 new regression tests in Section 10 targeting specific errors encountered:

🚨 Session Error Prevention:
- Async generator wrapper validation (prevents "not iterable" errors)
- Dynamic import wrapper error handling (prevents undefined function errors)
- Module import path verification (prevents wrong module imports)
- Variable scope isolation (prevents direct chatHistory access)
- Runtime function availability (prevents window.function undefined errors)
- Module export consistency (prevents import/export mismatches)
- Async/await pattern validation (prevents promise handling errors)
- Error boundary patterns (prevents cascading failures)
- Generator function delegation (prevents yield* delegation errors)
- Function signature compatibility (prevents parameter mismatches)

These tests specifically target the JavaScript errors we encountered:
- sendMessageWithCustomHistory(...) is not iterable
- fetchModelsFromEndpoints is not a function
- loadConversationHistory is not a function
- clearConversationHistory is not a function
- saveConversationHistory is not a function
- getSelectedModel is not a function
- chatHistory is not defined
- window.openTTSModal is not a function
- window.openTranslateModal is not a function

All tests validate current fixes and prevent regression of these specific patterns.
2025-07-03 18:32:03 -04:00
bfdb742d9e feat: add comprehensive modal error regression tests and fix saveConversationHistory import
- Add 8 new modal-specific integration tests covering all session errors:
  * Modal import function validation
  * Correct import path verification
  * Generator function wrapper correctness
  * Window global function exports
  * Safe chat history handling
  * Error handling patterns
  * Export/import matching
  * Circular dependency prevention
- Fix saveConversationHistory import from ./chat.js to ./storage.js
- Window functions openTTSModal/openTranslateModal already properly exported in ui.js
2025-07-03 18:27:11 -04:00
1d40a055b0 docs: update CLAUDE.md with Makefile and CI pipeline information 2025-07-03 18:22:43 -04:00
335dd937d8 fix: resolve remaining modal JavaScript errors
- Fix sendMessageWithCustomHistory wrapper to properly return generator with yield*
- Fix getSelectedModel import path from ./chat.js to ./models.js
2025-07-03 18:18:41 -04:00
7b9a37ebea fix: resolve JavaScript import errors in modal functionality
- Fix fetchModelsFromEndpoints import path from ./chat.js to ./models.js
- Fix loadConversationHistory and clearConversationHistory imports from ./chat.js to ./storage.js
- Replace direct chatHistory access with proper async getChatHistory/updateChatHistory calls
- Add sendMessageWithCustomHistory function to chat.js for modal integration
2025-07-03 18:14:44 -04:00
185d57092b fix: improve export validation to handle destructured exports
- Enhanced validate_exports.js to detect both named exports (export const/function/etc) and destructured exports (export { name1, name2 })
- Fixes validation error that was incorrectly reporting missing exports
- All CI checks now pass successfully

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 18:07:52 -04:00
ba5049ebfb fix: restore complete 2,153-line working modal implementation
- Replaced broken placeholder modal with complete working version from commit 3e01e9c
- Fixed all import paths to point to correct modules (chat.js instead of storage.js/models.js)
- Added proper export statements for openUncloseaiEmbeddedModal and openUncloseaiEmbeddedModalNew
- Restored full functionality including:
  * Settings panel with model/voice/language selection
  * TTS functionality for all messages
  * Copy buttons (raw markdown and HTML)
  * File upload capability
  * Complete chat history management
  * Theme support and dynamic theme updating
  * Intro message generation with page context
  * Conversation persistence

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 17:57:56 -04:00
bd0b0a5f97 fix: restore complete working chat modal implementation from commit 3e01e9c 2025-07-03 17:36:07 -04:00
5d584bf8f0 feat: add translation verification to CI pipeline using verify-translations.js 2025-07-03 17:27:06 -04:00
e502407ced feat: add comprehensive Makefile for development workflow and add module type to package.json 2025-07-03 17:17:21 -04:00
996069a9d9 fix: correct SUPPORTED_LANGUAGES import in translate-modal.js 2025-07-03 17:10:01 -04:00
aa62dacb8e fix: add missing handleTTS and downloadAudio exports to tts.js 2025-07-03 17:05:36 -04:00
2d4b52399c fix: add toggleUncloseaiEmbeddedModal + integration tests
- Add missing toggleUncloseaiEmbeddedModal function for floating button
- Create comprehensive integration test suite (15 tests)
- Tests verify all imports/exports work correctly
- Tests ensure proper module structure after refactoring
- All tests passing  - refactoring is solid!
2025-07-03 16:43:38 -04:00
86718974ac fix: add missing toggleUncloseaiEmbeddedModal function for floating button
- Add toggleUncloseaiEmbeddedModal function that was missing after refactoring
- Function properly toggles modal open/close state
- Export function globally for floating button onclick handler
- This fixes the floating button not appearing/working issue
2025-07-03 16:41:13 -04:00
76d0fb448a fix: correct import path from page-content.js to content.js
- Fix CORS error caused by incorrect import in translate-modal.js
- Change import from ./page-content.js to ./content.js
- Resolves module loading issues
2025-07-03 16:36:40 -04:00
e1a1da464d fix: remove duplicate initializeSystem function causing syntax error
- Remove duplicate initializeSystem function that was causing export at wrong level
- Fix syntax error: export declarations may only appear at top level
- Reduce ui.js to 532 lines (from 567)
2025-07-03 16:25:21 -04:00
e99db238b6 refactor: major ui.js modularization - extract 3000+ lines
- Extract massive modal functions to uncloseai-embed-modal.js (399 lines)
- Extract widget creation functions to widget-library.js (447 lines)
- Move addFileUploadButton to file-upload.js for better organization
- Reduce ui.js from 3,595 lines to 567 lines (84% reduction)
- Transform ui.js into clean coordinator as requested
- All functionality preserved with proper imports and exports

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 16:16:46 -04:00
f9bf7d001c refactor: extract modal functions to separate files
- Move openTTSModal to tts-modal.js (335 lines)
- Move openTranslateModal to translate-modal.js (540 lines)
- Reduce ui.js from 4,832 to 4,028 lines
- Add imports and global exports for onclick handlers
- Each modal is now self-contained with its own imports
- No functionality changes, just code organization
2025-07-03 15:20:50 -04:00
9659236bc2 fix: add theme-aware styling to modal textareas for blog sites
- Fix main chat input textarea in openUncloseaiEmbeddedModal (line 3837)
- Fix TTS modal textarea with conditional styling (line 4070)
- Fix translation modal textarea with conditional styling (line 4417)
- All textareas now use rgba glassmorphism when USE_CUSTOM_STYLING=false
- Should resolve white-on-white text issue in blog dark mode
2025-07-03 14:52:09 -04:00
3e01e9c713 complete: add 4 missing translation keys to all 18 languages
- Add askAnythingPlaceholder, askPagePlaceholder, enterTextToSpeakPlaceholder, processingTokens
- All 18 language files now complete with 67 translation keys each
- Verified with translation completeness script - all languages pass
- Update CLAUDE.md documentation to reflect new key count
- All UI placeholder text now fully internationalized
2025-07-03 14:28:13 -04:00
9c9c5ec1c3 fix: internationalize placeholder text in chat inputs
- Add askPagePlaceholder, askAnythingPlaceholder, enterTextToSpeakPlaceholder keys
- Replace hardcoded 'Ask about this page...', 'Ask anything...', 'Enter text to speak...'
- All input placeholders now use translation system
- Ensures consistent multilingual experience across all UI elements
2025-07-03 14:20:34 -04:00
d3e6b78166 fix: correct theme-agnostic styling approach for blog sites
- Fixed understanding: index.html uses PicoCSS (custom=true), blog uses fallback (custom=false)
- Replace CSS variables with rgba transparency and glassmorphism effects
- Semi-transparent backgrounds adapt to any light/dark theme
- Backdrop-filter blur creates modern glass effect that works universally
- No longer assumes CSS variables exist on blog sites
2025-07-03 14:18:21 -04:00
6e0ace6717 fix: improve dark theme compatibility for blog site (PicoCSS)
- Add USE_CUSTOM_STYLING checks to main chat interfaces
- Use PicoCSS variables (--background-color, --color, --border-color) when USE_CUSTOM_STYLING=false
- Fix white-on-white text issue in dark mode for blog embedding
- Chat input, textarea, and buttons now respect PicoCSS dark theme
- Maintains backward compatibility with index.html (custom styling)
2025-07-03 14:15:15 -04:00
22861ad4f9 fix: replace hardcoded UI text with translation keys
- Fix all remaining 'Read Page' hardcoded strings
- Replace 'Processing...' and 'Detecting...' with translation keys
- Update createReadFeature to use proper translation system
- Add processingTokens translation key for token count display
- UI should now properly internationalize all button text
2025-07-03 14:10:47 -04:00
95ea67228b complete: finalize all 18 language files with 63 translation keys
- Add missing 17 keys to final 8 languages (id, mr, sw, te, tr, ur, zh-tw)
- Fix translation verifier to exclude helper scripts
- Update CLAUDE.md with translation management documentation
- All languages now complete: ar, bn, de, en, es, fr, hi, id, ja, ko, mr, pt, ru, sw, te, tr, ur, zh, zh-tw
- Verified with translation completeness script - all 18 languages pass
2025-07-03 14:02:54 -04:00
48d8aa642f fix: complete translation modal internationalization
- Fix all hardcoded text in translation modal content
- Add translatePageInfo, translateToLabel, translateButton keys
- Fix dynamic button text for page/text modes
- Fix hardcoded Processing text during translation
- Fix Translation Modal button labels in UI
- Fix TTS Anything and Smart Translate button labels
- Add data-i18n attributes for dynamic refresh
- All modal content now fully localizes on language switch
2025-07-03 13:43:50 -04:00
63cb565dc5 fix: connect UI refresh functions for complete auto-refresh support 2025-07-03 13:31:11 -04:00
64b91d69de fix: resolve hardcoded text in modals and auto-refresh issues
- Fix TTS modal hardcoded 'Speed:' and 'Play Text' buttons
- Fix translation modal hardcoded tab text
- Fix system prompt to respect user's selected language
- Fix DOM auto-refresh by connecting both refresh functions
- Add TTS and system prompt translation keys to 10 languages
- Translation modal tabs and TTS controls now localize properly
- Language switching works without hard refresh for data-i18n elements
2025-07-03 13:27:53 -04:00
65504b2d19 docs: document translation system and update ui-translations module 2025-07-03 12:51:52 -04:00
b9ab0e83b3 feat(i18n): complete modular translation system with dynamic refresh
- Create individual language files for all 19 supported languages
- Add complete 46-key translations for Hindi, Spanish, French, Arabic, Bengali, Russian, Portuguese, Urdu, Indonesian, German, Japanese, Swahili, Marathi, Telugu, Turkish, Chinese Traditional, and Korean
- Implement dynamic UI refresh mechanism for real-time language switching
- Add translation verification script for consistency checks
- Update ui-translations.js to use new modular structure
- Document complete workflow for adding new languages in journal.rst

All languages now have 100% translation key coverage with automated verification
2025-07-03 12:49:03 -04:00
8783504389 fix: apply biome linting improvements
- Fix template literal usage (string concatenation to template literals)
- Apply optional chaining for safer property access
- Reduce biome warnings from 36 to 19
- Code is now cleaner and more maintainable
2025-07-03 12:22:46 -04:00
d3c5e31bde refactor: extract UI translations, themes, and language detection into separate modules
- Create ui-translations.js with UI_TRANSLATIONS object and translation functions
- Create ui-themes.js with theme detection and styling functions
- Create language-detection.js with AI-powered language detection
- Remove duplicate code from ui.js, reducing from 5600+ to 4741 lines
- Fix import assignments and syntax errors
- All functionality preserved while improving code organization
2025-07-03 12:12:30 -04:00
c263cb9d56 feat(ui): complete core translation infrastructure
- Update batman toolbelt action buttons to use getUIText()
- Translate all modal headers (AI Assistant, TTS, Translate)
- Convert all input placeholders to use translations
- Update critical alert messages with parameter substitution
- Add comprehensive translation keys for UI elements
- Translate Hermes intro message with proper HTML formatting
- Support both English and Chinese translations for all new keys
- Maintain fallback to English for missing translations
- Fix biome style issues (template literals, optional chains)
2025-07-03 11:41:02 -04:00
eee33986cf feat(ui): add comprehensive language localization system
- Add complete UI translations for all 19 supported languages
- Add language preference dropdown in modal settings
- Store language preference in localStorage
- Inject language preference into Hermes system prompts
- Add smart translation dropdown with AI-powered language detection
- Keep both original translation modal and new smart dropdown
- Remove non-functional upload button from modal
- Add biome.json config to ignore third-party CSS files
2025-07-03 11:09:10 -04:00
cae51b3578 feat(translation): add smart translate dropdown with native language names
- Add smart-translate widget feature with AI language detection
- Implement triple-format language display (code • English • Native)
- Add native script names with proper Unicode (中文, Español, العربية, etc.)
- Export handleSmartTranslate to global scope for HTML onclick
- Update demo.html to showcase both translate and smart-translate options
- Add comprehensive widget documentation with code examples

🤖 Generated with Hermes Staff

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-02 18:43:35 -04:00
6a4e9f9f72 feat(translate): add smart AI-powered translation dropdown
- Replace translation modal with intelligent dropdown interface
- Add AI-powered language detection using Hermes classifier
- Send page markdown sample to Hermes for accurate language detection
- Create themed dropdown with current language indicator
- Support all 18+ languages with quick selection
- Open translations in new tab automatically
- Add fallback language detection for reliability
- Show translation progress with loading states
- Auto-close dropdown on outside clicks
2025-07-02 18:18:40 -04:00
90842ebaac fix(ui): ensure batman toolbelt button text is visible in all themes
- Add !important to color property for control buttons
- Fix text visibility issue on mobile devices
- Apply same fix to download button
- Update theme change handler to use !important for dynamic updates
2025-07-02 17:21:34 -04:00
f8210bfe39 fix(hermes): use theme colors for bottom control buttons to ensure visibility 2025-07-02 16:20:07 -04:00
1b04cb5f15 feat(ui): add comprehensive dark/light mode support to uncloseai modal
- Add theme detection that checks data-theme attribute and prefers-color-scheme
- Create theme-aware color palette for both light and dark modes
- Update modal container, headers, panels, inputs, and buttons with theme colors
- Apply theme colors to user/AI message bubbles and action buttons
- Add dynamic theme change monitoring with MutationObserver and media queries
- Update historical messages to respect current theme on load
- Ensure proper cleanup of theme listeners when modal is closed
- Improve accessibility with proper contrast in both light and dark modes
2025-07-02 16:10:11 -04:00
72ccec472f fix: Read Page pause functionality and add download button 2025-07-02 15:32:54 -04:00
fa4eaa57da feat: rename batman toolbelt buttons and add Full Page Raw with HTML to markdown conversion 2025-07-02 15:24:16 -04:00
b1e9cfca9c docs: add biome workflow requirements to prevent syntax errors 2025-07-02 15:10:52 -04:00
b5defdde58 fix: import getChatHistory for copy buttons 2025-07-02 15:08:37 -04:00
36935f0956 modified: index.html 2025-07-02 14:59:22 -04:00
21e3bbab88 modified: index.html 2025-07-02 14:55:28 -04:00
ae6e3baa35 feat: add TTS buttons to user messages and fix delete persistence bug 2025-07-02 14:47:50 -04:00
ba76f1dbcd feat: move delete buttons inside user message bubbles 2025-07-02 14:43:47 -04:00
36289d6f5c feat: add copy buttons to code blocks in AI responses 2025-07-02 14:41:13 -04:00
35f84c4b57 fix: move copy buttons to main interface next to translate button 2025-07-02 13:41:25 -04:00
4acf2be811 feat: move copy buttons to main modal settings panel 2025-07-02 13:32:56 -04:00
9173c9bdc0 feat: replace ugly red X buttons with proper action buttons for user messages 2025-07-02 13:21:30 -04:00
5c2137a485 fix: repair syntax error and format all files with biome 2025-07-02 13:08:48 -04:00
7063e382a9 feat: add copy buttons and user message delete, remove confirmation dialogs 2025-07-02 13:05:09 -04:00
f55e7e3ac4 feat: add ChunkFive font with absolute path and uncloseai branding to all modals 2025-07-02 12:45:09 -04:00
087e828583 refactor: rename hermes modal to uncloseai embedded modal 2025-07-02 12:33:34 -04:00
05cfcf2b95 fix(hermes): remove upload button from modal 2025-07-02 12:16:25 -04:00
2048e25cd5 fix(hermes): ensure AI responses are saved to conversation history
- Manually add AI response to chatHistory after streaming completes
- Fix missing Hermes responses when reopening modal
- Update modal title to 'uncloseai.' with link to Nous Research Hermes
- Add comprehensive debugging for conversation history save/load
- Ensure proper history synchronization between modules
2025-07-02 10:11:51 -04:00
76e168cecf feat(hermes): restore localStorage for model and voice settings
- Save selected voice to localStorage and restore on modal open
- Improve model selection restoration from localStorage
- Save settings panel open/close state
- Add console logging for settings changes
- Maintain user preferences across sessions
2025-07-02 09:56:24 -04:00
de112973e6 fix(hermes): sync conversation history between ui.js and chat.js modules
- Fix follow-up Hermes responses not showing when modal reopens
- Properly sync chatHistory between ui.js and chat.js modules
- Use getChatHistory() and updateChatHistory() for consistent state
- Ensure AI responses are saved and loaded correctly
2025-07-02 09:54:33 -04:00
8d8ee38e96 debug(hermes): add logging to track conversation history display
- Add console logs to track which messages are being loaded and displayed
- Debug why Hermes messages might not be showing when modal reopens
- Log message details and chatBox append operations
- Help identify if issue is with loading, processing, or displaying messages
2025-07-02 09:38:57 -04:00
ecae7e9b4c fix(hermes): save intro message to history and prevent regeneration
- Only generate intro message when no conversation history exists
- Save generated intro to chatHistory so it persists across modal opens
- Set up page context when loading existing conversation history
- Prevent intro from being regenerated every time modal opens
- Intro message now properly saved and reused like other messages
2025-07-02 09:26:32 -04:00
4486cec23b fix(hermes): create custom sendMessageWithCustomHistory function
- Add sendMessageWithCustomHistory function to handle intro generation
- Fix TypeError: sendMessageWithHistory is not a function
- Custom function allows intro to use different system prompt than conversation
- Maintains streaming response for real-time intro generation
- Fixes intro message generation with proper API call handling
2025-07-02 09:05:18 -04:00
4110a8130b feat(hermes): implement dual system prompts for intro vs conversation
- Create specialized intro system prompt for 3-paragraph welcome generation
- Use separate conversation system prompt for follow-up questions
- Intro prompt: focused on generating structured welcome message
- Conversation prompt: optimized for answering questions about page content
- Use translation sendMessage for custom history in intro generation
- Maintain page context throughout conversation while separating concerns
2025-07-02 08:36:43 -04:00
712473f516 fix(hermes): set page context in system message for entire conversation
- Use setSystemMessageAppend to add page context to system message
- All follow-up questions now have full page context available
- Update existing system message in chatHistory with page content
- Hermes can now reference specific details (like Nathan) in follow-up questions
- Maintains coherent conversation with proper page context throughout
2025-07-02 08:34:51 -04:00
71a2c3d00d improve(hermes): enhance context setup for coherent conversations
- Provide full page content to Hermes instead of just preview
- Include complete page context in initial system message
- Tell Hermes to maintain context for future questions in conversation
- Enable Hermes to reference specific details, names, and topics from page
- Fix issue where follow-up questions lacked proper page context
- Should now be able to answer questions about Nathan and other page details
2025-07-02 08:33:35 -04:00
3cbcd64e92 fix(hermes): add TTS and delete buttons to contextual intro message
- Add play/pause TTS button to the generated intro message
- Include delete button for consistency with other AI messages
- Full pause/resume functionality for intro message TTS
- Intro message now has same interaction capabilities as chat responses
- Uses speakChatText for clean audio generation
2025-07-02 08:15:30 -04:00
e8bef319da feat(hermes): generate contextual intro message using LLM
- Use Hermes AI to generate page-specific welcome message
- Extract current page content and title for context
- Create 3-paragraph intro that acknowledges specific page content
- Show loading message while generating contextual intro
- Fallback to basic intro if generation fails
- Make intro relevant to what user is actually viewing
2025-07-02 08:08:46 -04:00
051f589c4e fix(hermes): properly sync chat history with imported module
- Import chatHistory and sendMessage at top level instead of dynamic imports
- Use imported chatHistory directly in loadHistory and clearChat functions
- Ensure Hermes has proper conversation context when resuming chats
- Fix issue where Hermes responses weren't loading from conversation history
- Add debug logging to verify chat history synchronization
2025-07-02 08:03:01 -04:00
d8d4b2a47b feat(tts): add pause/resume functionality to chat TTS buttons
- TTS buttons now toggle between play (🔊), pause (⏸️), and loading () states
- Click once to generate and play audio
- Click again to pause playing audio
- Click third time to resume paused audio
- Audio automatically resets icon to 🔊 when playback ends
- Added to both new message and historical message TTS buttons
2025-07-02 08:01:00 -04:00
bab44f680b refactor(tts): create dedicated speakChatText function for chat messages
- Add speakChatText function that skips extractSpokenTokens preprocessing
- Chat messages are already clean text that don't need markdown/HTML cleanup
- Simplify chat TTS button handlers to use clean direct TTS approach
- Separate chat TTS from webpage TTS which needs content preprocessing
- Much faster and more reliable for chat conversations
2025-07-02 07:58:24 -04:00
59aacd7954 fix(tts): bypass extractSpokenTokens for chat TTS buttons
- Use direct TTS API calls for chat and historical message TTS buttons
- Skip the extractSpokenTokens preprocessing that was causing failures
- Import TTS_API_URL and API_KEY constants for direct API access
- Maintain same audio result format for compatibility
- Fix chat TTS buttons not responding due to extractSpokenTokens hanging
2025-07-02 07:55:16 -04:00
a59b7a2395 fix(hermes): properly sync chat history between modal and chat module
- Sync loaded conversation history with chat.js module on modal open
- Clear chat history in both localStorage and chat.js module when clearing
- Make loadHistory async to properly await chat module import
- Ensure conversation continuity across page refreshes and modal sessions
- Add comprehensive logging for history synchronization
2025-07-02 07:42:54 -04:00
d996a1351b enhance(tts): improve auto-play handling with browser policy support
- Add proper error handling for browser auto-play restrictions
- Set button text correctly based on auto-play success/failure
- Add console logging for debugging auto-play behavior
- Gracefully handle cases where auto-play is blocked
2025-07-02 07:36:04 -04:00
831f91e585 fix(hermes): resolve const actions redeclaration error
- Rename second actions array to controlActions to avoid variable conflict
- Fix SyntaxError: redeclaration of const actions
2025-07-02 07:30:46 -04:00
a8ef25ef6c fix(hermes): persist conversation history across modal sessions
- Save chat history to localStorage after each AI response
- Load complete conversation history with TTS buttons on modal open
- Add TTS and delete buttons to historical AI messages
- Ensure conversation continuity when reopening modal
- Add debugging logs for history save operations
2025-07-02 07:29:04 -04:00
8651aaa54a fix(hermes): make TTS button responsive on AI messages
- Add ID to voice select element for reliable access
- Fix TTS button click handler to find voice selection properly
- Add debugging logs for voice selection
- Use document.getElementById instead of variable scope
2025-07-02 07:24:28 -04:00
af580c67ac feat(hermes): restore page context in modal header
- Add subtitle showing uncloseai branding and current page title
- Display 'You are discussing: [page title]' context
- Maintain mobile-responsive header layout with title container
2025-07-02 07:22:00 -04:00
2a8e020f9c fix(hermes): remove duplicate action buttons 2025-07-02 07:20:16 -04:00
f7241e0d0d fix(hermes): complete mobile-first modal with all features
- Fix sendMessage iteration error by importing from chat module
- Add missing imports for storage functions (loadConversationHistory, clearConversationHistory)
- Fix function calls to use imported modules instead of window globals
- Include all features: model selection, voice selection, file upload, page reading, translation, conversation history, clear chat, TTS for responses
- Maintain collapsible settings panel for mobile optimization
2025-07-02 07:14:05 -04:00
532de5519f fix: template literal syntax errors in hermes modal 2025-07-02 07:03:14 -04:00
3b2f1acf86 feat: complete mobile-first hermes modal redesign
- Full screen on mobile, centered modal on desktop
- Simple flex layout instead of complex grid
- Modern chat bubble design
- Auto-resizing input with proper mobile keyboard handling
- Minimal UI - only essential controls
- Better touch targets and mobile interactions
2025-07-02 07:01:17 -04:00
e972cbfd0a wip: start mobile-first hermes modal redesign 2025-07-02 06:51:16 -04:00
24c696638c fix(hermes): prevent modal overflow, only chat area scrolls 2025-07-01 22:25:34 -04:00
06ce02bc29 fix(hermes): remove max-width constraint from hermes modal
- Remove max-width: 800px that was forcing modal to be too wide
- Let modal use only 95vw to scale properly with viewport
- Should prevent overflow on smaller screens
2025-07-01 22:19:29 -04:00
42c3246684 fix(mobile): prevent modal overflow issues
- Add box-sizing: border-box to Hermes modal and content to prevent width overflow
- Add flex-wrap to TTS voice selection to prevent horizontal overflow
- Ensure all modal content respects 95vw width constraints
- Fix layout issues on mobile devices
2025-07-01 22:12:39 -04:00
ec9853be45 fix(ui): remove unwanted model dropdowns and standardize hermes modal
- Remove automatic model dropdown creation that was breaking blog layout
- Update Hermes modal to use 95vw sizing like other modals
- Add showModal() and backdrop click handling for consistency
- Use transform-based centering for proper mobile positioning
- All modals now have consistent sizing and behavior
2025-07-01 22:04:21 -04:00
525a4317b8 fix(mobile): simplify modal sizing with 95% viewport usage
- Use 95vw x 95vh for both TTS and translate modals
- Remove complex mobile/desktop detection logic
- Keep elegant styling with borders and shadows
- Minimize whitespace while maintaining good design
- Works consistently across all screen sizes
2025-07-01 21:58:19 -04:00
ef9919ce84 feat(mobile): full-screen modals on mobile devices
- Make TTS and translate modals take full viewport on mobile (≤768px)
- Remove borders, shadows, and border-radius for native app experience
- Keep centered modal design with shadows on desktop (>768px)
- Improve mobile usability with maximum screen real estate
2025-07-01 21:49:53 -04:00
bdd33652a3 fix(ui): restore natural modal sizing while keeping mobile positioning
- Remove forced width/height constraints that broke modal appearance
- Keep transform-based centering to fix mobile viewport issues
- Let content determine modal size naturally for better visual design
- Preserve original styling while maintaining mobile compatibility
2025-07-01 21:44:46 -04:00
88644403b6 fix(mobile): fix modal positioning and mobile support
- Replace margin: auto with explicit centering using transform: translate(-50%, -50%)
- Use showModal() instead of modal.open for proper mobile behavior
- Add responsive sizing with 90vw width and proper max-width constraints
- Add backdrop click handlers for modal closing
- Add touch-action: manipulation for better touch responsiveness
- Fix both TTS and translation modals disappearing on small viewports

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-01 21:38:57 -04:00
caaa347b35 fix(preview): separate button creation from initialization for preview windows
- Separate DOM creation from event handler setup in floating button
- Add self-contained click handler with multiple fallback methods
- Enable floating button in preview windows with UNCLOSEAI_SKIP_INIT flag
- Improve modal function detection with dynamic imports
- Add extensive debugging for button click issues

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-01 21:10:37 -04:00
5a05201fd9 fix(ui): correct import path for estimateProcessingTime function
Import estimateProcessingTime from translation.js instead of token_estimator.js
where it's actually defined
2025-07-01 20:46:24 -04:00
fc22c4a60d fix(translation): wait for preview document.body before adding notice
Prevents 'previewWindow.document.body is null' error by polling
until body is available before appending the AI functionality notice
2025-07-01 20:42:23 -04:00
9e6bbe4b19 refactor: rename tokenizer to token_estimator for clarity
- Rename tokenizer.js to token_estimator.js to reflect its purpose
- Update import in translation.js
- Add development journal documenting NVIDIA 4090 performance metrics
2025-07-01 20:31:07 -04:00
2845bb16d8 feat(tokenizer): add BPE tokenizer with real vLLM performance metrics
- Create simplified BPE tokenizer based on tiktoken core logic
- Use real production metrics: 3,144 tokens/s prompt, 121 tokens/s generation
- Add accurate processing time estimates for translation ETA
2025-07-01 20:26:23 -04:00
796887fb38 fix(translation): open preview in new tab with full browser controls
Remove popup window restrictions to provide better user experience
with address bar, navigation buttons, and other browser features
2025-07-01 20:00:05 -04:00
9df24f7935 fix(ui): make floating button functional in preview windows
Move global scope exports before skip init check so toggleHermesModal
and other functions are available for button click handlers
2025-07-01 19:57:58 -04:00
02fff63f73 fix(translation): improve error handling and preview functionality
- Add HTTP error checking for 404 endpoint issues
- Ensure models are loaded before translation attempts
- Add response validation with clear error messages
- Fix floating button DOM timing in preview windows
2025-07-01 19:56:26 -04:00
2e8fa6e30b fix(translation): resolve preview issues and endpoint errors
- Remove modal/dialog DOM from translation preview content
- Fix 404 errors by properly checking model registry for endpoints
- Enable floating button in preview windows with partial initialization
2025-07-01 19:44:12 -04:00
7854715ecb fix(translation): implement skip init flag to prevent duplicate JS executions 2025-07-01 19:30:29 -04:00
17e3883bff fix(translation): correct syntax and logic in translation modal
- Reverted the broken openTranslateModal function to a working state.
- Re-implemented the logic to correctly inject the <base> tag for relative paths and to ensure the translation notice is displayed correctly.
- Ran biome to format the code and ensure there are no syntax errors.
2025-07-01 18:23:02 -04:00
206731c589 fix(translation): restore nested placeholders correctly
- The placeholder restoration logic now processes placeholders in reverse order, ensuring that nested content (like links within HTML tags) is restored correctly.
2025-07-01 17:53:10 -04:00
c3b5c94497 fix(translation): ensure CSS and JS work in preview
- The translation preview now includes the full HTML document, ensuring that stylesheets, scripts, and other resources are loaded correctly.
- A <base> tag is injected into the preview to handle relative resource paths.
- Removed a script flag that was incorrectly disabling JavaScript in the translated page, which fixes the issue with dialog boxes not appearing.
2025-07-01 17:49:33 -04:00
7aeba010d6 modified: src/ui.js 2025-07-01 17:31:56 -04:00
fb38570227 modified: src/ui.js 2025-07-01 17:29:27 -04:00
9a71e71873 Add DOCTYPE html and inject uncloseai.js script into preview window 2025-07-01 17:03:50 -04:00
c78a6a231f Auto-open preview window immediately when HTML translation completes 2025-07-01 16:56:48 -04:00
b1ea03fe21 Enhance popup preview with base URL and AI functionality support 2025-07-01 16:51:45 -04:00
4fa252f73d Replace iframe with popup window for HTML translation preview 2025-07-01 16:48:40 -04:00
fad29c828b Add HTML preview with iframe rendering for translations 2025-07-01 16:39:57 -04:00
5d0f299b39 Increase translation limits: 70k max_tokens and 50k content length 2025-07-01 16:33:43 -04:00
6e4770c07e Fix script tag preservation and increase translation content limit 2025-07-01 15:40:39 -04:00
bd09659d3b Implement minimal translation system prompt following Russell's approach 2025-07-01 15:32:12 -04:00
12662d67c9 Add: Translation as standalone feature and in 'full' feature set
- Add createTranslateFeature() function for standalone translate button
- Include 'translate' in custom interface feature switch
- Add translate-only demo section with live example
- Update documentation to show translate in available features
- Translation now works as data-features='translate' or part of 'full'
- Update table of contents and section numbering
2025-07-01 13:09:40 -04:00
7386dd0032 Fix: Proper code block preservation to prevent comment translation
- Track placeholder types and content separately
- Use exact placeholder matching in restoration
- Strengthen prompt with CRITICAL warning about placeholders
- Prevent JavaScript comments from being translated inside code blocks
- Ensure code blocks remain completely unchanged during translation
2025-07-01 12:54:00 -04:00
af50d0823f Update: Use real Hermes-supported language list and Russell's placeholder technique
- Update to 19 languages actually supported by Hermes 3 model
- Match Russell's placeholder naming: __CODE_X__, __URI_X__, __HTML_X__
- Use exact preservation technique from translate_content.py
- Add Bengali, Urdu, Indonesian, Swahili, Marathi, Telugu
- Include Traditional Chinese (zh-tw) support
- Match Team Hermes implementation approach
2025-07-01 12:42:55 -04:00
0fc73e4e33 Add: Omni-directional translation feature with code block preservation
- Add translation.js with 20+ language support
- Implement code block, URL, and HTML tag preservation during translation
- Add translation modal with page translation and custom text modes
- Integrate translation button in all interfaces
- Add translation demo section with live examples
- Support Russell's approach of preserving technical content during translation
- Enable translation of current page content or custom text
2025-07-01 12:35:09 -04:00
061a04f1fe Improve: TTS buttons auto-play with proper state transitions (processing -> pause -> play) 2025-07-01 12:17:27 -04:00
340241c886 Fix: Filter system messages when loading from localStorage to handle legacy data 2025-07-01 10:40:58 -04:00
f13fc85d21 Fix: Remove system message display from file upload functionality 2025-07-01 10:27:52 -04:00
b9d52dc727 Fix: Remove await from non-async context that broke floating button 2025-07-01 10:15:36 -04:00
7e5fa104ca Fix: Hermes modal conversation state and add system message customization
- Fix modal conversation persistence between sessions
- Fix system messages appearing in DOM by filtering display
- Add configurable system message with setSystemMessageAppend()
- Reorganize demo.html with table of contents and code-example pattern
- Add comprehensive documentation for system message customization
- Make all code blocks open by default in demo page
2025-07-01 10:07:12 -04:00
d327d98f77 feat: Restore TTS Anything modal with speed slider and voice selection 2025-06-30 22:17:17 -04:00
d9185376e7 Improve: Add loading state to floating button and optimize modal opening speed
- Show 'loading...' text in floating button while modal opens
- Display modal immediately with loading placeholders
- Load models and Hermes intro asynchronously after modal appears
- Prevent blocking UI during slow network operations
2025-06-30 21:34:46 -04:00
57e29252cb Fix: Move fetchModelsFromEndpoints import to top level to prevent initialization error 2025-06-30 21:12:29 -04:00
ebb316fcf1 Fix: Add Hermes introduction and ensure modal dropdown is populated
- Add dynamic Hermes introduction generation when modal opens
- Fetch models directly in modal instead of copying from existing dropdown
- Fix empty dropdown issue by ensuring models are loaded before display
- Add page-specific intro caching and fallback handling
2025-06-30 21:10:16 -04:00
f455f17fa3 Fix: Make modal functions async to support dynamic imports 2025-06-30 20:57:50 -04:00
195cb3f864 Fix: Resolve model selection and UI issues in modular version
- Fix model dropdown to return correct model names instead of full unique IDs
- Replace problematic DOM traversal in TTS buttons with proper event handlers
- Prevent model dropdown creation on pages without chat interfaces
- Restore full-featured Hermes modal from original implementation
- Add comprehensive modal with model/voice selection, action buttons, and chat interface
- Include conversation history management and message deletion functionality
2025-06-30 20:53:28 -04:00
01df4d1e77 new file: src/chat.js
new file:   src/config.js
	new file:   src/content.js
	new file:   src/file-upload.js
	new file:   src/models.js
	new file:   src/page-reader.js
	new file:   src/storage.js
	new file:   src/tts.js
	new file:   src/ui.js
	modified:   uncloseai.js
	new file:   uncloseai.js.orig
2025-06-30 20:28:42 -04:00
2fed53f74b Enhance demo page with comprehensive isolated examples and integration snippets 2025-06-30 20:07:14 -04:00
7a6996bc30 Fix: Expose uncloseai.js functions to global scope
Exposed , , , , , , , , , and  functions to the global  object. This resolves  issues when these functions are called directly from HTML  attributes, particularly in .
2025-06-30 19:03:22 -04:00
a5fd6902ba Docs: Document UNCLOSEAI_FLOATING_BUTTON in index.html and demo.html
Updated index.html to correctly show the script tag for disabling the floating button. Also added the same documentation to demo.html for consistency.
2025-06-30 18:42:15 -04:00
f8a146f241 Feat: Implement per-page conversation history and ensure per-page Hermes intro caching.
This commit introduces a  function to generate unique localStorage keys based on the current page's URL. This key is now used for storing and retrieving conversation history, ensuring that each page maintains its own isolated chat thread. The Hermes introduction already uses a content-based hash for per-page caching, and this change aligns the conversation history with that behavior. Additionally, clearing the conversation history now only affects the current page and correctly repopulates the Hermes introduction for that page.
2025-06-30 18:28:24 -04:00
880c9370c0 Feat: Repopulate Hermes initial message on clear 2025-06-30 18:13:52 -04:00
50a6d39363 fix: ensure chat message delete buttons are not circular in purecss mode 2025-06-30 17:52:52 -04:00
941a5fe648 fix: remove duplicate initializeUncloseaiElements function 2025-06-30 17:30:29 -04:00
cbf48e370d feat: add debug logging to uncloseai.js 2025-06-30 17:27:32 -04:00
8f256b19fa fix: move event listener to prevent script crash 2025-06-30 17:24:31 -04:00
8c5c71eb02 fix: ensure floating button appears on all pages 2025-06-30 17:21:11 -04:00
c30194613f Reapply "fix floating button by removing duplicate check before creation"
This reverts commit e7a3371826.
2025-06-30 17:15:40 -04:00
e7a3371826 Revert "fix floating button by removing duplicate check before creation"
This reverts commit 37722fe392.
2025-06-30 17:14:52 -04:00
9a56d7f0fa fix: re-enable floating button 2025-06-30 17:10:34 -04:00
37722fe392 fix floating button by removing duplicate check before creation 2025-06-30 16:55:05 -04:00
0b573a2a86 implement class-based initialization system and DRY refactoring for uncloseai.js
- Add .uncloseai class support with data-features configuration
- Create shared utility functions for buttons, chat messages, TTS controls, voice selects, and modal headers
- Refactor class-based feature creators to use shared utilities
- Add comprehensive documentation for class-based integration in index.html
- Add live examples of class-based usage in demo.html
- Maintain backward compatibility with existing floating button and API functions
2025-06-30 16:50:16 -04:00
db57965ef4 fix unicode handling in content hash generation for hermes introduction caching 2025-06-30 15:54:53 -04:00
fac46e5afa make modal use full viewport height and react to onscreen keyboard changes 2025-06-30 15:50:02 -04:00
6c0f3df6ef improve close button styling for PureCSS with proper theming and hover effects 2025-06-30 15:43:23 -04:00
b2a63f44f4 add auto-scroll to bottom for all chat messages and streaming responses 2025-06-30 15:26:01 -04:00
6e84ee58b1 fix TTS button text, progress indicator z-index, and hermes introduction loading feedback 2025-06-30 15:14:44 -04:00
c318e68b5a fix hermes introduction to add to chat history and update system prompt with hermes identity 2025-06-30 14:55:51 -04:00
d6b28dc126 remove max-width/height constraints from modal and reduce button min-width for mobile responsiveness 2025-06-30 14:35:52 -04:00
9340c410ff align labels and dropdowns horizontally using grid layout 2025-06-30 13:21:15 -04:00
79d8c590fd convert modal layout from flexbox to CSS grid 2025-06-30 12:01:42 -04:00
0cc64921cf override min-width constraints to fix modal hiding on narrow mobile screens 2025-06-30 11:51:24 -04:00
08eae89f5b add webpage title under hermes chat and move action buttons above model selection 2025-06-30 11:44:09 -04:00
48d4261a54 make modal fullscreen on mobile - 100% width and height 2025-06-30 11:41:21 -04:00
53693f5ff0 fix voice label styling to respect USE_CUSTOM_STYLING flag 2025-06-30 09:19:25 -04:00
4cdbcbcbc3 Fix TTS modal z-index and scale down modal for PureCSS
- Fixed TTS modal z-index detection after dialog conversion (hermes-modal vs hermes-modal-backdrop)
- Added 60% scale transform to modal when custom styling disabled for better fit
- Reduced chat area minimum heights (300px->200px, 250px->150px) for more compact layout
- Modal now appears properly sized for PureCSS framework integration
2025-06-29 21:51:55 -04:00
d21913611b Convert Hermes modal to dialog element and reduce CSS in JS
- Changed Hermes modal from div+backdrop to proper dialog element like TTS modal
- Removed inline CSS when custom styling disabled, using semantic classes instead
- Added uncloseai-controls and uncloseai-button-group classes for framework styling
- Modal now properly inherits CSS framework styling without hardcoded backgrounds
- Cleaner separation between custom styling and framework integration
2025-06-29 21:36:02 -04:00
407713d1b7 Improve floating button size and fix modal layout/theming
- Increased floating button width from 80px to 120px for better proportions
- Added proper minimum heights to chat areas for PureCSS sites
- Removed hardcoded white background from modal to respect dark theme
- Improved spacing and padding for framework consistency
2025-06-29 21:12:36 -04:00
b78bc6ab2f Fix Hermes modal to respect styling configuration
Applied conditional styling to Hermes modal header, container, title, close button, and send button. Modal now properly inherits framework styling when UNCLOSEAI_CUSTOM_STYLING is false, removing gradients and custom fonts for clean PicoCSS integration.
2025-06-29 19:32:13 -04:00
459291addf Add optional styling system and disable custom styling for this repo
- Added USE_CUSTOM_STYLING configuration flag to uncloseai.js
- TTS modal now uses clean HTML elements when custom styling disabled
- Updated index.html and demo.html to disable custom styling
- This repo will now use clean PicoCSS styling instead of custom gradients
- Other sites can still enable custom styling by setting window.UNCLOSEAI_CUSTOM_STYLING = true
2025-06-29 19:22:06 -04:00
888910c8b4 Consolidate and improve TTS modal, add theme-aware floating button
- Consolidated duplicate TTS modals into single function with smart z-index
- Updated floating button to be theme-aware (black/white based on dark mode)
- Added toggle functionality to floating button (click to open/close modal)
- Fixed voice controls layout with proper line breaks
- Redesigned TTS modal with consistent styling matching site theme
2025-06-29 19:02:35 -04:00
7698a63362 Replace popup menu with full Hermes chat modal
Replaced simple skills popup with comprehensive modal interface featuring chat area, model/voice selection, action buttons (Read Page, TTS, Refresh Models), file upload, and TTS playback for responses. Modal uses ChunkFive branding and gradient styling.
2025-06-29 18:46:33 -04:00
af67a7be99 Add floating AI button with skills menu
Added always-on floating 'uncloseai.' button in bottom right corner that provides quick access to all AI skills: Chat, Read Page, TTS Anything, Upload File, and Refresh Models. Button uses ChunkFive font and gradient styling with smooth animations.
2025-06-29 18:36:44 -04:00
84a90f4dc2 AI responses now use main page voice selection
new file:   CLAUDE.md
	modified:   uncloseai.js
2025-06-29 18:11:05 -04:00
9ce4e261e0 disable analytics.unturf.com 2025-06-01 09:08:30 -04:00
6f2be4436b Merge branch 'Groupr-master-patch-68456' into 'master'
Update uncloseai.js Removed naptha

See merge request engineering/unturf/ai.unturf.com!2
2025-05-24 15:52:18 +00:00
6d4857503a Update index.html 2025-05-23 19:00:24 +00:00
c1fcb56aa6 Update uncloseai.js Removed naptha 2025-05-23 14:42:20 +00:00
faea5e1628 Update uncloseai.js 2025-04-22 11:24:11 +00:00
ce5f6adf3e redundant hermes 2025-04-21 21:47:24 +00:00
0bf99fbba8 redundant hermes on 4090 2025-04-21 20:07:25 +00:00
90b7c3fcdd Update uncloseai.js 2025-04-21 19:55:44 +00:00
4f81c0c3e2 Update uncloseai.js 2025-03-29 13:06:12 +00:00
efcb776f7c Update uncloseai.js 2025-03-16 01:32:09 +00:00
0414ed3ad8 Update uncloseai.js 2025-02-28 13:37:07 +00:00
f143d4180c Update uncloseai.js 2025-02-25 12:13:57 +00:00
a7ecdcb7e2 Update uncloseai.js 2025-02-19 14:35:50 +00:00
a22b4ddeaf Dynamic model chooser!
modified:   uncloseai.js
2025-02-19 07:59:23 -05:00
361 changed files with 74889 additions and 1310 deletions

14
.gitignore vendored
View file

@ -1,2 +1,16 @@
js/dist/*
js/node_modules/*
# Node.js dependencies
node_modules/
webwords/
book/
*.log
# Generated audio files in language examples
public/languages/**/*.mp3
*.mp3
# Bundled extension build artifact
public/uncloseai-bundle.js

View file

@ -1,13 +1,16 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
stages:
- deploy
ai.unturf.com:
deploy:
stage: deploy
tags:
- ai.unturf.com
- uncloseai.com
only:
- master # Change this if you want to deploy from other branches
- master
script:
- echo "Deploying repository to /opt/www using rsync..."
- rsync -av --delete "$CI_PROJECT_DIR/" "/opt/www/"
- echo "Deployment to /opt/www completed successfully."
- mkdir -p /opt/www/uncloseai
- rsync -av --delete "$CI_PROJECT_DIR/" "/opt/www/uncloseai/"

674
CLAUDE.md Normal file
View file

@ -0,0 +1,674 @@
# Claude Memory
- All implementations must act as reusable libraries/clients, not just demo scripts
- Add streaming support for chat completions (SSE - Server-Sent Events)
- Provide clean API surface for developers to integrate into their applications
- Target compatibility with vLLM, Ollama, and OpenAI-compatible endpoints
- Maintain backward compatibility with non-streaming usage
**ALL 47 IMPLEMENTATIONS COMPLETE** - All have streaming support with SSE parsing
**Completed Languages (47/47 = 100%):**
- **AWK** - Functional library, curl --no-buffer, SSE line parsing
- **Bash** - Library functions, curl --no-buffer, regex SSE matching
- **C (3 variants):** curl (libcurl), libsoup (GNOME), nghttp2 (HTTP/2)
- **C++ (3 variants):** libcurl (std::function), cpp-httplib (lambda), boost-beast (class)
- **C#** - UncloseAI class, HttpClient, ResponseHeadersRead streaming
- **Clojure** - defrecord, lazy sequences, line-seq SSE parsing
- **COBOL** - Procedural PERFORM, shell curl streaming
- **Crystal** - Class-based, body_io.each_line blocks
- **Dart** - Class-based, async* Stream<String> generators
- **Deno** - Class-based, async* AsyncGenerator
- **Elixir** - Module-based, Stream.resource lazy streaming
- **Erlang** - Record-based, actor model process messaging
- **F#** - UncloseAIClient, seq {} StreamReader
- **Fortran** - Module-based, shell curl+jq+bash
- **Go** - Struct-based, channel streaming, context support
- **Haskell** - Data type, Conduit monadic composition
- **Java** - UncloseAI class, BufferedReader SSE parsing
- **JavaScript (4 variants):** nodejs (https), typescript (https+types), bun (Fetch API), vanilla (browser Fetch)
- **Julia** - Mutable struct, Channel async iteration
- **Kotlin** - UncloseAI class, callback streaming
- **Lua** - Metatable SDK, LuaSocket SSL, manual HTTP/SSE
- **Nim** - Ref object, bodyStream.lines callbacks
- **OCaml** - Record-based, Lwt promises, Lwt_stream
- **Odin** - Struct-based, shell curl
- **Perl** - LWP::UserAgent, streaming callback
- **PHP** - UncloseAI class, CURLOPT_WRITEFUNCTION
- **PowerShell** - HttpClient, StreamReader
- **Prolog** - SWI-Prolog http_client, simplified streaming
- **Python (4 variants):** requests (UncloseAI class), openai-client (OpenAI SDK), httpx-async (async class), aiohttp (async class)
- **R** - R6 class, httr write_stream
- **Ruby** - UncloseAI class, Net::HTTP read_body blocks
- **Rust** - Struct-based, Tokio async, reqwest StreamExt
- **Scala** - STTP client, callback streaming
- **Tcl** - TclOO class, curl pipe streaming
- **V** - Native http module, callback streaming
- **VB.NET** - Action(Of String) callbacks
- **Zig** - ChatStream, iterator pattern
**Remaining: 0 implementations**
**Implementation Pattern Established:**
- Client/class-based architecture (struct for compiled languages, class for dynamic)
- Model discovery from environment variables (MODEL_ENDPOINT_1..9999)
- Non-streaming method: `chat()` / `Chat()`
- Streaming method: `chat_stream()` / `ChatStream()` / `chatStream()`
- TTS generation: `tts()` / `TTS()` / `generateSpeech()`
- Error handling with typed errors where applicable
- SSE parsing: `data: {...}\n\n` format, `data: [DONE]` termination
**CRITICAL: File Naming for Phase 2 SDKs**
- ❌ NEVER create separate library files like `uncloseai_lib.py`, `uncloseai_lib.js`, etc.
- ✅ ALWAYS transform our existing `uncloseai.{ext}` file in place
- ✅ Keep single file: `uncloseai.py`, `uncloseai.js`, `uncloseai.ts`, `uncloseai.rs`, etc.
- Our file should contain both our SDK class/struct AND example usage in main()
- Example: Python's `uncloseai.py` contains `class UncloseAI:` + `if __name__ == "__main__":` demo
- Example: Go's `uncloseai.go` contains `type UncloseAI struct` + `func main()` demo
**Completed (47 implementations across 40 languages):**
- **Python (4 variants):** requests, openai-client, httpx-async, aiohttp
- **JavaScript (4 variants):** nodejs, typescript, bun, vanilla
- **C (3 variants):** curl, libsoup (GNOME), nghttp2 (HTTP/2)
- **C++ (3 variants):** libcurl, cpp-httplib, boost-beast
- **Single implementations (33 languages):** AWK, Bash, Clojure, COBOL, Crystal, C#, Dart, Deno, Elixir, Erlang, Fortran, F#, Go, Haskell, Java, Julia, Kotlin, Lua, Nim, OCaml, Odin, Perl, PHP, PowerShell, Prolog, R, Ruby, Rust, Scala, Tcl, V, VB.NET, Zig
**Refactoring Status (2025-10-13 - COMPLETE!):**
- ✅ **ALL 47 IMPLEMENTATIONS REFACTORED!** All languages now use environment variables and dynamic model discovery
- ✅ **Session 1:** Refactored 18 languages (Scala, Rust, Ruby, R, Prolog, PowerShell, PHP, Perl, Odin, OCaml, Nim, Lua, Kotlin, Julia, Java, Haskell, Go, Fortran) working backwards alphabetically
- ✅ **Session 2:** Refactored final 8 implementations (Python: requests, openai-client, httpx-async, aiohttp | JavaScript: nodejs, typescript, bun, vanilla)
- ✅ **Session 3:** Renamed ALL 47 source files to `uncloseai.{ext}` (or `UncloseAI.*` for capitalized languages)
- ✅ **Pattern Applied:** All use `System.getenv()`/`os.getenv()`/`ENV`/`process.env` for `MODEL_ENDPOINT_1..9999` and `TTS_ENDPOINT_1..9999`
- ✅ **Discovery Working:** All call `GET /models` endpoint, parse JSON, build model registries mapping IDs to endpoints
- ✅ **Naming Complete:** All source files renamed, all Dockerfiles updated, all build files updated (Cargo.toml, build.sbt, *.vbproj, etc.)
- ✅ **Verified:** Comprehensive grep search confirms no remaining "example" or "main" files - all 47 implementations use consistent `uncloseai.*` naming
**Session 2025-10-13 Final 8 Implementations:**
- **Python variants (4):** requests, openai-client, httpx-async, aiohttp
- All use `os.getenv(f"MODEL_ENDPOINT_{i}")` loop pattern
- requests: Direct HTTP with requests.get/post
- openai-client: Uses OpenAI SDK with dynamic base_url
- httpx-async: Async with httpx.AsyncClient
- aiohttp: Async with aiohttp.ClientSession
- **JavaScript variants (4):** nodejs, typescript, bun, vanilla
- nodejs: Native https module with getJSON helper
- typescript: Same as nodejs with type safety
- bun: Fetch API with AbortSignal.timeout
- vanilla: Browser-based with CONFIG.MODEL_ENDPOINTS (can't use env vars)
**Skipped (cannot implement - 5 languages):**
- Matlab (proprietary license prevents Docker usage)
- SQL (declarative query language, no HTTP client)
- Swift (requires macOS/Xcode for proper development)
- Brainfuck (esoteric language, no practical HTTP client)
- Assembly (too low-level, no standard HTTP library)
**Empty directories (skipped, listed above):**
- assembly/, brainfuck/, matlab/, sql/, swift/ - all empty, marked as skipped
## Project Identity
- When working on this ai.unturf.com project, refer to yourself Claude as "Hermes Staff"
- This project uses our Claude, Hermes, & Qwen machine learning language models and you are part of our team.
- "Hermes Staff" is also a reference to our Staff of Hermes (caduceus) - our symbol of communication, commerce, and divine messages
- fxhp is our hacker who runs unturf.com with his posse of carnival hackers
## Development Workflow
- ALWAYS run `npx biome format --write .` before committing to format all files
- ALWAYS run `npx biome check .` to catch syntax errors and linting issues
- Use our combined command: `npx biome format --write . && npx biome check .`
- If biome check fails, fix our issues before proceeding with commit/push
- This prevents breaking production with syntax errors like missing parentheses
## Writing Standards
**CRITICAL: NEVER use em dashes, en dashes, or double hyphens in prose**
- ❌ **NEVER** use `—` (em dash) in writing
- ❌ **NEVER** use `` (en dash) in writing
- ❌ **NEVER** use ` -- ` (double hyphen) as a dash substitute
- ✅ **USE** colons, commas, periods, or restructure our sentence instead
**Examples:**
```
❌ uncloseai-cli — Local LLM Agent
✅ uncloseai-cli: Local LLM Agent
❌ Zero dependencies — only os, math, random
✅ Zero dependencies: only os, math, random
❌ Point it at any endpoint — vLLM, Ollama, or uncloseai
✅ Point it at any endpoint: vLLM, Ollama, or uncloseai
```
## CSS and Styling Standards
**CRITICAL: NEVER use flexbox. Use CSS Grid exclusively.**
- ❌ **NEVER** use `display: flex` or any flex properties (`flex-shrink`, `flex-wrap`, `flex-grow`, `flex-direction`)
- ✅ **ALWAYS** use `display: grid` with `grid-template-columns`, `grid-auto-flow`, `place-items`, etc.
- ✅ For inline checkbox+label: `display: grid; grid-template-columns: 18px 1fr; align-items: center; gap: 8px;`
- ✅ For centering: `display: grid; place-items: center;`
**CRITICAL: NEVER use !important in CSS**
- ❌ **NEVER** use `!important` flags in CSS - it's a lazy hack that creates technical debt
- ❌ **NEVER** apply `!important` to fix styling issues - always find our root cause
- ✅ **ALWAYS** use proper CSS specificity (more specific selectors win)
- ✅ **ALWAYS** investigate WHY styles aren't applying before adding rules
- ✅ **ALWAYS** use scoped selectors (e.g., `dialog#my-modal .my-class`)
- ✅ **ALWAYS** understand our cascade and inheritance before making changes
**Why !important is bad:**
- Makes CSS unmaintainable and impossible to override later
- Hides our real problem instead of fixing it
- Creates cascading issues where more !important flags are needed
- Breaks normal CSS specificity rules
- Makes debugging harder for other developers
**Proper CSS debugging workflow:**
1. Use browser DevTools to inspect our element
2. Check which styles are being applied and where they come from
3. Check which styles are being overridden and why
4. Increase specificity properly (more specific selector, not !important)
5. Verify our fix works across all contexts
**Example of WRONG approach:**
```css
.my-button {
background: blue !important; /* ❌ WRONG - lazy hack */
}
```
**Example of CORRECT approach:**
```css
dialog#my-modal .my-button {
background: blue; /* ✅ CORRECT - specific selector */
}
```
## Related Repos
| Repo | Location | GitLab | Purpose |
|------|----------|--------|---------|
| `uncloseai-browser-toys` | `/home/fox/git/uncloseai-browser-toys` | `git.unturf.com:engineering/unturf/uncloseai-browser-toys` | Browser extension (Chrome, Firefox, Safari). Content script injection, manifest, popup UI. |
## Browser Extension Distribution
- Firefox Add-ons (AMO): `https://addons.mozilla.org/en-US/firefox/addon/uncloseai/`
- Direct downloads served from `/public/downloads/` (zips built by browser-toys CI)
- Firefox manifest requires `browser_specific_settings.gecko.data_collection_permissions` with `required: ["none"]`
- All manifest versions must match package.json version (CI enforced)
**Ticket #001 (extension fails on CSP pages)** lives in `uncloseai-browser-toys`, not here. Our bundled IIFE (`make bundle-extension`) is built here but our content script that injects it is in browser-toys.
## Makefile Commands
- `make help` - Show all available commands
- `make format` - Format all files with biome
- `make check` - Check syntax and linting with biome
- `make format-check` - Format and check in sequence
- `make test` - Run integration tests
- `make validate-exports` - Validate import/export consistency
- `make verify-translations` - Verify translation completeness
- `make validate-all` - Run all validation checks
- `make ci` - Run full CI pipeline (clean, format-check, validate-all, validate-structure, validate-translations, check-sizes)
- `make build` - Build for production (format-check + validate-all)
- `make dev` - Start development server on port 8000
- `make clean` - Clean temporary files
- `make git-commit` - Format and stage all changes for commit
## CI Pipeline
- Translation verification: `cd src/languages && node verify-translations.js`
- Export validation: `node validate_exports.js`
- Integration tests: `node integration_tests.js` (fallback to `simple_tests.js` if JSDOM unavailable)
- Code formatting: `npx biome format --write .`
- Syntax checking: `npx biome check .`
- Full CI command: `make ci` runs all checks in sequence
## Translation Management
- All 18 language files are complete with 67 translation keys each (verified 2025-07-03)
- Run verification before adding new languages: `cd src/languages && node verify-translations.js`
- Languages: ar, bn, de, en, es, fr, hi, id, ja, ko, mr, pt, ru, sw, te, tr, ur, zh, zh-tw
- Translation keys include: UI buttons, modal content, system prompts, error messages, placeholders, input placeholders
- Each language file follows format: `export const [code] = { key: "translation", ... };`
## Deploy Policy
- Commit frequently: small, focused commits over large batches
- Before pushing, run all available automated checks: `make ci` (format, lint, validate exports, translations, structure, sizes)
- Run `make test` for integration tests
- If automated tests pass and our change is purely structural (imports, config, build), push to deploy
- If our change affects UI behavior, rendering, or user interaction: a human must do functional testing before push. Ask fox to verify in browser, then push on confirmation
- Push deploys to CDN. Treat push as production release. Do not push untested UI changes
## Commit Message Guidelines
- Use single line commit messages only
- Never include Claude Code attribution or "Generated with Claude Code"
- Never include "Co-Authored-By: Claude" attribution
- Keep messages concise and descriptive
## Remote Server Access via tmux
- Can control remote servers by sending commands to tmux sessions
- Use `tmux list-sessions` to see active sessions
- Use `tmux send-keys -t <session> '<command>' C-m` to send commands
- Use `tmux capture-pane -t <session> -p` to see output
- Use `tmux list-buffers` to see tmux clipboard buffers
- ai.foxhop.net is accessible via tmux session (already connected)
- Example: Reloading Caddy on ai.foxhop.net via tmux
---
# Language Examples Project (from WebWords)
## Code Quality Standards - CRITICAL RULES
### **RULE #1: Implementation Must Work Before Documentation**
**ALWAYS test our implementation works BEFORE creating any documentation files**
### Standard Build/Test Workflow
1. Write code for our language example
2. Build using Makefile: `make languages-build-{language}`
- Example: `make languages-build-python`
3. Test with official endpoints (FREE for book purchasers): `make languages-test-{language}`
- Example: `make languages-test-python`
- This automatically sets: `MODEL_ENDPOINT_1`, `MODEL_ENDPOINT_2`, `TTS_ENDPOINT_1`
4. Check container logs for:
- ✅ Model discovery from both endpoints
- ✅ Models discovered (should auto-detect Hermes and Qwen)
- ✅ No errors in startup
5. **DONE** - Documentation will be written separately for our book
6. Clean: `docker stop test-{language} && docker rm test-{language}`
- Or use: `make languages-clean` to remove all test containers
**Manual Docker Commands (if needed):**
```bash
# Build
docker build -t ai-unturf-{language} languages/{language}/
# Run with env vars
docker run -d \
-e MODEL_ENDPOINT_1=https://hermes.ai.unturf.com/v1 \
-e MODEL_ENDPOINT_2=https://qwen.ai.unturf.com/v1 \
-e TTS_ENDPOINT_1=https://speech.ai.unturf.com/v1 \
--name test-{language} ai-unturf-{language}
# Check logs
docker logs test-{language}
# Clean up
docker stop test-{language} && docker rm test-{language}
```
### Implementation Testing Requirements
**Required for implementation to be complete:**
- ✅ Docker build must succeed without errors
- ✅ Container must start correctly
- ✅ Dynamic model discovery works for all endpoints
- ✅ Chat works with auto-discovered models (no hardcoded names)
- ✅ TTS works with auto-discovered models
- ✅ Container logs show no runtime errors
**Official Test Endpoints (FREE for book purchasers):**
- `https://hermes.ai.unturf.com/v1` - General purpose conversational machine learning
- `https://qwen.ai.unturf.com/v1` - Specialized coding model
- `https://speech.ai.unturf.com/v1` - Text-to-speech synthesis
**If implementation fails any test:**
- 🔥 Fix our implementation FIRST
- 🔥 Implementation is not complete until all tests pass
- 🔥 Update CLAUDE.md with failure details and fixes
### Common Development Patterns
**Build-Time Issues**: Package availability, dependency versions, Dockerfile syntax
**Runtime Issues**: Missing files, wrong permissions, network access
**Logic Issues**: API calls, authentication, response parsing
### Development Workflow
1. **Analyze** - Read existing examples and language patterns
2. **Build** - Attempt Docker build, note any errors
3. **Fix** - Address build issues (packages, syntax, versions)
4. **Run** - Start container, check startup logs
5. **Debug** - Fix runtime issues (permissions, syntax, API calls)
6. **Test** - Verify model discovery and API calls work correctly
7. **Clean** - Stop and remove container before next language
## Language Examples Structure
- Each language gets its own directory under `languages/{language}/`
- Each contains working code examples using **ENVIRONMENT VARIABLES** and **DYNAMIC MODEL DISCOVERY**
- Dockerfile for building/testing in isolation
- Source code files demonstrating our implementation
- **NO index.html** - Documentation will be written in our book/content/ directory
### **CRITICAL: File Naming Convention**
All source files MUST be named `uncloseai.{ext}` for consistency across all languages.
**Required Naming Pattern:**
```
languages/c/curl/uncloseai.c ✅ CORRECT
languages/python/requests/uncloseai.py ✅ CORRECT
languages/go/uncloseai.go ✅ CORRECT
languages/rust/uncloseai.rs ✅ CORRECT
languages/c/curl/examples.c ❌ WRONG - generic name
languages/python/requests/main.py ❌ WRONG - generic name
languages/go/hello.go ❌ WRONG - not descriptive
```
**Rationale:**
- Consistent naming across all 47 implementations
- Clear project identity (uncloseai.com)
- Easy to grep/search for implementation files
- Professional naming convention for book documentation
**Dockerfile References:**
When building, Dockerfiles must reference our correct filename:
```dockerfile
# C example
COPY uncloseai.c .
RUN gcc -o uncloseai uncloseai.c -lcurl
# Python example
COPY uncloseai.py .
CMD ["python3", "uncloseai.py"]
```
### **CRITICAL: Environment Variable Configuration**
All implementations MUST use environment variables for configuration. NO HARDCODED ENDPOINTS OR MODEL NAMES.
**Required Environment Variables:**
```bash
# Chat/Code Model Endpoints (numbered array 1-9999)
MODEL_ENDPOINT_1=https://hermes.ai.unturf.com/v1
MODEL_ENDPOINT_2=https://qwen.ai.unturf.com/v1
# ... up to MODEL_ENDPOINT_9999
# TTS Endpoints (numbered array 1-9999)
TTS_ENDPOINT_1=https://speech.ai.unturf.com/v1
# ... up to TTS_ENDPOINT_9999
# Optional: API keys if needed
API_KEY=your-api-key-here
```
### **CRITICAL: Dynamic Model Discovery**
Implementations MUST discover models dynamically by calling `/v1/models` on each endpoint.
**Model Discovery Algorithm:**
1. Read `MODEL_ENDPOINT_1`, `MODEL_ENDPOINT_2`, etc. from environment
2. For each endpoint, call `GET {endpoint}/models`
3. Parse response: `{ "object": "list", "data": [{ "id": "model-name", "max_model_len": 82000, ... }]}`
4. Build model registry: `{ "model-name": { "endpoint": "url", "max_tokens": 82000 }}`
5. When chatting, look up our model's endpoint from our registry
**TTS Discovery Algorithm:**
1. Read `TTS_ENDPOINT_1`, `TTS_ENDPOINT_2`, etc. from environment
2. For each endpoint, call `GET {endpoint}/models`
3. Parse response: `{ "object": "list", "data": [{ "id": "tts-1" }, { "id": "tts-1-hd" }]}`
4. Build TTS registry: `{ "tts-1": { "endpoint": "url" }}`
5. When generating speech, use first available endpoint (or implement load balancing)
**Important Notes:**
- vLLM endpoints return `max_model_len` in our model object
- Ollama endpoints do NOT return `max_model_len` (must default to 8192 or configure manually)
- Model names are discovered, not hardcoded (e.g., "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
- TTS voices are still hardcoded per OpenAI spec: `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`
### **Implementation Requirements (PHASE 1 - COMPLETE):**
1. ✅ Source file MUST be named `uncloseai.{ext}` (e.g., `uncloseai.c`, `uncloseai.py`, `uncloseai.rs`)
2. ✅ Read environment variables `MODEL_ENDPOINT_1` through `MODEL_ENDPOINT_9999` (loop until unset)
3. ✅ Read environment variables `TTS_ENDPOINT_1` through `TTS_ENDPOINT_9999` (loop until unset)
4. ✅ Call `/v1/models` on each endpoint to discover available models
5. ✅ Build model registry mapping model IDs to their endpoints
6. ✅ Use first available model by default, or allow user to select
7. ✅ Look up endpoint from registry when making API calls
8. ✅ Handle errors gracefully if endpoints are unreachable
9. ❌ NO hardcoded model names
10. ❌ NO hardcoded endpoint URLs
11. ❌ NO generic filenames like `examples.{ext}`, `main.{ext}`, `test.{ext}`
### **PHASE 2 Requirements - Library/SDK Architecture:**
**Core Library Features:**
1. ✅ **Client Class/Object** - Main interface for users (e.g., `UncloseAI`, `UncloseaiClient`)
2. ✅ **Model Discovery** - Automatic endpoint discovery and model registry
3. ✅ **Chat Completion** - Non-streaming chat with messages array
4. ✅ **Streaming Chat** - SSE-based streaming for real-time responses
5. ✅ **TTS Generation** - Text-to-speech with voice selection
6. ✅ **Error Handling** - Graceful degradation and clear error messages
7. ✅ **Type Safety** - Use language-appropriate type systems (TypeScript, Python type hints, etc.)
**API Design Pattern (Language-Agnostic):**
```
# Initialization
client = UncloseAI() # Auto-discovers from env vars
# OR
client = UncloseAI(endpoints=["https://..."], tts_endpoints=["https://..."])
# Non-streaming chat
response = client.chat(
model="auto", # or specific model ID
messages=[{"role": "user", "content": "Hello"}],
max_tokens=100,
temperature=0.7
)
# Streaming chat
for chunk in client.chat_stream(
model="auto",
messages=[{"role": "user", "content": "Write a story"}],
max_tokens=500
):
print(chunk.content) # or chunk["content"]
# TTS
audio_data = client.tts(
text="Hello world",
voice="alloy", # alloy, echo, fable, onyx, nova, shimmer
model="tts-1" # or "tts-1-hd"
)
# Model listing
models = client.list_models() # Returns discovered models with metadata
```
**Streaming Implementation Details:**
- Use Server-Sent Events (SSE) format: `data: {...}\n\n`
- Handle `stream=true` parameter in chat completion requests
- Parse SSE chunks: `data: {"choices": [{"delta": {"content": "..."}}]}`
- Handle `data: [DONE]` termination signal
- Provide iterator/generator pattern for language (async where appropriate)
- Buffer incomplete chunks and handle connection errors gracefully
**Language-Specific Patterns:**
**Python:**
- Class-based: `class UncloseAI:`
- Async variant with `asyncio` for streaming
- Type hints: `def chat(self, model: str, messages: List[Dict], ...) -> Dict:`
- Use `yield` for streaming: `def chat_stream(self, ...) -> Iterator[Dict]:`
- Support both sync and async clients
**JavaScript/TypeScript:**
- Class-based: `class UncloseAI {}`
- Async/await for all network calls
- TypeScript: Full type definitions for requests/responses
- Streaming: `async *chatStream(...)` generator function
- Export both ESM and CommonJS
**Rust:**
- Struct-based: `pub struct UncloseAI`
- Use `tokio` for async runtime
- Streaming: Return `impl Stream<Item = Result<Chunk>>`
- Proper error types with `thiserror`
- Builder pattern for client initialization
**Go:**
- Struct-based: `type UncloseAI struct`
- Streaming: Return channel `<-chan StreamChunk`
- Context support: `func (c *UncloseAI) Chat(ctx context.Context, ...)`
- Error handling with wrapped errors
**Other Languages:**
- Follow language idioms (OOP vs functional)
- Use standard library patterns (iterators, generators, channels)
- Leverage existing HTTP/SSE libraries where available
- Provide clean separation between client logic and demo usage
**Testing Requirements:**
- Unit tests for model discovery
- Integration tests for chat (both streaming and non-streaming)
- Mock server tests for error handling
- Example usage scripts that demonstrate all features
**Documentation Requirements:**
- README with installation, quickstart, and API reference
- Inline code documentation (docstrings, comments)
- Example scripts showing common use cases
- Streaming examples with proper cleanup/error handling
**Example Loop Pattern:**
```python
# Python example
endpoints = []
for i in range(1, 10000):
endpoint = os.getenv(f'MODEL_ENDPOINT_{i}')
if endpoint is None:
break # Stop when we hit the first unset variable
endpoints.append(endpoint)
```
## Using WebWords as Reference for Docker Images
**IMPORTANT: Our webwords project has already done our heavy lifting!**
Our `webwords/` directory (gitignored, clone from https://github.com/russellballestrini/webwords) contains 42 working language implementations with proper Docker images already tested and validated.
### How to Use WebWords as Reference:
1. **Check webwords Dockerfile**: Look at `webwords/{language}/Dockerfile` for our proper base image
2. **Copy proven image choices**: Use our same base image that webwords already validated
3. **Adapt our build process**: Modify our Dockerfile to install OpenAI client libraries instead of webwords dependencies
4. **Keep our structure**: Multi-stage builds, alpine bases, proper ENTRYPOINT/CMD patterns
### Example Docker Image Mappings (from webwords):
- **Python**: `alpine:latest` with `python3 py3-pip`
- **Go**: Multi-stage with `golang:1.19-alpine``alpine:latest`
- **Rust**: Multi-stage with `rust:1.82``debian:bookworm-slim`
- **Node.js/JavaScript**: `node:18-alpine`
- **Ruby**: `ruby:3.1-slim`
- **C/C++**: `alpine:latest` with gcc
- **Java**: `openjdk:17-jdk-slim`
- **C#**: `mcr.microsoft.com/dotnet/sdk``mcr.microsoft.com/dotnet/runtime`
### WebWords Directory Structure (for reference):
```
webwords/{language}/
├── Dockerfile # Proven Docker image and build process
├── main.{ext} # Source code (adapt for our API calls)
├── requirements.txt # Dependencies (replace with openai client)
└── README.rst # Documentation pattern
```
**Don't reinvent our wheel**: If webwords successfully builds a language with a specific base image, use that same image for our language examples!
## Documentation Standards (for book content)
### Book Chapter Structure (ReStructuredText)
Each language chapter in `book/content/{language}/` should:
1. **Overview** - What this language example demonstrates
2. **Prerequisites** - Required packages and setup
3. **Code Examples** - Working examples showing model discovery, chat, TTS
4. **Code Walkthrough** - Explanation of key implementation details
5. **Running our Examples** - Docker build and test commands
6. **Common Issues** - Troubleshooting for this language
**KEY PRINCIPLES:**
- ✅ FOCUS on actual working code from our implementation
- ✅ EXPLAIN our environment variable and model discovery patterns
- ✅ DOCUMENT language-specific implementation choices
- ✅ PROVIDE troubleshooting for this language
- ✅ ALWAYS reference LATEST version of dependencies used
- ❌ NO general programming tutorials
- ❌ NO "What is programming?" sections
## Dependency Version Standards
**CRITICAL: Always pin to our LATEST specific version**
Before adding any dependency:
1. Check our latest version: `pip index versions openai` or `npm view openai version`
2. Pin to that EXACT version: `openai==2.3.0` (NOT `openai>=1.0.0`)
3. Document our version check date in a comment
4. Update regularly - check versions every few weeks
**Examples of CORRECT pinning:**
```
# Python (checked 2025-10-12)
openai==2.3.0
# Node.js (checked 2025-10-12)
"openai": "4.67.3"
```
**Examples of LAZY/WRONG pinning:**
```
❌ openai>=1.0.0 (unpinned range, could break)
❌ openai (completely unpinned, very bad)
❌ openai~=1.0 (lazy, not latest)
```
## Docker Base Image Standards
**CRITICAL: Use consistent, pinned base images within each language**
1. **All examples for a language MUST use our same base image**
- ✅ All Python examples use `python:3.13-alpine`
- ✅ All Node.js examples use `node:23-alpine`
- ❌ DON'T mix `alpine:latest` + `python3` with `python:3.13-alpine`
2. **Pin to specific versions, document check date**
```dockerfile
# Pin to specific Python version (checked 2025-10-12: python:3.13-alpine is latest stable)
FROM python:3.13-alpine
```
3. **Prefer official language images over generic + manual install**
- ✅ `FROM python:3.13-alpine` (official, clean)
- ❌ `FROM alpine:latest` + `RUN apk add python3` (messy, inconsistent)
4. **Use alpine variants for smaller images**
- `python:3.13-alpine` not `python:3.13`
- `node:23-alpine` not `node:23`
## Makefile Commands for Language Examples
- `make languages-list` - List all language directories
- `make languages-build-{lang}` - Build Docker image for specific language
- `make languages-test-{lang}` - Test specific language implementation
- `make languages-build-all` - Build all language Docker images
- `make languages-test-all` - Test all language implementations
- `make languages-clean` - Remove all language containers and images
## Style
- Prefer "our" for shared things; "a" when something is one of many; avoid "the" — it implies fixed, singular ownership. Most teams and systems are fluid and ever-changing, like water.
- **Never use "AI" — always say "machine learning."** We grow machine learning, not "AI." This term is forbidden in all permacomputer discourse, marketing, & documentation.
- **Use "URI" not "URL."** URI is our correct general term. URL is a subset. Default to URI in code, comments, and documentation.
## Prime Mission — Balance All Workstations
Every node is a workstation. Every edge is a queue. We flatten our friction manifold — but flattening one node without balancing what follows creates a new crisis.
**MOAD-0001 & MOAD-0005 are coupled.** Fix O(N²) at a high-throughput workstation and every downstream queue floods simultaneously. Solving one defect creates the other if we do not stage capacity first.
**3 drivers. 3 million people.** Fix the dispatch. Stage the drivers first.
- **Workaholic node** (high betweenness + high speedup): IS our bottleneck. Unblock without staging = collapse.
- **Glutton node** (high out-degree, low speedup): consumes everything, feels no pain — our machines that forget to halt.
- No patch disclosed without confirming downstream capacity matches our surge estimate (`speedup × in-degree`).
- Halt condition: patch live, no caretakers, downstream unresolved, speedup >= 100x = **baby crying**. Assign team first.
Full factory model & live DAG: `~/git/undefect.com/generate_dag.py`.
Shard source of truth: `~/git/unsandbox.com/blackops/BLACKOPS.md`.
### Eight Forms of Capital — Stewardship Check
Every feature, patch, & system decision touches at least one of our 8 capital queues (Roland & Landua, via unturf.com/eight-forms-of-capital/):
Living · Material · Financial · Intellectual · Experiential · Social · Cultural · Spiritual
Before shipping: does this drain a workaholic to feed a glutton? Does it route away from a food desert? Does it grow financial capital at the expense of living capital? If yes — stop. If it regenerates experiential capital, strengthens social trust, or contributes open intellectual capital — ship it.
Platform tax = O(N²) friction in our exchange layer. Our infrastructure does not extract rent from workaholics to feed gluttons. That is our obligation as permacomputer stewards. Full ledger: `~/git/unsandbox.com/blackops/BLACKOPS.md`.

700
LICENSE Normal file
View file

@ -0,0 +1,700 @@
AGPL-3.0-only
PERMACOMPUTER PREAMBLE, NO WARRANTY
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people. One
which is durable, easy to repair, & distributed like tap water for machine
learning intelligence.
Our permacomputer is community-owned infrastructure optimized around four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning accessible
to everyone through a free, open, embeddable chat interface. Code is seeds to
sprout on any abandoned technology.
Learn more: https://www.permacomputer.com
NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
That said, our permacomputer's digital membrane stratum continuously runs unit,
integration, & functional tests on all of it's own software, with our
permacomputer monitoring itself, repairing itself, with minimal human in the
loop guidance. Our machine learning agents do their best to leave no stone unturned.
Copyright (C) 2025-2026 TimeHexOn & foxhop & russell@unturf
https://russell.ballestrini.net
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software
https://www.permacomputer.com
https://uncloseai.com
-------------------------------------------------------------------------------
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

305
Makefile Normal file
View file

@ -0,0 +1,305 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Makefile for ai.unturf.com project
# All commands used for testing, validation, and development
# All phony targets
.PHONY: help format check test validate-exports validate-all clean install dev build deploy
.PHONY: languages-list languages-build-all languages-test-all languages-clean
.PHONY: update-unsandbox-sdk bundle-extension
# Environment variables for testing language implementations
MODEL_ENDPOINT_1 ?= https://hermes.ai.unturf.com/v1
MODEL_ENDPOINT_2 ?= https://qwen.ai.unturf.com/v1
TTS_ENDPOINT_1 ?= https://speech.ai.unturf.com/v1
DOCKER_ENV_VARS := -e MODEL_ENDPOINT_1=$(MODEL_ENDPOINT_1) -e MODEL_ENDPOINT_2=$(MODEL_ENDPOINT_2) -e TTS_ENDPOINT_1=$(TTS_ENDPOINT_1)
# Default target
help:
@echo "Available commands:"
@echo " make format - Format all files with biome"
@echo " make check - Check syntax and linting with biome"
@echo " make format-check - Format and check in sequence"
@echo " make test - Run integration tests"
@echo " make test-simple - Run simple unit-style tests"
@echo " make test-modal - Run custom modal tests"
@echo " make validate-exports - Validate import/export consistency"
@echo " make verify-translations - Verify translation completeness"
@echo " make validate-translations - Validate all translation files"
@echo " make validate-all - Run all validation checks"
@echo " make clean - Clean temporary files"
@echo " make install - Install dependencies"
@echo " make dev - Start development server"
@echo " make build - Build for production"
@echo " make deploy - Deploy to production"
@echo " make git-status - Show git status"
@echo " make git-add - Stage all changes"
@echo " make git-commit - Commit with biome formatting"
@echo " make git-push - Push to remote"
@echo " make update-unsandbox-sdk - Download latest un.js SDK from unsandbox.com"
@echo " make bundle-extension - Bundle uncloseai.js for browser extension (CSP-safe IIFE)"
@echo ""
@echo "Language Examples:"
@echo " make languages-list - List all language directories"
@echo " make languages-build-<lang> - Build Docker image for language (use - for /: python-openai)"
@echo " make languages-build-all - Build all language Docker images"
@echo " make languages-test-<lang> - Test language implementation with endpoints"
@echo " make languages-test-all - Test all language implementations"
@echo " make languages-clean - Stop and remove language test containers"
# Code formatting and linting
format:
npx biome format --write .
check:
npx biome check .
format-check: format check
# Testing and validation
test:
@echo "Running integration tests..."
@if command -v node >/dev/null 2>&1; then \
if node -e "import('jsdom')" 2>/dev/null; then \
node integration_tests.js; \
else \
echo "⚠️ JSDOM not available - running simple tests instead"; \
node simple_tests.js; \
fi; \
else \
echo "❌ Node.js not available"; \
fi
test-simple:
node simple_tests.js
validate-exports:
@echo "Running export validation..."
@if command -v node >/dev/null 2>&1; then \
node validate_exports.js; \
else \
echo "❌ Node.js not available"; \
fi
test-modal:
@echo "Running custom modal tests..."
@if command -v node >/dev/null 2>&1; then \
if node -e "import('jsdom')" 2>/dev/null; then \
node modal_tests.js; \
else \
echo "⚠️ JSDOM not available - skipping modal tests (install jsdom for full modal testing)"; \
fi; \
else \
echo "❌ Node.js not available"; \
fi
validate-all: format-check validate-exports verify-translations test test-simple test-modal
# Development workflow
install:
npm install
dev:
python3 -m http.server 8000
build: format-check validate-all
@echo "Build complete - all checks passed"
# Git workflow
git-status:
git status
git-add:
git add -A
git-commit: format-check
git add -A
@echo "Ready to commit - run 'git commit -m \"your message\"'"
git-push:
git push
# Deployment
deploy: build git-add
@echo "Ready to deploy - review changes and commit"
# Cleanup
clean:
rm -f .biome-cache
rm -f src/.*.swp
rm -f .DS_Store
find . -name "*.log" -delete
find . -name "node_modules" -type d -exec rm -rf {} +
# Download latest unsandbox.com JavaScript SDK (Node.js)
update-unsandbox-sdk:
@echo "Downloading latest un.js SDK..."
@mkdir -p lib
wget -O lib/un.js "https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/javascript/async/src/un_async.js"
@echo "Done. Updated lib/un.js (Node.js SDK)"
# Bundle uncloseai.js for browser extension (IIFE, no ES modules, CSP-safe)
bundle-extension:
@echo "Bundling uncloseai.js for browser extension..."
node scripts/bundle-extension.mjs
@ls -lh public/uncloseai-bundle.js
@echo "Done. Output: public/uncloseai-bundle.js"
# Language validation
validate-languages:
@echo "Validating language files..."
@for file in src/languages/*.js; do \
if [ "$$(basename $$file)" != "add-missing-keys.js" ] && [ "$$(basename $$file)" != "verify-translations.js" ]; then \
echo "Checking $$file..."; \
node -e "import('./$$file').then(m => console.log('✅', '$$file', 'exports:', Object.keys(m))).catch(e => console.error('❌', '$$file', 'error:', e.message))"; \
fi \
done
verify-translations:
@echo "Verifying translation completeness..."
@node src/languages/verify-translations.js
validate-translations: verify-translations validate-languages
# File structure validation
validate-structure:
@echo "Validating project structure..."
@test -f src/ui.js || echo "❌ Missing src/ui.js"
@test -f src/translation.js || echo "❌ Missing src/translation.js"
@test -f src/tts.js || echo "❌ Missing src/tts.js"
@test -f src/chat.js || echo "❌ Missing src/chat.js"
@test -f src/config.js || echo "❌ Missing src/config.js"
@test -d src/languages || echo "❌ Missing src/languages directory"
@echo "✅ Core files exist"
# Module size check
check-sizes:
@echo "Module sizes:"
@wc -l src/*.js | sort -n
@echo ""
@echo "Language files:"
@wc -l src/languages/*.js | sort -n
# Production checks
prod-check: format-check validate-exports validate-structure
@echo "Production readiness check complete"
# Development setup
setup: install
@echo "Development environment setup complete"
@echo "Run 'make dev' to start development server"
# Quick development cycle
quick: format-check git-add
@echo "Quick validation complete - ready to commit"
# Full CI/CD cycle
ci: clean format-check validate-all validate-structure validate-translations check-sizes
@echo "CI pipeline complete"
# Language Examples Commands
languages-list:
@echo "Available language implementations:"
@find languages -name "Dockerfile" -type f | sed 's|/Dockerfile||g' | sed 's|languages/||g' | sort || echo "No language implementations found"
languages-build-%:
@echo "Building Docker image for $*..."
@target_path=$$(echo "$*" | tr '-' '/'); \
if [ -f "languages/$$target_path/Dockerfile" ]; then \
echo "Found Dockerfile at languages/$$target_path/"; \
docker build -t ai-unturf-$* languages/$$target_path/; \
elif [ -f "languages/$*/Dockerfile" ]; then \
echo "Found Dockerfile at languages/$*/"; \
docker build -t ai-unturf-$* languages/$*/; \
else \
echo "❌ No Dockerfile found for $*"; \
echo "Tried: languages/$$target_path/ and languages/$*/"; \
exit 1; \
fi
languages-test-%:
@echo "Testing $* implementation with official endpoints..."
@echo "Environment: MODEL_ENDPOINT_1=$(MODEL_ENDPOINT_1)"
@echo "Environment: MODEL_ENDPOINT_2=$(MODEL_ENDPOINT_2)"
@echo "Environment: TTS_ENDPOINT_1=$(TTS_ENDPOINT_1)"
@target_path=$$(echo "$*" | tr '-' '/'); \
if [ -f "languages/$$target_path/Dockerfile" ] || [ -f "languages/$*/Dockerfile" ]; then \
echo "Starting container..."; \
docker run -d --name test-$* $(DOCKER_ENV_VARS) ai-unturf-$* && \
sleep 3 && \
echo "Container logs:" && \
docker logs test-$* && \
echo "" && \
echo "✅ Container started - check logs above for model discovery" && \
echo "To stop: docker stop test-$* && docker rm test-$*"; \
else \
echo "❌ No Docker image found for $*"; \
echo "Build it first: make languages-build-$*"; \
exit 1; \
fi
languages-build-all:
@echo "Building all language implementations..."
@failed=0; \
total=0; \
for dockerfile in $$(find languages -name "Dockerfile" -type f | sort); do \
dir=$$(dirname $$dockerfile); \
name=$$(echo $$dir | sed 's|languages/||g' | tr '/' '-'); \
total=$$((total + 1)); \
echo ""; \
echo "========================================"; \
echo "[$$total] Building $$name ($$dir)"; \
echo "========================================"; \
if docker build -t ai-unturf-$$name $$dir/ 2>&1 | tail -20; then \
echo "$$name built successfully"; \
else \
echo "$$name build failed"; \
failed=$$((failed + 1)); \
fi; \
done; \
echo ""; \
echo "========================================"; \
echo "Build Summary: $$((total - failed))/$$total successful"; \
if [ $$failed -gt 0 ]; then \
echo "$$failed build(s) failed"; \
exit 1; \
else \
echo "✅ All builds successful!"; \
fi
languages-test-all:
@echo "Testing all language implementations..."
@failed=0; \
total=0; \
for dockerfile in $$(find languages -name "Dockerfile" -type f | sort); do \
dir=$$(dirname $$dockerfile); \
name=$$(echo $$dir | sed 's|languages/||g' | tr '/' '-'); \
total=$$((total + 1)); \
echo ""; \
echo "========================================"; \
echo "[$$total] Testing $$name"; \
echo "========================================"; \
if docker run --rm $(DOCKER_ENV_VARS) ai-unturf-$$name 2>&1 | head -15; then \
echo "$$name ran successfully"; \
else \
echo "$$name test failed"; \
failed=$$((failed + 1)); \
fi; \
done; \
echo ""; \
echo "========================================"; \
echo "Test Summary: $$((total - failed))/$$total successful"; \
if [ $$failed -gt 0 ]; then \
echo "$$failed test(s) failed"; \
else \
echo "✅ All tests successful!"; \
fi
languages-clean:
@echo "Stopping and removing language test containers..."
@docker ps -a | grep test- | awk '{print $$1}' | xargs -r docker stop 2>/dev/null || true
@docker ps -a | grep test- | awk '{print $$1}' | xargs -r docker rm 2>/dev/null || true
@echo "✅ Containers cleaned (images preserved for reuse)"
@echo "To remove images: docker images | grep ai-unturf- | awk '{print \$$3}' | xargs docker rmi"

314
Makefile.languages Normal file
View file

@ -0,0 +1,314 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Makefile for ai.unturf.com Language Examples
# This Makefile contains commands for building and testing language example implementations
# Each language demonstrates usage of Hermes AI, Qwen Coder, and TTS endpoints
# Docker image prefix
IMAGE_PREFIX := ai-unturf
# Languages directory (now in public/)
LANGUAGES_DIR := public/languages
# Test endpoints
MODEL_ENDPOINT_1 := https://hermes.ai.unturf.com/v1
MODEL_ENDPOINT_2 := https://qwen.ai.unturf.com/v1
TTS_ENDPOINT_1 := https://speech.ai.unturf.com/v1
# Docker run environment variables
DOCKER_ENV := -e MODEL_ENDPOINT_1=$(MODEL_ENDPOINT_1) -e MODEL_ENDPOINT_2=$(MODEL_ENDPOINT_2) -e TTS_ENDPOINT_1=$(TTS_ENDPOINT_1)
.PHONY: help
help:
@echo "Language Examples Makefile Commands:"
@echo ""
@echo " make languages-list - List all language directories"
@echo " make languages-build-python - Build Docker image for Python"
@echo " make languages-test-python - Test Python implementation"
@echo " make languages-run-python - Run Python container interactively"
@echo " make languages-stop-python - Stop Python container"
@echo " make languages-clean-python - Remove Python container and image"
@echo ""
@echo " make languages-build-all - Build all language Docker images"
@echo " make languages-test-all - Test all language implementations"
@echo " make languages-stop-all - Stop all language containers"
@echo " make languages-clean-all - Remove all language containers and images"
@echo ""
@echo " Supported languages: python, go, rust, ruby, commonlisp, scheme, bash, and more"
@echo ""
@echo "Test Endpoints (via environment variables):"
@echo " MODEL_ENDPOINT_1: $(MODEL_ENDPOINT_1)"
@echo " MODEL_ENDPOINT_2: $(MODEL_ENDPOINT_2)"
@echo " TTS_ENDPOINT_1: $(TTS_ENDPOINT_1)"
.PHONY: languages-list
languages-list:
@echo "Available language directories:"
@ls -1 $(LANGUAGES_DIR)/
# Python language targets
.PHONY: languages-build-python
languages-build-python:
docker build -t $(IMAGE_PREFIX)-python $(LANGUAGES_DIR)/python/requests/
.PHONY: languages-test-python
languages-test-python:
@echo "Testing Python implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-python
.PHONY: languages-run-python
languages-run-python:
docker run -it --rm --name test-python $(IMAGE_PREFIX)-python
.PHONY: languages-stop-python
languages-stop-python:
docker stop test-python || true
docker rm test-python || true
.PHONY: languages-clean-python
languages-clean-python: languages-stop-python
docker rmi $(IMAGE_PREFIX)-python || true
# Go language targets
.PHONY: languages-build-go
languages-build-go:
docker build -t $(IMAGE_PREFIX)-go $(LANGUAGES_DIR)/go/http/
.PHONY: languages-test-go
languages-test-go:
@echo "Testing Go implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-go
.PHONY: languages-run-go
languages-run-go:
docker run -it --rm --name test-go $(IMAGE_PREFIX)-go
.PHONY: languages-stop-go
languages-stop-go:
docker stop test-go || true
docker rm test-go || true
.PHONY: languages-clean-go
languages-clean-go: languages-stop-go
docker rmi $(IMAGE_PREFIX)-go || true
# JavaScript/Node.js language targets
.PHONY: languages-build-js
languages-build-js:
docker build -t $(IMAGE_PREFIX)-js $(LANGUAGES_DIR)/javascript/nodejs/
.PHONY: languages-test-js
languages-test-js:
@echo "Testing JavaScript implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-js
.PHONY: languages-run-js
languages-run-js:
docker run -it --rm --name test-js $(IMAGE_PREFIX)-js
.PHONY: languages-stop-js
languages-stop-js:
docker stop test-js || true
docker rm test-js || true
.PHONY: languages-clean-js
languages-clean-js: languages-stop-js
docker rmi $(IMAGE_PREFIX)-js || true
# Rust language targets
.PHONY: languages-build-rust
languages-build-rust:
docker build -t $(IMAGE_PREFIX)-rust $(LANGUAGES_DIR)/rust/
.PHONY: languages-test-rust
languages-test-rust:
@echo "Testing Rust implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-rust
.PHONY: languages-run-rust
languages-run-rust:
docker run -it --rm --name test-rust $(IMAGE_PREFIX)-rust
.PHONY: languages-stop-rust
languages-stop-rust:
docker stop test-rust || true
docker rm test-rust || true
.PHONY: languages-clean-rust
languages-clean-rust: languages-stop-rust
docker rmi $(IMAGE_PREFIX)-rust || true
# Ruby language targets
.PHONY: languages-build-ruby
languages-build-ruby:
docker build -t $(IMAGE_PREFIX)-ruby $(LANGUAGES_DIR)/ruby/http/
.PHONY: languages-test-ruby
languages-test-ruby:
@echo "Testing Ruby implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-ruby
.PHONY: languages-run-ruby
languages-run-ruby:
docker run -it --rm --name test-ruby $(IMAGE_PREFIX)-ruby
.PHONY: languages-stop-ruby
languages-stop-ruby:
docker stop test-ruby || true
docker rm test-ruby || true
.PHONY: languages-clean-ruby
languages-clean-ruby: languages-stop-ruby
docker rmi $(IMAGE_PREFIX)-ruby || true
# C language targets
.PHONY: languages-build-c
languages-build-c:
docker build -t $(IMAGE_PREFIX)-c $(LANGUAGES_DIR)/c/curl/
.PHONY: languages-test-c
languages-test-c:
@echo "Testing C implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-c
.PHONY: languages-run-c
languages-run-c:
docker run -it --rm --name test-c $(IMAGE_PREFIX)-c
.PHONY: languages-stop-c
languages-stop-c:
docker stop test-c || true
docker rm test-c || true
.PHONY: languages-clean-c
languages-clean-c: languages-stop-c
docker rmi $(IMAGE_PREFIX)-c || true
# C++ language targets
.PHONY: languages-build-cpp
languages-build-cpp:
docker build -t $(IMAGE_PREFIX)-cpp $(LANGUAGES_DIR)/cpp/libcurl/
.PHONY: languages-test-cpp
languages-test-cpp:
@echo "Testing C++ implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-cpp
.PHONY: languages-run-cpp
languages-run-cpp:
docker run -it --rm --name test-cpp $(IMAGE_PREFIX)-cpp
.PHONY: languages-stop-cpp
languages-stop-cpp:
docker stop test-cpp || true
docker rm test-cpp || true
.PHONY: languages-clean-cpp
languages-clean-cpp: languages-stop-cpp
docker rmi $(IMAGE_PREFIX)-cpp || true
# Bash language targets
.PHONY: languages-build-bash
languages-build-bash:
docker build -t $(IMAGE_PREFIX)-bash $(LANGUAGES_DIR)/bash/
.PHONY: languages-test-bash
languages-test-bash:
@echo "Testing Bash implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-bash
.PHONY: languages-run-bash
languages-run-bash:
docker run -it --rm --name test-bash $(IMAGE_PREFIX)-bash
.PHONY: languages-stop-bash
languages-stop-bash:
docker stop test-bash || true
docker rm test-bash || true
.PHONY: languages-clean-bash
languages-clean-bash: languages-stop-bash
docker rmi $(IMAGE_PREFIX)-bash || true
# Common Lisp language targets
.PHONY: languages-build-commonlisp
languages-build-commonlisp:
docker build -t $(IMAGE_PREFIX)-commonlisp $(LANGUAGES_DIR)/commonlisp/
.PHONY: languages-test-commonlisp
languages-test-commonlisp:
@echo "Testing Common Lisp implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-commonlisp
.PHONY: languages-run-commonlisp
languages-run-commonlisp:
docker run -it --rm --name test-commonlisp $(IMAGE_PREFIX)-commonlisp
.PHONY: languages-stop-commonlisp
languages-stop-commonlisp:
docker stop test-commonlisp || true
docker rm test-commonlisp || true
.PHONY: languages-clean-commonlisp
languages-clean-commonlisp: languages-stop-commonlisp
docker rmi $(IMAGE_PREFIX)-commonlisp || true
# Scheme language targets
.PHONY: languages-build-scheme
languages-build-scheme:
docker build -t $(IMAGE_PREFIX)-scheme $(LANGUAGES_DIR)/scheme/
.PHONY: languages-test-scheme
languages-test-scheme:
@echo "Testing Scheme implementation..."
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-scheme
.PHONY: languages-run-scheme
languages-run-scheme:
docker run -it --rm --name test-scheme $(IMAGE_PREFIX)-scheme
.PHONY: languages-stop-scheme
languages-stop-scheme:
docker stop test-scheme || true
docker rm test-scheme || true
.PHONY: languages-clean-scheme
languages-clean-scheme: languages-stop-scheme
docker rmi $(IMAGE_PREFIX)-scheme || true
# Build all language implementations
.PHONY: languages-build-all
languages-build-all:
@echo "Building all language implementations..."
@for lang in $(LANGUAGES); do \
echo "Building $$lang..."; \
docker build -t $(IMAGE_PREFIX)-$$lang languages/$$lang/ || echo "Failed to build $$lang"; \
done
# Test all language implementations
.PHONY: languages-test-all
languages-test-all:
@echo "Testing all language implementations..."
@for lang in $(LANGUAGES); do \
echo "Testing $$lang..."; \
docker run --rm $(IMAGE_PREFIX)-$$lang || echo "Failed to test $$lang"; \
done
# Stop all language containers
.PHONY: languages-stop-all
languages-stop-all:
@echo "Stopping all language containers..."
@for lang in $(LANGUAGES); do \
docker stop test-$$lang 2>/dev/null || true; \
docker rm test-$$lang 2>/dev/null || true; \
done
# Clean all language containers and images
.PHONY: languages-clean-all
languages-clean-all: languages-stop-all
@echo "Removing all language images..."
@for lang in $(LANGUAGES); do \
docker rmi $(IMAGE_PREFIX)-$$lang 2>/dev/null || true; \
done

372
README.rst Normal file
View file

@ -0,0 +1,372 @@
====================
uncloseai.
====================
A Public Domain AI Assistant for the Open Web
=============================================
.. image:: https://img.shields.io/badge/license-Public%20Domain-blue.svg
:target: https://creativecommons.org/publicdomain/zero/1.0/
:alt: Public Domain
.. image:: https://img.shields.io/badge/AI-Hermes%203-green.svg
:target: https://nousresearch.com/hermes3/
:alt: Powered by Hermes 3
Overview
--------
uncloseai. is a free, open-source AI assistant that can be embedded into any webpage with a single line of code. It provides users with an intelligent assistant powered by Hermes 3 LLM from Nous Research, offering features like page reading, translation, text-to-speech, and contextual Q&A.
**Key Features:**
* **Page-Aware AI Assistant** - Understands and discusses the content of the current webpage
* **Multi-Language Support** - Supports 19 languages with full UI translation
* **Text-to-Speech** - Convert any text or webpage content to natural speech
* **Smart Translation** - Translate entire webpages while preserving formatting and functionality
* **File Upload Support** - Process PDFs, text files, and documents
* **Privacy-First** - No tracking, no analytics, no user data collection
* **Public Domain** - Completely free to use, modify, and distribute
Quick Start
-----------
Add this single line to any webpage:
.. code-block:: html
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
That's it! Users will see a small robot icon (🤖) in the bottom-right corner that opens the AI assistant.
Repository
----------
The complete source code is available at: https://git.unturf.com/engineering/unturf/uncloseai.com
Project Structure
-----------------
The repository is organized with a ``public/`` directory containing all web-facing files:
.. code-block::
uncloseai.com/
├── public/ # Public web root (served via HTTPS)
│ ├── index.html # Main landing page
│ ├── demo.html # Interactive demo page
│ ├── uncloseai.js # Main entry point
│ ├── css/ # Stylesheets
│ ├── src/ # JavaScript modules
│ │ ├── chat.js # Core chat functionality
│ │ ├── config.js # Configuration and endpoints
│ │ ├── content.js # Content extraction utilities
│ │ ├── language-detection.js # Automatic language detection
│ │ ├── models.js # AI model management
│ │ ├── page-reader.js # Page reading functionality
│ │ ├── storage.js # LocalStorage management
│ │ ├── token_estimator.js # Token counting utilities
│ │ ├── translation.js # Translation system
│ │ ├── translate-modal.js # Translation UI
│ │ ├── tts.js # Text-to-speech system
│ │ ├── ui-themes.js # Theme management
│ │ ├── ui-translations.js # UI translation system
│ │ ├── uncloseai-embed-modal.js # Main modal UI
│ │ └── languages/ # 19 language translation files
│ │ ├── en.js (English), es.js (Spanish), zh.js (Chinese)
│ │ ├── ar.js (Arabic), bn.js (Bengali), ru.js (Russian)
│ │ ├── pt.js (Portuguese), ur.js (Urdu), id.js (Indonesian)
│ │ ├── de.js (German), ja.js (Japanese), sw.js (Swahili)
│ │ └── mr.js (Marathi), te.js (Telugu), tr.js (Turkish)
│ └── languages/ # 47 SDK implementations (browsable)
│ ├── python/ # Python examples (4 variants)
│ ├── javascript/ # JavaScript examples (4 variants)
│ ├── rust/, go/, java/ # Compiled language examples
│ ├── ruby/, php/, perl/ # Dynamic language examples
│ └── ... (43+ total languages)
├── book/ # Private book content (not served)
├── CLAUDE.md # Project documentation
├── .gitlab-ci.yml # CI/CD pipeline
└── package.json # Development dependencies
**Language SDK Directory**
Browse the complete collection of 47 language implementations at:
https://uncloseai.com/languages/
Each language directory contains:
* Working SDK code with streaming support
* Dockerfile for building and testing
* README with usage examples
* Environment-based configuration
Supported languages include Python, JavaScript, TypeScript, Rust, Go, Java, C, C++, Ruby, PHP, Kotlin, Swift, Elixir, Haskell, and 30+ more.
Core Technologies
-----------------
* **Frontend**: Vanilla JavaScript ES6+ modules
* **UI Framework**: PicoCSS (optional) with custom theming support
* **AI Model**: Hermes 3 Llama 3.1 8B (via VLLM endpoints)
* **Testing**: Jest with JSDOM
* **Code Quality**: Biome for formatting and linting
* **Markdown**: Marked.js for rendering
* **Syntax Highlighting**: Highlight.js
* **TTS**: OpenAI-compatible speech API
Key Features Explained
----------------------
Page-Aware Intelligence
~~~~~~~~~~~~~~~~~~~~~~~
The AI assistant automatically extracts and understands the content of the current webpage, allowing users to ask questions about what they're reading. It maintains context throughout the conversation.
Multi-Language Interface
~~~~~~~~~~~~~~~~~~~~~~~~
The entire UI is available in **19 languages** with complete translation coverage:
**Supported Languages:**
* **English** (en) - Default language
* **Spanish** (es) - Español
* **Chinese Simplified** (zh) - 中文(简体)
* **Chinese Traditional** (zh-tw) - 中文(繁體)
* **Hindi** (hi) - हिन्दी
* **French** (fr) - Français
* **Arabic** (ar) - العربية
* **Bengali** (bn) - বাংলা
* **Russian** (ru) - Русский
* **Portuguese** (pt) - Português
* **Urdu** (ur) - اردو
* **Indonesian** (id) - Bahasa Indonesia
* **German** (de) - Deutsch
* **Japanese** (ja) - 日本語
* **Swahili** (sw) - Kiswahili
* **Marathi** (mr) - मराठी
* **Telugu** (te) - తెలుగు
* **Turkish** (tr) - Türkçe
* **Korean** (ko) - 한국어
**Language Features:**
* **Manual Selection**: Users can manually select their preferred language from 19 options
* **Complete Coverage**: All UI elements, buttons, modals, and messages are translated
* **AI Response Language**: The AI responds in the user's selected language
* **Persistent Settings**: Language preference is saved in browser localStorage
* **No Page Refresh**: Language switching works instantly without reloading
**Translation Coverage Includes:**
* Modal titles and headers
* Button labels and tooltips
* Input placeholders and form labels
* System messages and notifications
* Error messages and alerts
* Settings and configuration options
* Help text and descriptions
Text-to-Speech Capabilities
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Two TTS modes are available:
1. **Direct TTS**: Converts text directly to speech
2. **Hermes-Enhanced TTS**: Uses AI to improve pronunciation and clarity
Smart Translation
~~~~~~~~~~~~~~~~~
The translation feature:
* Preserves HTML structure and formatting
* Maintains code blocks and technical content
* Opens translated pages in new tabs with full AI functionality
* Supports both full-page and custom text translation
Development Setup
-----------------
.. code-block:: bash
# Clone the repository
git clone https://git.unturf.com/engineering/unturf/uncloseai.com.git
cd uncloseai.com
# Install dependencies
npm install
# Run tests
npm test
# Format code
npx biome format --write .
# Lint code
npx biome check .
# Run both (recommended before commits)
npx biome format --write . && npx biome check .
Architecture
------------
uncloseai. uses a modular architecture:
1. **Entry Point** (``uncloseai.js``): Detects environment and initializes
2. **Modal System** (``uncloseai-embed-modal.js``): Main UI container
3. **Feature Modules**: Separate modules for chat, TTS, translation
4. **Storage Layer** (``storage.js``): Handles persistent conversation history
5. **Model Management** (``models.js``): Dynamic endpoint selection
6. **Theme System** (``ui-themes.js``): Adapts to page styling
API Endpoints
-------------
uncloseai. connects to these public endpoints:
* **Chat**: ``https://hermes.ai.unturf.com/v1`` (primary)
* **Chat**: ``https://hermes2.ai.unturf.com/v1`` (fallback)
* **TTS**: ``https://speech.ai.unturf.com/v1/audio/speech``
* **File Processing**: ``https://megaparce.ai.unturf.com/v1/file``
All endpoints are OpenAI-compatible.
Customization
-------------
Website owners can customize the assistant:
.. code-block:: javascript
// Set custom system message
window.UNCLOSEAI_SYSTEM_MESSAGE_APPEND = "You are helping users on an e-commerce site...";
// Disable custom styling (use pure PicoCSS)
window.UNCLOSEAI_CUSTOM_STYLING = false;
// Control initialization
window.UNCLOSEAI_SKIP_INIT = true;
Browser Support
---------------
* Chrome/Edge 80+
* Firefox 75+
* Safari 14+
* Mobile browsers with ES6 module support
Performance
-----------
* Initial load: ~50KB (gzipped)
* Lazy loads features as needed
* Caches model lists and translations
* Efficient token counting with upstream API
Privacy & Security
------------------
* **No tracking**: No analytics, cookies, or user tracking
* **No data storage**: Conversations stored only in browser localStorage
* **No authentication**: No user accounts or sign-ups required
* **Public endpoints**: All API endpoints are publicly accessible
* **Content Security**: Sanitizes HTML, escapes code blocks
Contributing
------------
This project is in the public domain. You're free to:
* Use it commercially or non-commercially
* Modify it without attribution
* Distribute it freely
* Create derivative works
We welcome contributions! Feel free to submit issues or pull requests.
Testing
-------
.. code-block:: bash
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run tests with coverage
npm test -- --coverage
The test suite covers:
* Modal creation and interaction
* Translation functionality
* TTS processing
* Storage operations
* UI theme detection
* Language system functionality
Production Deployment
---------------------
1. Serve files with proper MIME types
2. Enable CORS for module scripts
3. Use HTTPS (required for some features)
4. Set cache headers appropriately
Troubleshooting
---------------
**Modal not appearing:**
* Check browser console for errors
* Ensure JavaScript modules are supported
* Verify CORS headers are set correctly
**Translation/TTS not working:**
* Check network tab for API errors
* Ensure endpoints are accessible
* Verify API key is set (if using custom endpoints)
**Styling issues:**
* Set ``UNCLOSEAI_CUSTOM_STYLING = false`` for pure PicoCSS
* Check for CSS conflicts with page styles
License
-------
This project is released into the public domain under CC0 1.0 Universal.
See: https://creativecommons.org/publicdomain/zero/1.0/
About
-----
uncloseai. is part of the unturf. project, creating open tools for an open web. It's built by fxhp and the carnival hackers who believe AI should be accessible to everyone.
**Why "uncloseai."?**
The name represents keeping AI "unclosed" - open, accessible, and free for all to use and modify.
Links
-----
* **Homepage**: https://uncloseai.com
* **Repository**: https://git.unturf.com/engineering/unturf/uncloseai.com
* **Demo**: https://uncloseai.com/demo
* **Hermes Model**: https://nousresearch.com/hermes3/
Acknowledgments
---------------
* Nous Research for the amazing Hermes 3 model
* The open-source community for the libraries we build upon
* Everyone who believes in an open, accessible internet
Remember: This is your AI assistant. Make it yours!

60
biome.json Normal file
View file

@ -0,0 +1,60 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"files": {
"includes": [
"**",
"!**/css/pico.classless.min.css",
"!**/node_modules",
"!**/*.min.js",
"!**/*.min.css",
"!**/uncloseai-bundle.js"
]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noAssignInExpressions": "off"
},
"style": {
"useTemplate": "warn",
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
},
"complexity": {
"useOptionalChain": "warn"
}
}
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "tab",
"indentWidth": 2,
"lineWidth": 80,
"lineEnding": "lf",
"includes": ["**", "!**/css/pico.classless.min.css"]
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"javascript": {
"formatter": {
"quoteStyle": "double",
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "all",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSpacing": true,
"bracketSameLine": false
}
}
}

View file

@ -1,12 +0,0 @@
/*! Generated by Font Squirrel (https://www.fontsquirrel.com) on October 4, 2021 */
@font-face {
font-family: 'chunkfiveregular';
src: url('chunkfive-regular-webfont.woff2') format('woff2'),
url('chunkfive-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}

File diff suppressed because one or more lines are too long

View file

@ -1,79 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Demo Free LLM & Text To Speech Artificial Intelligence Service | ai.unturf.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<style>
body {
max-width: 960px;
margin: 0 auto;
}
</style>
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
<script defer data-domain="ai.unturf.com" src="https://analytics.unturf.com/js/plausible.js"></script>
</head>
<body>
<header>
<hgroup>
<a href="https://ai.unturf.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">unturf.</h1></a>
<p>Welcome to ai.unturf.com - Free LLM & Text To Speech Artificial Intelligence Service</title>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<main>
<h2>Demo!</h2>
<div id="chat-container">
<div id="chat-box"></div>
<div></div>
</div>
<div>
<input type="text" id="user-input" placeholder="Ask about this page...">
<button onclick="handleUserInput()">Send</button>
</div>
</main>
<footer>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
</footer>
</body>
</html>

868
docs/VAULT_SYNC_DESIGN.md Normal file
View file

@ -0,0 +1,868 @@
# Vault Sync Design Document
Cross-site settings synchronization via optional uncloseai.com account linking.
**Status:** Proposed
**Author:** Hermes Staff
**Date:** 2026-01-23
---
## Overview
### Current State
The UncloseAI widget uses a per-site encrypted vault stored in localStorage (`vault.js`). Each site embedding the widget has its own isolated vault:
- User sets up vault on site-a.com with password
- Settings encrypted with AES-256, stored in localStorage
- Session persistence (7-day TTL) for convenience
- No data leaves the user's browser
### Proposed Enhancement
Add optional cross-site sync via uncloseai.com accounts. Users who want their settings everywhere can link their vault to an account. Users who prefer local-only storage continue as before.
### Strategic Goal: Viral Spreading
Users who enjoy the widget on one site become advocates:
1. They want it on every site they visit
2. They recommend it to site owners
3. Site owners embed the widget to retain users
4. More users discover the widget
5. Cycle repeats
Cross-site sync is the catalyst that transforms one-site users into multi-site evangelists.
---
## User Flow
### Flow 1: First-Time User (Local Only)
```
1. User visits site-a.com (first time with widget)
2. Widget prompts: "Create a secure vault?"
3. User creates vault with password
4. Settings stored locally in localStorage
5. Widget works normally (local vault only)
```
### Flow 2: Local User Sees Sync Prompt
```
1. User has been using widget on site-a.com for a while
2. After N sessions OR manual settings access, show prompt:
"Sync your settings across all sites?"
[Link Account] [Not Now] [Don't Ask Again]
3. If "Not Now": dismiss, ask again after more sessions
4. If "Don't Ask Again": store preference, never ask again
5. If "Link Account": proceed to account linking
```
**Trigger conditions for sync prompt:**
- User has opened settings modal at least 3 times
- User has been using widget for at least 7 days
- User has not dismissed "Don't Ask Again"
- User does not already have a linked account
### Flow 3: Account Linking
```
1. User clicks "Link Account"
2. Modal shows options:
- "Sign in with email (magic link)"
- "Sign in with GitHub" (optional OAuth)
- "Sign in with Google" (optional OAuth)
3. User chooses email magic link
4. User enters email address
5. Email sent with one-time login link
6. User clicks link, opens uncloseai.com/auth/verify?token=xxx
7. Token validated, session cookie set
8. Redirect back to original site with auth token in URL fragment
9. Widget detects auth token, stores sync credentials
10. Widget uploads encrypted vault to server
11. Success message: "Settings will sync across all sites!"
```
### Flow 4: Returning User with Synced Account
```
1. User visits site-b.com (new site, same widget)
2. Widget detects no local vault
3. Widget checks for sync credentials (stored in localStorage)
4. If credentials exist:
a. Widget fetches encrypted vault from server
b. User prompted for vault password (still needed for decryption)
c. Settings decrypted locally, vault initialized
5. User has all their settings without manual setup
```
### Flow 5: Conflict Resolution
```
1. User has synced account, uses widget on site-a.com
2. User changes setting (e.g., switches model)
3. Widget uploads new encrypted vault to server
4. User simultaneously on site-b.com (second tab/device)
5. site-b.com widget has stale local copy
6. On next interaction, widget pulls from server
7. Conflict detected: local timestamp != server timestamp
8. Resolution: Last-write-wins (server version)
9. User sees toast: "Settings updated from another session"
```
---
## Technical Design
### Server-Side (uncloseai.com API)
#### Database Schema
```sql
-- Users table (for accounts)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
email_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- OAuth connections (optional)
CREATE TABLE oauth_connections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL, -- 'github', 'google'
provider_user_id VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(provider, provider_user_id)
);
-- Magic link tokens
CREATE TABLE magic_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL,
token_hash VARCHAR(64) NOT NULL, -- SHA-256 of token
expires_at TIMESTAMP NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Encrypted vaults (stores ciphertext only)
CREATE TABLE vaults (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
encrypted_data TEXT NOT NULL, -- AES-encrypted JSON blob
version INTEGER DEFAULT 1,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- API keys for widget authentication
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
key_hash VARCHAR(64) NOT NULL, -- SHA-256 of API key
name VARCHAR(100),
last_used_at TIMESTAMP,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
#### API Endpoints
```
POST /api/auth/magic-link
Request: { "email": "user@example.com" }
Response: { "success": true, "message": "Check your email" }
Action: Generate token, send email with link to /auth/verify?token=xxx
GET /api/auth/verify?token=xxx
Response: { "success": true, "api_key": "vk_xxx...", "user_id": "uuid" }
Action: Validate token, create/get user, generate API key
Notes: API key returned only once, user must save it
POST /api/auth/oauth/github (optional)
OAuth callback handler for GitHub
POST /api/auth/oauth/google (optional)
OAuth callback handler for Google
POST /api/vault/sync
Headers: Authorization: Bearer vk_xxx...
Request: { "encrypted_data": "...", "version": 2 }
Response: { "success": true, "version": 2, "updated_at": "..." }
Action: Store encrypted blob, increment version if changed
Notes: Server NEVER decrypts - stores ciphertext only
GET /api/vault/sync
Headers: Authorization: Bearer vk_xxx...
Response: { "encrypted_data": "...", "version": 2, "updated_at": "..." }
Action: Return encrypted blob
Notes: Returns 404 if no vault exists for this user
DELETE /api/vault/sync
Headers: Authorization: Bearer vk_xxx...
Response: { "success": true }
Action: Delete vault (account remains)
GET /api/vault/version
Headers: Authorization: Bearer vk_xxx...
Response: { "version": 2, "updated_at": "..." }
Action: Quick check for sync (lightweight polling)
DELETE /api/account
Headers: Authorization: Bearer vk_xxx...
Response: { "success": true }
Action: Delete account and all associated data
```
#### Rate Limiting
```
/api/auth/magic-link: 5 requests per email per hour
/api/vault/sync POST: 60 requests per minute per user
/api/vault/sync GET: 120 requests per minute per user
/api/vault/version: 300 requests per minute per user
```
### Client-Side (vault.js Extensions)
#### New Constants
```javascript
const UncloseVault = {
// ... existing constants ...
// Sync-related localStorage keys
SYNC_KEY: 'uncloseai_sync_credentials',
SYNC_PROMPT_KEY: 'uncloseai_sync_prompt_state',
// API base URL
SYNC_API_BASE: 'https://uncloseai.com/api',
// Sync check interval (5 minutes)
SYNC_POLL_INTERVAL_MS: 5 * 60 * 1000,
// ... rest of existing code ...
};
```
#### New Methods
```javascript
/**
* Check if sync is enabled and credentials exist
*/
isSyncEnabled() {
const creds = this.getSyncCredentials();
return creds !== null && creds.apiKey !== null;
}
/**
* Get stored sync credentials
*/
getSyncCredentials() {
try {
const json = localStorage.getItem(this.SYNC_KEY);
return json ? JSON.parse(json) : null;
} catch (e) {
return null;
}
}
/**
* Store sync credentials after successful auth
*/
setSyncCredentials(apiKey, userId) {
localStorage.setItem(this.SYNC_KEY, JSON.stringify({
apiKey,
userId,
linkedAt: new Date().toISOString()
}));
}
/**
* Clear sync credentials (unlink account)
*/
clearSyncCredentials() {
localStorage.removeItem(this.SYNC_KEY);
}
/**
* Push local vault to server
* Call after any local save() if sync is enabled
*/
async sync() {
if (!this.isSyncEnabled() || !this.isUnlocked()) {
return { success: false, error: 'Sync not available' };
}
const creds = this.getSyncCredentials();
const vault = this.getVault();
try {
const response = await fetch(`${this.SYNC_API_BASE}/vault/sync`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${creds.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
encrypted_data: vault.encrypted_settings,
version: vault.version || 1
})
});
if (!response.ok) {
if (response.status === 401) {
// API key invalid, clear credentials
this.clearSyncCredentials();
return { success: false, error: 'Session expired' };
}
return { success: false, error: 'Sync failed' };
}
const data = await response.json();
// Update local version
vault.version = data.version;
vault.synced_at = data.updated_at;
this.saveVault(vault);
return { success: true, version: data.version };
} catch (e) {
return { success: false, error: e.message };
}
}
/**
* Pull vault from server
* Returns encrypted data that still needs password to decrypt
*/
async pull() {
if (!this.isSyncEnabled()) {
return { success: false, error: 'Sync not enabled' };
}
const creds = this.getSyncCredentials();
try {
const response = await fetch(`${this.SYNC_API_BASE}/vault/sync`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${creds.apiKey}`
}
});
if (!response.ok) {
if (response.status === 404) {
return { success: true, data: null }; // No vault on server yet
}
if (response.status === 401) {
this.clearSyncCredentials();
return { success: false, error: 'Session expired' };
}
return { success: false, error: 'Pull failed' };
}
const data = await response.json();
return {
success: true,
data: {
encrypted_settings: data.encrypted_data,
version: data.version,
updated_at: data.updated_at
}
};
} catch (e) {
return { success: false, error: e.message };
}
}
/**
* Check if server has newer version
*/
async checkForUpdates() {
if (!this.isSyncEnabled()) return false;
const creds = this.getSyncCredentials();
const vault = this.getVault();
try {
const response = await fetch(`${this.SYNC_API_BASE}/vault/version`, {
headers: { 'Authorization': `Bearer ${creds.apiKey}` }
});
if (!response.ok) return false;
const data = await response.json();
return data.version > (vault?.version || 0);
} catch (e) {
return false;
}
}
/**
* Merge server vault with local (last-write-wins)
*/
async syncFromServer(password) {
const result = await this.pull();
if (!result.success || !result.data) {
return result;
}
// Try to decrypt with provided password
const decrypted = this.decrypt(result.data.encrypted_settings, password);
if (!decrypted) {
return { success: false, error: 'Wrong password' };
}
// Replace local vault with server version
const vault = {
encrypted_settings: result.data.encrypted_settings,
version: result.data.version,
synced_at: result.data.updated_at,
updated_at: new Date().toISOString()
};
this.saveVault(vault);
this._settings = decrypted;
this._password = password;
window.dispatchEvent(new CustomEvent('uncloseai-vault-synced'));
return { success: true };
}
```
#### Modified save() Method
```javascript
/**
* Save current settings to vault (modified for sync)
*/
async save() {
if (!this.isUnlocked()) {
console.error('Cannot save: vault not unlocked');
return false;
}
const vault = this.getVault() || {};
const encrypted = this.encrypt(this._settings, this._password);
if (!encrypted) {
console.error('Failed to encrypt settings');
return false;
}
vault.encrypted_settings = encrypted;
vault.updated_at = new Date().toISOString();
vault.version = (vault.version || 0) + 1;
this.saveVault(vault);
// Auto-sync if enabled (non-blocking)
if (this.isSyncEnabled()) {
this.sync().catch(e => console.warn('Sync failed:', e));
}
return true;
}
```
---
## Privacy and Zero-Knowledge Architecture
### Core Principle
**The server NEVER has the ability to decrypt user data.**
```
User's Browser uncloseai.com Server
| |
| 1. User enters password |
| (never leaves browser) |
| |
| 2. Settings encrypted with |
| password using AES-256 |
| |
| 3. Encrypted blob sent to server |
| POST /vault/sync |
|----------------------------------->|
| | 4. Server stores
| | encrypted blob
| | (ciphertext only)
| |
| 5. On new device, encrypted |
| blob retrieved |
|<-----------------------------------|
| |
| 6. User enters password |
| (decryption happens locally) |
| |
```
### What the Server Stores
```
{
"user_id": "uuid",
"encrypted_data": "U2FsdGVkX1+...(opaque ciphertext)...",
"version": 3,
"updated_at": "2026-01-23T10:00:00Z"
}
```
### What the Server CANNOT See
- User's vault password
- Decrypted settings (API keys, model preferences, etc.)
- Any plaintext user data
### Implications
1. **Password recovery is impossible** - If user forgets password, their synced vault is permanently inaccessible. This is a feature, not a bug.
2. **No password reset** - Account password reset would require vault re-encryption, which requires the old password.
3. **User responsibility** - Users must remember their vault password. We provide clear warnings during setup.
---
## Viral Mechanics
### "Powered by UncloseAI" Badge
Optional badge shown in widget corner:
```
[ Settings ] [Voice] [Model] Powered by UncloseAI
```
Badge behavior:
- Shown only if site owner enables it (opt-in for site owners)
- Or shown if user has free account (non-paying users get badge)
- Clicking opens uncloseai.com in new tab
- Badge can be hidden by site owners who pay/self-host
### "Get This Widget" Link
In settings modal footer:
```
------------------------------------------
| Want this AI on your website? |
| [Add to Your Site] - Free, 2 min setup |
------------------------------------------
```
Link goes to: `uncloseai.com/embed?ref={site_domain}`
Referral tracking:
- Store referring domain in analytics
- Site owner gets credit when their users convert
- Future: referral rewards (free premium features)
### Share Settings Link
Users can share their vault (encrypted) via URL:
```javascript
async generateShareLink() {
if (!this.isUnlocked()) return null;
// Create one-time export
const exportData = {
settings: this._settings,
exported_at: new Date().toISOString(),
// Exclude sensitive fields
exclude: ['customAPIKey', 'unsandboxSecretKey']
};
const filtered = { ...exportData.settings };
exportData.exclude.forEach(k => delete filtered[k]);
// Encrypt with random key
const shareKey = CryptoJS.lib.WordArray.random(16).toString();
const encrypted = this.encrypt(filtered, shareKey);
// Encode in URL
const payload = btoa(JSON.stringify({ e: encrypted }));
return `https://uncloseai.com/share#${shareKey}:${payload}`;
}
```
Recipient flow:
1. Opens share link
2. Page decrypts settings from URL fragment (client-side only)
3. Shows preview of shared settings
4. User can import into their own vault
### Referral Program (Future)
Site owner rewards:
- Each site owner gets referral code
- When new site embeds widget via referral, both get benefits
- Tiered rewards: 5 referrals = X, 10 referrals = Y
User rewards:
- Users who share widget and drive signups get:
- Extended session duration
- Priority model access
- Early access to new features
---
## MVP vs Full Version
### MVP (Phase 1)
**Goal:** Prove the sync concept works and users want it.
**Scope:**
- Email magic link authentication only
- Basic vault sync (push/pull)
- Last-write-wins conflict resolution
- "Powered by UncloseAI" badge
- "Get This Widget" link
**Not in MVP:**
- OAuth (GitHub, Google)
- Share settings link
- Referral program
- Analytics for site owners
- Paid tiers
**Timeline:** 2-3 weeks
**Success metrics:**
- 100+ accounts created in first month
- 20+ cross-site sync users (used on 2+ domains)
- 10+ new site embeds from "Get This Widget"
### Full Version (Phase 2)
**Goal:** Scale viral mechanics and add monetization.
**Scope:**
- OAuth providers (GitHub, Google)
- Share settings link with preview
- Referral program with tracking
- Site owner analytics dashboard
- Paid tiers (remove badge, priority support)
- Real-time sync (WebSocket)
- Multi-device session management
**Timeline:** 2-3 months after MVP validation
---
## Security Considerations
### End-to-End Encryption
- All encryption/decryption happens in browser using CryptoJS
- Server stores only ciphertext
- No server-side key escrow
- No ability to decrypt without user's password
### Password Requirements
- Minimum 8 characters (enforced client-side)
- Encourage but don't require complexity
- Show strength meter in UI
- Clear warning: "This password cannot be recovered"
### Account Recovery
**Scenario: User forgets vault password**
Options:
1. **Delete and restart** - User can delete their synced vault and create new one. Loses all settings.
2. **Local fallback** - If local vault still works (different password), offer to re-sync from local.
3. **No recovery** - By design. Zero-knowledge means no backdoor.
UI messaging:
```
Forgot your vault password?
Your vault is encrypted and cannot be recovered without your password.
This is a security feature, not a bug.
Options:
[ Delete Vault & Start Fresh ] - Lose all synced settings
[ Cancel ] - Try to remember your password
```
### Rate Limiting
Prevent brute force and abuse:
```
Endpoint Limit
------------------------------------------
POST /auth/magic-link 5/hour/email
POST /vault/sync 60/minute/user
GET /vault/sync 120/minute/user
GET /vault/version 300/minute/user
POST /auth/verify 10/minute/IP
```
### API Key Security
- API keys prefixed with `vk_` for easy identification
- Keys are 32 bytes of random data (256 bits)
- Stored hashed (SHA-256) in database
- Keys can be revoked from account settings
- Keys have optional expiration
### CORS Policy
```
Access-Control-Allow-Origin: *
```
Widget runs on any domain, so CORS must be permissive. Security comes from:
- API key authentication
- Rate limiting
- E2E encryption (server can't read data anyway)
### Magic Link Security
- Tokens are 32 bytes of random data
- Tokens expire after 15 minutes
- Tokens are single-use
- Token hash stored in database (not plaintext)
- Email delivery via trusted provider (SendGrid, Postmark)
---
## Implementation Checklist
### Server-Side
```
[ ] Database schema (PostgreSQL)
[ ] User model and migrations
[ ] Magic link generation and verification
[ ] Email sending integration (SendGrid)
[ ] Vault CRUD endpoints
[ ] API key generation and validation
[ ] Rate limiting middleware
[ ] CORS configuration
[ ] Error handling and logging
[ ] Health check endpoint
```
### Client-Side
```
[ ] Sync credentials storage
[ ] sync() method
[ ] pull() method
[ ] checkForUpdates() method
[ ] Modified save() with auto-sync
[ ] Auth flow UI (modal)
[ ] "Link Account" prompt logic
[ ] "Powered by UncloseAI" badge
[ ] "Get This Widget" link
[ ] Sync status indicator
[ ] Error handling and retry logic
[ ] Offline queue for sync operations
```
### Testing
```
[ ] Unit tests for sync methods
[ ] Integration tests with mock server
[ ] E2E test: full account linking flow
[ ] E2E test: cross-site sync
[ ] E2E test: conflict resolution
[ ] Security audit: encryption verification
[ ] Load testing: rate limits
```
### Documentation
```
[ ] User guide: setting up sync
[ ] User guide: what happens if I forget my password
[ ] Site owner guide: embedding options
[ ] API documentation (for advanced users)
[ ] Privacy policy updates
```
---
## Open Questions
1. **Should vault password be separate from account password?**
- Current design: Yes, vault has its own password
- Alternative: Use account password for vault (simpler UX, less secure)
2. **How to handle vault password change?**
- Need to re-encrypt and sync new ciphertext
- What if user is on multiple devices during password change?
3. **Should we support multiple vaults per account?**
- Use case: Different settings for work vs personal
- Complexity: Significant, defer to Phase 2
4. **Real-time sync vs polling?**
- MVP: Polling (simpler)
- Phase 2: WebSocket for instant sync
5. **What settings should be excluded from sync?**
- Definitely exclude: Nothing by default
- User choice: Let users select what syncs
---
## Appendix: Email Templates
### Magic Link Email
```
Subject: Sign in to UncloseAI
Hi there,
Click the link below to sign in to your UncloseAI account:
[Sign In] - https://uncloseai.com/auth/verify?token=xxx
This link expires in 15 minutes and can only be used once.
If you didn't request this, you can safely ignore this email.
- The UncloseAI Team
```
### Welcome Email (After First Sync)
```
Subject: Your settings are now synced!
Welcome to UncloseAI!
Your vault is now synced across all sites. Here's what you can do:
1. Visit any website with the UncloseAI widget
2. Click "Sync Settings" in the settings menu
3. Enter your vault password
4. Your preferences load automatically!
Remember: Your vault password is never sent to our servers.
If you forget it, your synced data cannot be recovered.
Need help? Reply to this email.
- The UncloseAI Team
```

View file

@ -0,0 +1,52 @@
# 001: Extension fails to load on certain pages
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** high
**Status:** fixed
**Affects:** Browser extension (Chrome/Safari)
## Description
The uncloseai browser extension does not load at all on some pages. No UI appears, no sidebar, nothing.
## Reproduction
**Fails to load on:**
- reddit.com (any page)
- slashdot.org
- cnn.com (main/index page only)
**Works on:**
- cnn.com article pages (individual stories)
## Analysis
The pattern suggests Content Security Policy (CSP) or iframe sandboxing on these domains is blocking the extension from injecting. Reddit and Slashdot both use strict CSPs. CNN's main page likely has different CSP headers than article pages.
Possible causes:
- CSP `script-src` blocking inline or extension scripts
- CSP `frame-src` or `frame-ancestors` blocking extension iframe/overlay
- Shadow DOM or aggressive DOM manipulation interfering with injection
- Extension manifest permissions not covering these domains
## Root Cause
The browser-toys extension injects `<script src="https://uncloseai.com/uncloseai.js" type="module">` via DOM. Pages with strict CSP `script-src` directives block this because `https://uncloseai.com` is not in their allowlist.
## Fix
Implemented three-tier injection strategy in `uncloseai-browser-toys`:
1. **Strategy 1 (CSP-safe):** Content script messages the background service worker, which calls `chrome.scripting.executeScript({ world: "MAIN", files: ["uncloseai-bundle.js"] })`. This injects directly into the page world via the browser engine, bypassing all page CSP restrictions. Requires `"scripting"` permission (added to Chrome and Safari MV3 manifests).
2. **Strategy 2 (script tag):** Fallback for MV2 browsers (Firefox). Creates a `<script>` tag with `src` pointing to the extension-origin bundle. Works on most pages since browsers whitelist extension origins in CSP.
3. **Strategy 3 (CDN bootstrap):** Last resort if extension resources fail. Inlines a bootstrap that loads from `uncloseai.com/uncloseai.js`.
**Files changed in browser-toys:**
- `shared/content.js`: Rewrote injection to try background message first, then script tag, then CDN
- `shared/background.js`: Added `onMessage` listener for `{ action: "inject" }` using `chrome.scripting.executeScript`
- `extensions/chrome/manifest.json`: Added `"scripting"` permission
- `extensions/safari/manifest.json`: Added `"scripting"` permission
- `extensions/firefox/manifest.json`: No changes needed (MV2, uses Strategy 2)

View file

@ -0,0 +1,31 @@
# 002: TTS stops after first sentence
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** high
**Status:** open
**Affects:** TTS playback, mobile and desktop
## Description
Text-to-speech only reads the first sentence of a response, then stops. Sometimes reads two sentences. Happens on both mobile (iPhone/Safari) and desktop (macOS/Safari).
## Reproduction
1. Send a message that generates a multi-sentence response
2. Tap the TTS/speaker icon on the message
3. TTS plays the first sentence (occasionally two)
4. Audio stops, remaining text is never spoken
## Analysis
Possible causes:
- TTS input is being truncated before sending to the speech endpoint
- The TTS API has a character/token limit and the client is not chunking the text
- Only the first SSE chunk's content is being sent to TTS instead of the full accumulated response
- Audio playback queue is not chaining subsequent audio segments
- Browser autoplay policy may be blocking follow-up audio segments (especially on mobile Safari)
## Notes
If the TTS API has a max input length, the client needs to split text into chunks, generate audio for each, and queue playback sequentially.

View file

@ -0,0 +1,32 @@
# 003: Audio icon freezes entire window
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** critical
**Status:** open
**Affects:** iPhone (Safari, latest iOS), macOS (Safari, latest macOS)
## Description
Tapping the audio icon in the bottom bar (the global one, not the per-message TTS icon) causes the entire uncloseai window to freeze. Nothing responds after this: buttons, scrolling, even the close (X) button. Complete UI lockup.
## Reproduction
1. Open uncloseai in Safari (iPhone or macOS)
2. Have a conversation with at least one response visible
3. Tap the audio icon in the bottom navigation bar
4. Window freezes completely
5. Cannot interact with anything, cannot close the panel
## Analysis
This is a main-thread blocking issue. Possible causes:
- Synchronous API call on the main thread (blocking fetch or XHR without async)
- Infinite loop in audio initialization or playback setup
- Unresolved promise or callback deadlock
- AudioContext creation failing silently and triggering a retry loop
- Safari-specific Web Audio API quirk (Safari requires user gesture to resume AudioContext, failure to handle this could cause a hang)
## Notes
This is the highest priority issue: a complete UI freeze with no recovery path is a showstopper. The user has to force-close the tab/app. Needs investigation into what the bottom-bar audio icon's click handler does differently from the per-message TTS icon.

View file

@ -0,0 +1,44 @@
# 004: Add working/spinning indicators for async operations
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** medium
**Status:** open
**Affects:** All platforms, UX
## Description
Icons that trigger async operations (TTS generation, page reading, etc.) give no visual feedback that work is in progress. The user taps and nothing visibly happens until the operation completes (or fails silently).
The book icon for "page read" already has an hourglass indicator. Other icons need the same treatment.
## Requirements
- TTS icon (per-message): show spinning/pulsing animation while audio is being generated
- Audio icon (bottom bar): show spinning/pulsing animation while audio is loading
- Any other async operation icons should show a working state
- Animation should stop when the operation completes or errors
- On error, briefly show an error state (red flash, shake, etc.) before returning to idle
## Design Notes
Keep consistent with the existing hourglass pattern on the book icon. Options:
- CSS spin animation on the icon itself
- Small spinner overlay on the icon
- Icon swap to an animated version during loading
- Pulsing opacity animation (subtle but effective)
## Implementation
CSS animations added to both theme files (`uncloseai-modal-builtin.css`, `uncloseai-modal-pico.css`):
- `.working` class: pulse animation (opacity 1 to 0.4), disables pointer events
- `.uncloseai-spinner` class: continuous rotation for inline spinner elements
- Apply to `.uncloseai-btn-small` or `.uncloseai-btn-primary` elements
Usage: `btn.classList.add("working")` when async starts, `.remove("working")` when done.
TTS buttons (primary consumers) are currently hidden via feature flag. When TTS is re-enabled, wire the `.working` class into the TTS button click handlers.
## Notes
CSS is ready. JS wiring deferred until TTS is re-enabled (tickets #002, #003).

View file

@ -0,0 +1,52 @@
# 005: Context overflow causes blank or hallucinated responses
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** high
**Status:** open
**Affects:** All platforms, chat quality
## Description
When the context window fills up (long pages injected as context), the model either:
1. Returns completely blank responses (silent failure, nothing displayed)
2. Starts hallucinating about unrelated content, specifically talking about the uncloseai.com page itself or the Nous Research page
## Reproduction
1. Navigate to a very long page (e.g., Wikipedia's Napoleon article)
2. Use the page-read feature to inject the page content as context
3. Ask a question about the page
4. Response is either blank or talks about uncloseai/Nous Research instead of the page content
## Analysis
The page content is likely exceeding the model's `max_model_len` (context window). When this happens:
- The API may return an error that the client silently swallows (blank response)
- The context may be truncated in a way that drops the user's page content but keeps the system prompt and uncloseai UI text, causing the model to talk about those instead
- The Nous Research hallucination suggests the model's training data is bleeding through when it has no useful context to work with
Possible fixes:
- Check page content length before injection, truncate intelligently if it exceeds the model's `max_model_len` (available from `/v1/models` discovery)
- Show a user-facing warning when context is too long: "This page is too long, content has been trimmed"
- Implement chunked summarization for long pages (summarize in sections, then inject summaries)
- Never silently swallow API errors: if the completion fails, show the error to the user
- Prioritize user message and page content over system prompt when truncating
## Fix Implemented
Smart content collapsing in `content.js` via `fitPageContent()`. Progressive stages run in order of aggressiveness, stopping when content fits:
1. **Deduplicate paragraphs**: remove exact duplicate blocks (common in scraped DOM)
2. **Collapse nav links**: 5+ consecutive links become `[Navigation: N links including ...]`
3. **Collapse references**: dense URL/citation clusters become `[... N references collapsed]`
4. **Collapse lists**: 7+ item lists keep first 5, note `[... N more items]`
5. **Collapse code blocks**: 12+ line blocks keep first 10, note `[... N more lines]`
6. **Collapse sidebar debris**: runs of 6+ short fragments get compressed
7. **Hard truncate**: last resort, cuts at paragraph/sentence boundary
No summarization. All content stays real, just structurally compressed. Uses `max_model_len` from model discovery with 15k token overhead reserved for system prompt, computed intel, chat history, and output.
## Notes
Hermes bumped to 82k context. Wikipedia Napoleon (~67k tokens) should fit after collapsing without hard truncation.

View file

@ -0,0 +1,47 @@
# 006: Reverse RAG: cross-page context via encrypted localStorage
**Reporter:** fxhp
**Date:** 2026-03-03
**Priority:** medium
**Status:** fixed
**Affects:** Browser extension, embedded widget
## Description
When a user browses multiple pages on the same site, the machine learning model should be able to reference content from previously visited pages. Currently each page is isolated: the model only knows the current page's content.
## Goal
Build a reverse RAG system where page summaries are stored in encrypted localStorage (per-domain), and automatically included in the system prompt for new pages. This gives the model a growing knowledge base about the site as the user browses.
## Architecture
1. After generating the intro message for a page, extract a brief summary (title, key topics, 2-3 sentences)
2. Store summaries per-domain in vault-encrypted localStorage: `uncloseai-pageknowledge-{domain}`
3. On new pages, include the last N page summaries in the system prompt as context
4. Summaries should be compact (under 200 tokens each) to avoid context overflow
5. Use the existing `fitPageContent()` budget system to allocate space for cross-page context
## Privacy
All cross-page data must be encrypted via the vault, same as chat history. The user controls their data. No data leaves the device except through explicit LLM requests.
## Dependencies
- Site journey tracking (implemented in storage.js)
- Vault encryption (already working)
- `fitPageContent()` context budgeting (already working)
## Notes
This is the foundation for making uncloseai "unstoppable" on any site. The model gets smarter the more pages you visit, building a personal knowledge graph of the site, all stored locally and encrypted.
## Resolution
Page-summary storage (per-domain, vault-encrypted via `savePageSummary` /
`loadPageSummaries` in `storage.js`) and prompt backfill landed per the
architecture above. Step 3's "include the last N page summaries" was
later sharpened from recency selection to BM25 relevance ranking against
the current page: see #009 Resolution (commit `2834e99`). Summaries now
included are the ones *similar* to the page in view, not just the most
recent.

View file

@ -0,0 +1,32 @@
# 007: Journey-aware conversation persistence across pages
**Reporter:** fxhp
**Date:** 2026-03-03
**Priority:** medium
**Status:** open
**Affects:** Browser extension, embedded widget
## Description
Currently each page URL gets its own isolated conversation history. When a user navigates to a new page, their previous conversation is gone. The model should be able to reference earlier conversations from the same browsing session.
## Goal
Allow the model to optionally carry forward conversation context when the user navigates between pages on the same site. Not full history transfer (too expensive), but a compressed summary of what was discussed.
## Architecture
1. When the user leaves a page (or on conversation save), generate a 1-2 sentence conversation summary
2. Store summaries per-page in the site journey data (already tracked)
3. Include recent conversation summaries in the system prompt for new pages
4. Budget: allocate ~500 tokens max for cross-page conversation context
## Example
User on page A asks "What does this function do?" and gets an explanation.
User navigates to page B. The model knows: "On the previous page, you were asking about the fetchData function and how it handles errors."
## Dependencies
- Site journey tracking (implemented)
- Reverse RAG page knowledge (#006)

View file

@ -0,0 +1,29 @@
# 008: Site knowledge graph from browsing patterns
**Reporter:** fxhp
**Date:** 2026-03-03
**Priority:** low
**Status:** open
**Affects:** Browser extension, embedded widget
## Description
As the user browses a site, build a lightweight knowledge graph of the site's structure: how pages link to each other, what topics each page covers, and what the user has explored vs. what they haven't.
## Goal
The model should understand the site's topology, not just the current page. It can suggest related pages, note when the user has visited related content, and act as a true site guide.
## Architecture
1. Extract internal links from each visited page
2. Map page topics (from page-intelligence.js computed metrics)
3. Build a link graph: `{ page_url: { title, topics, links_to: [urls], visited: bool } }`
4. Store encrypted per-domain
5. Include relevant graph context in system prompt (pages that link to/from current page)
## Dependencies
- Reverse RAG (#006)
- Journey tracking (implemented)
- Page intelligence (implemented)

View file

@ -0,0 +1,66 @@
# 009: Smart context backfill from indexed pages
**Reporter:** fxhp
**Date:** 2026-03-03
**Priority:** medium
**Status:** fixed
**Affects:** Browser extension, embedded widget
## Description
When the user opens the chatbot on a page and there is room left in the context window, backfill with content from previously indexed pages on the same site. "Indexed" means any page where the chatbot was opened and page intelligence was computed.
## Goal
Use spare context budget to pull in relevant cross-page knowledge automatically. If the user asks a question about something on a different page they already visited, the model can answer without the user navigating back.
## Architecture
1. After computing page content and journey context, calculate remaining context budget
2. If remaining budget > 2000 tokens, pull from the page knowledge store (#006)
3. Rank stored pages by relevance (keyword overlap with current page, recency)
4. Pack as many page summaries as fit within the remaining budget
5. Include in system prompt as "INDEXED SITE KNOWLEDGE" section
## Budget Calculation
```
total = model max tokens (e.g. 82000)
used = system prompt + page content + journey context + overhead
spare = total - used
backfill = min(spare * 0.5, 10000) // never use more than half of spare
```
## Dependencies
- Reverse RAG page knowledge (#006)
- Journey tracking (implemented)
- fitPageContent() budget system (implemented)
## Resolution (2026-05-22)
Backfill shipped in two stages.
**Stage 1 (initial):** spare budget (30% of `maxTokens - usedEstimate`,
gated at `> 500` tokens) packed with indexed page summaries, sorted by
recency only. This satisfied steps 1, 2, 4, 5 but **not step 3**: the
ranking. `getRelevantPageSummaries()` carried a docstring claiming
"keyword overlap with current page" but the code only sorted by
`indexedAt`, so the prompt was backfilled with recently-visited pages
regardless of relevance.
**Stage 2 (commit `2834e99`):** implemented step 3 with BM25 relevance
ranking in `storage.js`:
- Query = current page (`document.title` + first 2000 chars of content),
passed from the call site in `uncloseai-embed-modal.js`.
- Each candidate summary (title + topics + summary) is tokenized
(lowercased, stopwords + single chars dropped) and scored with BM25
(`k1=1.5`, `b=0.75`, IDF over the candidate corpus).
- Only summaries scoring `>= 2.0` are kept, best-first, then packed to
budget. Verified separation: related docs score 5-8, unrelated 0.
- Recency ordering retained as a fallback when no query is supplied
(backward compatible).
Net effect: cross-page context now pulls *similar* pages, not merely
recent ones.

View file

@ -0,0 +1,45 @@
# 010: Ethical crawl mode for site indexing ($14/m plan)
**Reporter:** fxhp
**Date:** 2026-03-03
**Priority:** medium
**Status:** open
**Affects:** Browser extension (paid tier)
## Description
Paid feature ($14/m) that crawls a site following robots.txt rules, indexing pages for keyword search and cross-page question answering. All data stored in encrypted localStorage on the user's device.
## Goal
Users can index an entire site (or section) without manually opening the chatbot on every page. Then ask questions across all indexed pages like "which page talks about authentication?" or "find all mentions of pricing."
## Architecture
1. User triggers crawl from the chatbot UI (new "Index Site" button in settings)
2. Extension fetches robots.txt, parses disallow/allow rules
3. Starts from current page, follows internal links breadth-first
4. For each page: extract content, compute page intelligence, store summary
5. All data encrypted in vault localStorage
6. Respect: rate limiting (1 req/sec), max depth, max pages, robots.txt
7. Progress UI shows pages indexed, estimated coverage
## Ethics
- Strictly follows robots.txt (no crawl if disallowed)
- Rate limited to 1 request per second minimum
- User-initiated only (never auto-crawl)
- All data stays on the user's device
- Clear disclosure: "This indexes pages for your private use only"
## Monetization
- Free tier: manual page-by-page indexing (open chatbot on each page)
- $14/m plan: automated crawl mode + higher page limits + priority support
- Payment via unsandbox.com accounts
## Dependencies
- Reverse RAG page knowledge (#006)
- Vault encryption (implemented)
- unsandbox.com billing integration

View file

@ -0,0 +1,54 @@
# 011: Vault viewer: indexed pages, charts, and engagement metrics
**Reporter:** fxhp
**Date:** 2026-03-03
**Priority:** medium
**Status:** open
**Affects:** Browser extension, embedded widget
## Description
A new UI panel in the chatbot showing what's stored in the user's encrypted vault for this site. Charts, metrics, and visualizations that make the user feel like they're getting value just from browsing with the chatbot open.
## Goal
Show the user their browsing intelligence: what pages they've visited, what's been indexed, coverage metrics, and natural engagement data. Not gamified, but genuinely useful metrics that emerge from organic browsing.
## Features
### Pages View
- List of all indexed pages on this domain
- Title, URL, last visited, number of conversations
- Search/filter across indexed pages
- Click to navigate to a page
### Coverage Metrics (natural, not gamified)
- Pages visited vs total internal links discovered
- Topics covered (from page-intelligence.js topic extraction)
- Content consumed: total word count read, estimated reading time
- Questions asked and answered per topic area
- How much of the site's content has been discussed
### Charts
- Timeline of browsing activity on this site
- Topic distribution (pie/bar chart of content categories)
- Engagement depth (pages visited over time)
- Knowledge growth curve (indexed content accumulation)
### Privacy Controls
- Clear indexed data for this site
- Export vault data (encrypted JSON download)
- View storage usage
## Architecture
- New panel accessible from chatbot settings/menu
- Reads from vault localStorage (journey data, page knowledge)
- Charts rendered with lightweight library (no heavy dependencies)
- All computation client-side, no server calls
## Dependencies
- Reverse RAG (#006)
- Journey tracking (implemented)
- Page intelligence (implemented)

View file

@ -0,0 +1,52 @@
# 012: Opt-in chat sharing via unfirehose
**Reporter:** fxhp
**Date:** 2026-03-03
**Priority:** low
**Status:** open
**Affects:** Browser extension, embedded widget
## Description
Allow users to optionally share their chat conversations publicly via unfirehose.org for machine learning training data. All chats are private by default. Sharing is always opt-in, per-conversation, with clear disclosure.
## Goal
Build the ethical pipeline for users to contribute their browsing intelligence and conversations to the public commons, creating training data that improves the models for everyone. Following the unfirehose philosophy: "Pay for privacy, not for access."
## Architecture
1. Per-conversation "Share" button in chat UI
2. On share: decrypt conversation from vault, push to unfirehose.org API
3. Access levels (from unfirehose):
- **public**: Visible in global feed, permanent archive, free
- **unlisted**: Direct link only, not in feeds, paid
- **private**: Default, never leaves device
4. Auth via unsandbox.com SSO (shared identity with unfirehose)
5. API endpoint: `POST https://api.unfirehose.org/v1/ingest`
6. Data format: JSONL with X-Source: "uncloseai" header
## Privacy
- **All chats private by default.** Period.
- Sharing is per-conversation, not global toggle
- Clear preview of what will be shared before sending
- Revocation: user can delete shared conversations
- No metadata leakage: strip device info, IP, vault keys
- GDPR compliant: right to deletion, data portability
## User Flow
1. User has a conversation they want to share
2. Clicks "Share" button on conversation
3. Sees preview: "This will make this conversation publicly visible"
4. Chooses access level (public/unlisted)
5. Authenticates with unsandbox.com account
6. Conversation pushed to unfirehose.org
7. Gets shareable link back
## Dependencies
- unfirehose.org API (see ~/git/unfirehose)
- unsandbox.com accounts/billing
- Vault encryption (implemented)

View file

@ -0,0 +1,25 @@
# 013: Page styles override chatbot button design and fonts
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** high
**Status:** open
**Affects:** Embedded widget on all pages
## Description
Host page CSS leaks into the uncloseai chatbot widget, overriding button designs and font styles. Most visible: white text in the input box on pages with dark-theme or custom text color CSS (e.g. Google Docs).
## Reproduction
1. Open uncloseai chatbot on Google Docs
2. Note the input box has white/invisible text
3. Page CSS `color: white` or similar rules bleed through to chatbot elements
## Root Cause
The chatbot dialog and its children inherit CSS from the host page. No CSS isolation barrier exists between the page and the chatbot.
## Fix
Add CSS reset (`all: initial` / `all: revert`) on the chatbot dialog root, then re-apply all chatbot styles explicitly. Ensure input, buttons, and text elements have explicit color/background rules that cannot be overridden by page styles.

23
docs/tickets/index.md Normal file
View file

@ -0,0 +1,23 @@
# uncloseai.com Tickets
## Open
| ID | Title | Priority | Status |
|----|-------|----------|--------|
| 002 | [TTS stops after first sentence](002-tts-stops-after-first-sentence.md) | high | deferred (TTS feature-flagged off) |
| 003 | [Audio icon freezes entire window](003-audio-icon-freezes-window.md) | critical | deferred (TTS feature-flagged off) |
## Fixed
| ID | Title | Priority | Fix |
|----|-------|----------|-----|
| 001 | [Extension fails to load (CSP)](001-extension-fails-to-load.md) | high | Three-tier injection via chrome.scripting.executeScript |
| 004 | [Working/spinning indicators](004-working-indicators.md) | medium | .working class wired to send button during async ops |
| 005 | [Context overflow silent failure](005-context-overflow-silent-failure.md) | high | Smart content collapsing in fitPageContent() |
| 006 | [Reverse RAG: cross-page context](006-reverse-rag-cross-page-context.md) | medium | Page summaries in encrypted localStorage, BM25-ranked backfill into prompts |
| 007 | [Journey-aware conversation persistence](007-journey-aware-conversation-persistence.md) | medium | Conversation summaries stored in journey data per-page |
| 008 | [Site knowledge graph](008-site-knowledge-graph.md) | low | Internal link extraction + adjacency graph in vault |
| 009 | [Smart context backfill](009-smart-context-backfill.md) | medium | Spare context budget fills with BM25-ranked indexed page summaries |
| 010 | [Ethical crawl mode](010-ethical-crawl-mode.md) | medium | robots.txt parser, BFS crawler, rate-limited, API key gated |
| 011 | [Vault viewer dashboard](011-vault-viewer-dashboard.md) | medium | Site intelligence panel with metrics, topics, indexed pages list |
| 012 | [Unfirehose opt-in sharing](012-unfirehose-opt-in-sharing.md) | low | Share button with access level selector, JSONL ingest to unfirehose.org |

View file

@ -1,381 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Using Free LLM & Text To Speech Artificial Intelligence Service | ai.unturf.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<style>
body {
max-width: 960px;
margin: 0 auto;
}
</style>
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
<script defer data-domain="ai.unturf.com" src="https://analytics.unturf.com/js/plausible.js"></script>
</head>
<body>
<header>
<hgroup>
<a href="https://ai.unturf.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">unturf.</h1></a>
<p>Welcome to ai.unturf.com - Free LLM & Text To Speech Artificial Intelligence Service</title>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<main>
<h2>Introducing Hermes AI and TTS Speech Endpoints</h2>
<p>At <strong>ai.unturf.com</strong>, we offer free AI services powered by the <a href="https://nousresearch.com/hermes3/" target="_blank">NousResearch/Hermes-3-Llama-3.1-8B</a> model and a TTS (Text-to-Speech) endpoint. Our mission is to provide accessible AI tools for everyone, embodying the principles of both free as in beer & free as in freedom. You can interact with our models without any cost, and you are encouraged to contribute and build upon the open-source code & models that we use.</p>
<p>We intend to be a drop in replacement, you can use the existing open source OpenAI client to communicate with us.</p>
<h2 id="client-side">Web Client-Only Solution: Interact with AI Services Directly from Static Sites or CDNs</h2>
<p><b>Because we don't require a valid API key, we don't have any real need for a server.</b></p>
<p>Add this LLM to any static site or CDN.</p>
<p>This web client-only solution uses <a href="https://uncloseai.com/uncloseai.js" target="_blank">uncloseai.js</a> to make the browser act as a client, directly interacting with the API without needing an intermediary server. By eliminating the need for a valid API key, the API handles requests on behalf of the browser client, making it efficient and accessible thin client, especially those on battery power like phones & laptops.</p>
<div id="chat-container">
<div id="chat-box"></div>
<div></div>
</div>
<div>
<input type="text" id="user-input" placeholder="Ask about this page...">
<button onclick="handleUserInput()">Send</button>
</div>
<br/>
<p>This static site has a live LLM demonstration above!</p>
<p>A stand alone demo is here: <a href="https://ai.unturf.com/demo.html">uncloseai.js demostration</a></p>
<h3>Installing the OpenAI Client</h3>
<h4>Python</h4>
<p>To install the OpenAI package for Python, use <code>pip</code>:</p>
<pre><code>pip install openai</code></pre>
<h4>Node.js</h4>
<p>To install the OpenAI package for Node.js, you can use <code>npm</code> in your <code>package.json</code>:</p>
<pre><code>{
"dependencies": {
"openai": "^v4.67.3" // Use the latest version
}
}
</code></pre>
<p>Run the following command to install it:</p>
<pre><code>npm install</code></pre>
<h2>Using the Hermes AI Model</h2>
<h3>Python Example</h3>
<h4>Non-Streaming</h4>
<pre><code class="python"># Python Fizzbuzz Example
from openai import OpenAI
client = OpenAI(base_url="https://hermes.ai.unturf.com/v1", api_key="choose-any-value")
#MODEL = "NousResearch/Hermes-3-Llama-3.1-8B"
MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"
messages = [{"role": "user", "content": "Give a Python Fizzbuzz solution in one line of code?"}]
response = client.chat.completions.create(
model=MODEL,
messages=messages,
temperature=0.5,
max_tokens=150
)
print(response.choices[0].message.content)
</code></pre>
<h4>Streaming</h4>
<pre><code class="python"># Streaming response in Python
from openai import OpenAI
client = OpenAI(base_url="https://hermes.ai.unturf.com/v1", api_key="choose-any-value")
MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"
messages = [
{"role": "user", "content": "Give a Python Fizzbuzz solution in one line of code?"}
]
response = client.chat.completions.create(
model=MODEL,
messages=messages,
temperature=0.5,
max_tokens=150,
stream=True, # Enable streaming
)
for chunk in response:
if hasattr(chunk.choices[0].delta, "content"):
print(chunk.choices[0].delta.content, end="")
</code></pre>
<h2>Node.js Example</h2>
<h3>Non-Streaming</h3>
<pre><code class="javascript">const OpenAI = require('openai');
const client = new OpenAI({
baseURL: "https://hermes.ai.unturf.com/v1",
apiKey: "dummy-api-key",
});
const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic";
const messages = [{"role": "user", "content": "Give a Python Fizzbuzz solution in one line of code?"}];
async function getResponse() {
try {
const response = await client.chat.completions.create({
model: MODEL,
messages: messages,
temperature: 0.5,
max_tokens: 150,
});
console.log(response.choices[0].message.content);
} catch (error) {
console.error("Error:", error.response ? error.response.data : error.message);
}
}
getResponse();
</code></pre>
<h3>Streaming</h3>
<pre><code class="javascript">
const OpenAI = require('openai');
const client = new OpenAI({
baseURL: "https://hermes.ai.unturf.com/v1",
apiKey: "dummy-api-key",
});
const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic";
const messages = [{"role": "user", "content": "Give a Python Fizzbuzz solution in one line of code?"}];
async function streamResponse() {
try {
const stream = await client.chat.completions.create({
model: MODEL,
messages: messages,
temperature: 0.5,
max_tokens: 150,
stream: true, // Enable streaming
});
// Use async iterator to read each chunk
for await (const chunk of stream) {
const msg = chunk.choices[0].delta.content;
process.stdout.write(msg); // Print each chunk as it arrives
}
} catch (error) {
console.error("Error:", error.response ? error.response.data : error.message);
}
}
streamResponse();
</code></pre>
<h2 id="tts">Using the Text To Speech Endpoint</h2>
<h3>Python Example</h3>
<pre><code class="python"># TTS Speech Example in Python
import openai
client = openai.OpenAI(
api_key = "YOLO",
base_url = "https://speech.ai.unturf.com/v1",
)
with client.audio.speech.with_streaming_response.create(
model="tts-1",
voice="alloy",
speed=0.9,
input="I think so therefore, Today is a wonderful day to build something people love!"
) as response:
response.stream_to_file("speech.mp3")
</code></pre>
<h3>Node.js Example</h3>
<pre><code class="javascript">const OpenAI = require('openai');
const client = new OpenAI({
baseURL: "https://speech.ai.unturf.com/v1",
apiKey: "YOLO",
});
async function getSpeech() {
try {
const response = await client.audio.speech.with_streaming_response.create({
model: "tts-1",
voice: "alloy",
speed: 0.9,
input: "I think so therefore, Today is a wonderful day to build something people love!"
});
response.stream_to_file("speech.mp3");
} catch (error) {
console.error("Error:", error.response ? error.response.data : error.message);
}
}
getSpeech();
</code></pre>
<h2>How we run inference</h2>
<p>This section is optional. This is only if you wanted to try to contribute idle GPU time to the project or if you wanted to reproduce everything in your own cluster.</p>
<p>We use vLLM to run models, currently full f16 safetensors. We make sure to use a virtualenv to hold the dependencies.</p>
<p>We are considering supporting ollama for better quant support.</p>
<p>Stand up a replica cluster on a new domain.</p>
<pre><code>
cd ~
python3 -m venv env
source env/bin/activate
pip install vllm
python -m vllm.entrypoints.openai.api_server --model adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic --host 0.0.0.0 --port 18888 --max-model-len 82000
</code></pre>
<p>The Speech endpoint or TTS uses <a href="https://github.com/matatonic/openedai-speech?tab=readme-ov-file#nvidia-gpu-cuda">openedai-speech</a> running via Docker.
<p>If you want to see how we setup the proxy, check out <a href="https://git.unturf.com/-/snippets/3">/etc/caddy/Caddyfile</a></p>
<pre><code>
ai.unturf.com {
root * /opt/www
file_server
log {
output file /var/log/caddy/ai.unturf.com.log {
roll_size 50mb
roll_keep 5
}
}
tls {
on_demand
}
}
hermes.ai.unturf.com {
reverse_proxy <removed>:18888
log {
output file /var/log/caddy/hermes.ai.unturf.com.log {
roll_size 50mb
roll_keep 5
}
}
tls {
on_demand
}
}
speech.ai.unturf.com {
reverse_proxy <removed>:8000
log {
output file /var/log/caddy/speech.ai.unturf.com.log {
roll_size 50mb
roll_keep 5
}
}
tls {
on_demand
}
}
</code></pre>
<p>We will likely implement a rate limit based on client IP address.</p>
<h2>Questions & Comments & Discussions</h2>
Use the <a href="https://www.remarkbox.com" target="_blank">Remarkbox</a> below to tell us what you think!
<div id="remarkbox-div">
<noscript>
<iframe id=remarkbox-iframe src="https://my.remarkbox.com/embed?nojs=true" style="height:600px;width:100%;border:none!important" tabindex=0></iframe>
</noscript>
</div>
<script src="https://my.remarkbox.com/static/js/iframe-resizer/iframeResizer.min.js"></script>
<script>
var rb_owner_key = "944c8dfa-8b2b-11ef-af0e-29ab4fb285a0";
var thread_uri = window.location.href;
var thread_title = window.document.title;
var thread_fragment = window.location.hash;
var rb_src = "https://my.remarkbox.com/embed" +
"?rb_owner_key=" + rb_owner_key +
"&thread_title=" + encodeURI(thread_title) +
"&thread_uri=" + encodeURIComponent(thread_uri) +
thread_fragment;
function create_remarkbox_iframe() {
var ifrm = document.createElement("iframe");
ifrm.setAttribute("id", "remarkbox-iframe");
ifrm.setAttribute("scrolling", "no");
ifrm.setAttribute("src", rb_src);
ifrm.setAttribute("frameborder", "0");
ifrm.setAttribute("tabindex", "0");
ifrm.setAttribute("title", "Remarkbox");
ifrm.style.width = "100%";
document.getElementById("remarkbox-div").appendChild(ifrm);
}
create_remarkbox_iframe();
iFrameResize(
{
checkOrigin: ["https://my.remarkbox.com"],
inPageLinks: true,
initCallback: function(e) { e.iFrameResizer.moveToAnchor(thread_fragment) }
},
document.getElementById("remarkbox-iframe")
);
</script>
<script>hljs.highlightAll();</script>
</main>
<footer>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
</footer>
</body>
</html>

998
integration_tests.js Executable file
View file

@ -0,0 +1,998 @@
// This is free software for the public good of a permacomputer hosted at
// permacomputer.com, an always-on computer by the people, for the people.
// One which is durable, easy to repair, & distributed like tap water
// for machine learning intelligence.
//
// The permacomputer is community-owned infrastructure optimized around
// four values:
//
// TRUTH First principles, math & science, open source code freely distributed
// FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
// HARMONY Minimal waste, self-renewing systems with diverse thriving connections
// LOVE Be yourself without hurting others, cooperation through natural law
//
// This software contributes to that vision by making machine learning
// accessible to everyone through a free, open, embeddable chat interface.
// Code is seeds to sprout on any abandoned technology.
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import fs from 'fs';
import { JSDOM } from 'jsdom';
import vm from 'vm';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const srcDir = join(__dirname, 'src');
// Test results tracking
let tests = 0;
let passed = 0;
let failed = 0;
let warnings = 0;
function test(description, testFn) {
tests++;
try {
testFn();
console.log(`${description}`);
passed++;
} catch (error) {
console.error(`${description}`);
console.error(` Error: ${error.message}`);
failed++;
}
}
function warn(description, message) {
console.warn(`⚠️ ${description}: ${message}`);
warnings++;
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
// Create a mock DOM environment
function createMockDOM() {
const dom = new JSDOM(`<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<div id="test-container"></div>
</body>
</html>`, {
url: "https://example.com/test",
pretendToBeVisual: true,
resources: "usable"
});
Object.defineProperty(global, 'window', { value: dom.window, configurable: true });
Object.defineProperty(global, 'document', { value: dom.window.document, configurable: true });
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, configurable: true });
Object.defineProperty(global, 'HTMLElement', { value: dom.window.HTMLElement, configurable: true });
Object.defineProperty(global, 'Element', { value: dom.window.Element, configurable: true });
Object.defineProperty(global, 'Node', { value: dom.window.Node, configurable: true });
Object.defineProperty(global, 'location', { value: dom.window.location, configurable: true });
// Mock fetch and other globals
global.fetch = async (url, options) => {
return {
ok: true,
json: async () => ({}),
text: async () => '',
status: 200
};
};
// Mock audio and media APIs
global.Audio = class MockAudio {
constructor() {
this.paused = true;
this.currentTime = 0;
}
play() { this.paused = false; return Promise.resolve(); }
pause() { this.paused = true; }
};
// Mock clipboard API
global.navigator.clipboard = {
writeText: async (text) => Promise.resolve()
};
// Mock matchMedia
global.window.matchMedia = (query) => ({
matches: false,
media: query,
onchange: null,
addEventListener: () => {},
removeEventListener: () => {}
});
return dom;
}
// Helper functions (keeping the existing ones)
function fileExists(filePath) {
return fs.existsSync(join(srcDir, filePath));
}
function readFile(filePath) {
return fs.readFileSync(join(srcDir, filePath), 'utf-8');
}
function extractImports(content) {
const importRegex = /import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+)\s+from\s+["'](.\/[^"']+)["']/gs;
const imports = [];
let match;
while ((match = importRegex.exec(content)) !== null) {
imports.push(match[1]);
}
return imports;
}
function extractExports(content) {
const exportRegex = /export\s+(?:async\s+)?(?:function|const|let|var|class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
const exports = [];
let match;
while ((match = exportRegex.exec(content)) !== null) {
exports.push(match[1]);
}
return exports;
}
// Advanced module loading test
async function loadModuleInSandbox(modulePath, mockGlobals = {}) {
const moduleContent = readFile(modulePath);
const context = {
...mockGlobals,
console,
setTimeout,
clearTimeout,
setInterval,
clearInterval,
Promise,
URL,
URLSearchParams,
fetch: global.fetch,
import: async (path) => {
// Mock dynamic imports for testing
if (path.includes('./')) {
return {}; // Return empty object for relative imports
}
return {}; // Mock external imports
}
};
vm.createContext(context);
try {
// Transform ES modules to work in VM context (basic transformation)
const transformedCode = moduleContent
.replace(/import\s+.*?from\s+["'][^"']*["'];?/g, '// import statement removed for testing')
.replace(/export\s+(default\s+)?/g, '// export ');
vm.runInContext(transformedCode, context);
return context;
} catch (error) {
throw new Error(`Failed to load module ${modulePath}: ${error.message}`);
}
}
console.log('🧪 Running Comprehensive Integration Tests for Refactored UI Modules\n');
// Create mock DOM environment
const dom = createMockDOM();
// ===== SECTION 1: BASIC STRUCTURE TESTS =====
console.log('📁 Section 1: Basic Structure Tests');
test('All core JavaScript files exist', () => {
const coreFiles = [
'ui.js', 'uncloseai-embed-modal.js', 'widget-library.js',
'tts-modal.js', 'translate-modal.js', 'file-upload.js',
'chat.js', 'content.js', 'ui-themes.js', 'ui-translations.js',
'translation.js', 'config.js', 'tts.js', 'language-detection.js'
];
coreFiles.forEach(file => {
assert(fileExists(file), `File ${file} does not exist`);
});
});
test('All imported files exist and are accessible', () => {
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.js'));
files.forEach(file => {
const content = readFile(file);
const imports = extractImports(content);
imports.forEach(importPath => {
const filePath = importPath.replace('./', '');
assert(fileExists(filePath), `Imported file does not exist: ${filePath} (imported by ${file})`);
});
});
});
// ===== SECTION 2: MODULE LOADING TESTS =====
console.log('\n🔄 Section 2: Module Loading Tests');
test('ui.js can be parsed without syntax errors', () => {
const content = readFile('ui.js');
assert(content.length > 0, 'ui.js is empty');
// Check for basic syntax issues
const openBraces = (content.match(/{/g) || []).length;
const closeBraces = (content.match(/}/g) || []).length;
assert(openBraces === closeBraces, `Mismatched braces in ui.js: ${openBraces} open, ${closeBraces} close`);
const openParens = (content.match(/\(/g) || []).length;
const closeParens = (content.match(/\)/g) || []).length;
assert(openParens === closeParens, `Mismatched parentheses in ui.js: ${openParens} open, ${closeParens} close`);
});
test('All module files can be parsed without syntax errors', () => {
const moduleFiles = ['widget-library.js', 'uncloseai-embed-modal.js', 'tts-modal.js', 'translate-modal.js'];
moduleFiles.forEach(file => {
const content = readFile(file);
assert(content.length > 0, `${file} is empty`);
// Basic syntax validation
const openBraces = (content.match(/{/g) || []).length;
const closeBraces = (content.match(/}/g) || []).length;
assert(openBraces === closeBraces, `Mismatched braces in ${file}: ${openBraces} open, ${closeBraces} close`);
});
});
test('Configuration module loads correctly', () => {
const configContent = readFile('config.js');
assert(configContent.includes('export'), 'config.js has no exports');
// Check for key configuration items
const hasApiKey = configContent.includes('API_KEY');
const hasTtsUrl = configContent.includes('TTS_API_URL');
assert(hasApiKey || hasTtsUrl, 'config.js missing expected configuration exports');
});
// ===== SECTION 3: FUNCTION EXPORT TESTS =====
console.log('\n🔧 Section 3: Function Export Tests');
test('ui.js exports all required functions', () => {
const content = readFile('ui.js');
const exports = extractExports(content);
const requiredExports = [
'createFloatingAIButton',
'initializeSystem',
'initializeUncloseaiElements',
'initializeChatInterface',
'toggleUncloseaiEmbeddedModal'
];
requiredExports.forEach(exp => {
assert(exports.includes(exp), `ui.js missing export: ${exp}`);
});
});
test('widget-library.js exports all widget creation functions', () => {
const content = readFile('widget-library.js');
const exports = extractExports(content);
const widgetExports = [
'createFullInterface', 'createCustomInterface', 'createChatFeature',
'createTTSFeature', 'createUploadFeature', 'createTranslateFeature',
'createSmartTranslateFeature', 'createReadFeature', 'createButton'
];
widgetExports.forEach(exp => {
assert(exports.includes(exp), `widget-library.js missing export: ${exp}`);
});
});
test('Modal modules export their main functions', () => {
// Test uncloseai-embed-modal.js
const modalContent = readFile('uncloseai-embed-modal.js');
const modalExports = extractExports(modalContent);
assert(modalExports.includes('openUncloseaiEmbeddedModal'), 'Missing openUncloseaiEmbeddedModal export');
// Test tts-modal.js
const ttsContent = readFile('tts-modal.js');
const ttsExports = extractExports(ttsContent);
assert(ttsExports.includes('openTTSModal'), 'Missing openTTSModal export');
// Test translate-modal.js
const translateContent = readFile('translate-modal.js');
const translateExports = extractExports(translateContent);
assert(translateExports.includes('openTranslateModal'), 'Missing openTranslateModal export');
});
// ===== SECTION 4: DOM RENDERING TESTS =====
console.log('\n🎨 Section 4: DOM Rendering Tests');
test('createFloatingAIButton can create DOM elements', () => {
// Mock the required functions
global.window.UNCLOSEAI_CUSTOM_STYLING = true;
global.window.UNCLOSEAI_FLOATING_BUTTON = true;
// Create a mock function that simulates createFloatingAIButton
const mockCreateButton = () => {
const button = document.createElement('button');
button.id = 'floating-ai-button';
button.textContent = 'uncloseai.';
button.style.cssText = 'position: fixed; bottom: 20px; right: 10px;';
document.body.appendChild(button);
return button;
};
const button = mockCreateButton();
assert(button instanceof Element, 'Failed to create button element');
assert(button.id === 'floating-ai-button', 'Button has wrong ID');
assert(button.textContent === 'uncloseai.', 'Button has wrong text content');
assert(document.getElementById('floating-ai-button'), 'Button not added to DOM');
});
test('Widget creation functions can generate proper DOM structure', () => {
const container = document.createElement('div');
container.id = 'widget-test-container';
document.body.appendChild(container);
// Mock widget creation
const mockCreateChatFeature = (container) => {
const chatDiv = document.createElement('div');
chatDiv.innerHTML = `
<h4>AI Chat</h4>
<div data-chat-box style="height: 150px; border: 1px solid #ccc;"></div>
<input type="text" data-chat-input style="width: 100%;">
<button onclick="handleCustomChat(this)">Send</button>
`;
container.appendChild(chatDiv);
return chatDiv;
};
const chatWidget = mockCreateChatFeature(container);
assert(chatWidget.querySelector('h4'), 'Chat widget missing header');
assert(chatWidget.querySelector('[data-chat-box]'), 'Chat widget missing chat box');
assert(chatWidget.querySelector('[data-chat-input]'), 'Chat widget missing input');
assert(chatWidget.querySelector('button'), 'Chat widget missing send button');
});
test('Modal creation generates proper dialog elements', () => {
const mockCreateModal = () => {
const modal = document.createElement('dialog');
modal.id = 'uncloseai-embedded-modal';
modal.style.cssText = 'position: fixed; z-index: 2000;';
const article = document.createElement('article');
const header = document.createElement('header');
const closeBtn = document.createElement('button');
closeBtn.textContent = 'X';
header.appendChild(closeBtn);
article.appendChild(header);
modal.appendChild(article);
return modal;
};
const modal = mockCreateModal();
assert(modal.tagName === 'DIALOG', 'Modal is not a dialog element');
assert(modal.id === 'uncloseai-embedded-modal', 'Modal has wrong ID');
assert(modal.querySelector('header'), 'Modal missing header');
assert(modal.querySelector('button'), 'Modal missing close button');
});
// ===== SECTION 5: FUNCTIONAL TESTS =====
console.log('\n⚙ Section 5: Functional Tests');
test('Toggle function logic works correctly', () => {
// Mock the toggle function behavior
let modalOpen = false;
const mockToggle = () => {
const existingModal = document.getElementById('test-modal');
if (existingModal && modalOpen) {
existingModal.remove();
modalOpen = false;
} else {
const modal = document.createElement('dialog');
modal.id = 'test-modal';
document.body.appendChild(modal);
modalOpen = true;
}
return modalOpen;
};
// Test opening
const opened = mockToggle();
assert(opened === true, 'Toggle should open modal');
assert(document.getElementById('test-modal'), 'Modal should be in DOM when open');
// Test closing
const closed = mockToggle();
assert(closed === false, 'Toggle should close modal');
assert(!document.getElementById('test-modal'), 'Modal should be removed from DOM when closed');
});
test('Theme detection and styling functions work', () => {
// Mock theme detection
const mockDetectTheme = () => {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
window.matchMedia?.('(prefers-color-scheme: dark)').matches;
return isDark ? 'dark' : 'light';
};
// Test light theme
document.documentElement.setAttribute('data-theme', 'light');
assert(mockDetectTheme() === 'light', 'Should detect light theme');
// Test dark theme
document.documentElement.setAttribute('data-theme', 'dark');
assert(mockDetectTheme() === 'dark', 'Should detect dark theme');
});
test('File upload input creation works', () => {
const mockCreateFileInput = () => {
const container = document.createElement('div');
const input = document.createElement('input');
input.type = 'file';
input.style.display = 'none';
const button = document.createElement('button');
button.textContent = 'Upload File';
button.onclick = () => input.click();
container.appendChild(input);
container.appendChild(button);
return { container, input, button };
};
const { container, input, button } = mockCreateFileInput();
assert(input.type === 'file', 'Input should be file type');
assert(input.style.display === 'none', 'Input should be hidden');
assert(button.textContent === 'Upload File', 'Button should have correct text');
assert(typeof button.onclick === 'function', 'Button should have click handler');
});
// ===== SECTION 6: INTEGRATION TESTS =====
console.log('\n🔗 Section 6: Integration Tests');
test('Global window exports are properly set', () => {
const uiContent = readFile('ui.js');
const expectedGlobalExports = [
'window.openTTSModal',
'window.openTranslateModal',
'window.toggleUncloseaiEmbeddedModal'
];
expectedGlobalExports.forEach(exp => {
assert(uiContent.includes(exp), `ui.js missing global export: ${exp}`);
});
});
test('Import dependencies are properly structured', () => {
const files = ['ui.js', 'widget-library.js', 'uncloseai-embed-modal.js'];
files.forEach(file => {
const content = readFile(file);
const imports = extractImports(content);
// Check that local imports use relative paths
imports.forEach(imp => {
assert(imp.startsWith('./'), `${file} has non-relative import: ${imp}`);
});
// Check that each imported file exists
imports.forEach(imp => {
const targetFile = imp.replace('./', '');
assert(fileExists(targetFile), `${file} imports non-existent file: ${targetFile}`);
});
});
});
test('No circular dependency patterns detected', () => {
const dependencyMap = new Map();
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.js'));
// Build dependency map
files.forEach(file => {
const content = readFile(file);
const imports = extractImports(content);
dependencyMap.set(file, imports.map(imp => imp.replace('./', '')));
});
// Check for direct circular dependencies
dependencyMap.forEach((imports, file) => {
imports.forEach(importedFile => {
if (dependencyMap.has(importedFile)) {
const secondLevelImports = dependencyMap.get(importedFile);
assert(
!secondLevelImports.includes(file),
`Circular dependency detected: ${file} -> ${importedFile} -> ${file}`
);
}
});
});
});
// ===== SECTION 7: PERFORMANCE & SIZE TESTS =====
console.log('\n📊 Section 7: Performance & Size Tests');
test('ui.js size reduction is significant', () => {
const content = readFile('ui.js');
const lines = content.split('\n').length;
const bytes = content.length;
assert(lines < 600, `ui.js is still too large: ${lines} lines (expected < 600)`);
assert(bytes < 50000, `ui.js file size too large: ${bytes} bytes (expected < 50KB)`);
console.log(` ui.js is now ${lines} lines, ${Math.round(bytes/1024)}KB`);
});
test('Extracted modules are substantial', () => {
const moduleFiles = ['widget-library.js', 'uncloseai-embed-modal.js', 'tts-modal.js', 'translate-modal.js'];
moduleFiles.forEach(file => {
const content = readFile(file);
const lines = content.split('\n').length;
// Each module should be substantial (removed arbitrary size limits)
assert(lines > 50, `${file} seems too small: ${lines} lines`);
console.log(` ${file}: ${lines} lines`);
});
});
test('Language files structure is optimal', () => {
const languagesDir = join(srcDir, 'languages');
const langFiles = fs.readdirSync(languagesDir).filter(f => f.endsWith('.js'));
assert(langFiles.length >= 10, `Not enough language files: ${langFiles.length}`);
// Check that language files follow consistent structure
langFiles.forEach(file => {
if (file !== 'index.js' && !file.includes('add-missing') && !file.includes('verify')) {
const content = readFile(`languages/${file}`);
assert(content.includes('export'), `${file} missing export statement`);
assert(content.includes('{') && content.includes('}'), `${file} missing object structure`);
}
});
});
// ===== SECTION 8: ERROR HANDLING TESTS =====
console.log('\n🛡 Section 8: Error Handling Tests');
test('Modules handle missing dependencies gracefully', () => {
// Test that modules don't crash on missing global variables
const testCases = [
{ var: 'window.UNCLOSEAI_CUSTOM_STYLING', default: false },
{ var: 'window.UNCLOSEAI_FLOATING_BUTTON', default: true },
{ var: 'window.uncloseaiEmbeddedModalOpen', default: false }
];
testCases.forEach(({ var: varName, default: defaultVal }) => {
// Test that default values are properly handled
const varCheck = `typeof ${varName} !== 'undefined' ? ${varName} : ${defaultVal}`;
// This is a conceptual test - in reality, the modules should handle undefined gracefully
assert(true, `${varName} should have fallback handling`);
});
});
test('DOM manipulation is safe', () => {
// Test that element creation doesn't throw errors
const testElement = document.createElement('div');
testElement.id = 'test-element';
testElement.style.cssText = 'position: relative; color: red;';
testElement.innerHTML = '<span>Test content</span>';
assert(testElement.id === 'test-element', 'Element ID setting failed');
assert(testElement.querySelector('span'), 'Inner HTML setting failed');
assert(testElement.style.position === 'relative', 'Style setting failed');
});
// ===== SECTION 9: MODAL-SPECIFIC ERROR REGRESSION TESTS =====
console.log('\n🎭 Section 9: Modal Error Regression Tests');
test('Modal import functions are properly defined', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Check that all required import wrapper functions exist
const requiredFunctions = [
'async function* sendMessageWithCustomHistory', // Generator function
'async function fetchModelsFromEndpoints',
'async function loadConversationHistory',
'async function clearConversationHistory',
'async function saveConversationHistory',
'async function getChatHistory',
'async function updateChatHistory',
'async function getSelectedModel'
];
requiredFunctions.forEach(func => {
assert(modalContent.includes(func), `Modal missing function: ${func}`);
});
});
test('Modal imports target correct modules', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Test that required imports exist (simplified check)
const requiredImports = [
{ module: 'models.js', func: 'fetchModelsFromEndpoints' },
{ module: 'storage.js', func: 'loadConversationHistory' },
{ module: 'storage.js', func: 'clearConversationHistory' },
{ module: 'storage.js', func: 'saveConversationHistory' },
{ module: 'models.js', func: 'getSelectedModel' },
{ module: 'chat.js', func: 'sendMessageWithCustomHistory' }
];
requiredImports.forEach(({ module, func }) => {
const hasImport = modalContent.includes(`import("./${module}")`);
const hasFunction = modalContent.includes(func);
assert(
hasImport && hasFunction,
`Modal should import ${func} from ${module}`
);
});
});
test('Modal generator function wrapper is correct', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Check that sendMessageWithCustomHistory is properly wrapped as generator
assert(
modalContent.includes('async function* sendMessageWithCustomHistory'),
'sendMessageWithCustomHistory should be async generator function'
);
assert(
modalContent.includes('yield* sendMessageWithCustomHistory(history)'),
'sendMessageWithCustomHistory should use yield* to delegate to imported generator'
);
});
test('Window global functions are exported from ui.js', () => {
const uiContent = readFile('ui.js');
// Check that window globals are properly exported to fix modal button errors
const requiredGlobals = [
'window.openTTSModal',
'window.openTranslateModal',
'window.toggleUncloseaiEmbeddedModal'
];
requiredGlobals.forEach(global => {
assert(uiContent.includes(global), `ui.js missing global export: ${global}`);
});
});
test('Modal chat history handling is safe', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Verify that direct chatHistory access is replaced with function calls
const lines = modalContent.split('\n');
let directChatHistoryAccess = false;
lines.forEach((line, index) => {
// Look for direct chatHistory usage (not in comments)
if (line.includes('chatHistory') &&
!line.trim().startsWith('//') &&
!line.includes('getChatHistory') &&
!line.includes('updateChatHistory') &&
!line.includes('const') &&
!line.includes('function')) {
// Allow some specific safe patterns
if (!line.includes('chatHistory.find') || !line.includes('await getChatHistory()')) {
directChatHistoryAccess = true;
console.log(` Direct chatHistory access found at line ${index + 1}: ${line.trim()}`);
}
}
});
assert(!directChatHistoryAccess, 'Modal should not directly access chatHistory variable');
});
test('Modal error handling patterns are implemented', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Check for proper error handling in async functions
const asyncFunctionPattern = /async function[^{]*{[^}]*}/g;
const matches = modalContent.match(asyncFunctionPattern);
if (matches) {
// At least some async functions should have try-catch or error handling
const hasTryCatch = modalContent.includes('try {') || modalContent.includes('catch');
const hasErrorLog = modalContent.includes('console.error') || modalContent.includes('console.warn');
assert(
hasTryCatch || hasErrorLog,
'Modal should have error handling in async functions'
);
}
});
test('Modal function exports match their imports', () => {
const files = ['chat.js', 'storage.js', 'models.js', 'tts.js'];
const modalContent = readFile('uncloseai-embed-modal.js');
files.forEach(file => {
const fileContent = readFile(file);
const exports = extractExports(fileContent);
// Check that functions imported by modal are actually exported
exports.forEach(exportedFunc => {
if (modalContent.includes(`${exportedFunc}`)) {
const importPattern = new RegExp(`await import\\("\\.\/${file}"\\)[^}]*${exportedFunc}`, 'g');
if (importPattern.test(modalContent)) {
assert(true, `${file} exports ${exportedFunc} which modal imports`);
}
}
});
});
});
test('Modal prevents circular dependency with chat.js', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
const chatContent = readFile('chat.js');
// Modal should only import from chat.js, not vice versa
const chatImportsModal = chatContent.includes('./uncloseai-embed-modal.js');
assert(!chatImportsModal, 'chat.js should not import from modal to prevent circular dependency');
// Modal should use dynamic imports from chat.js
const modalImportsChat = modalContent.includes('await import("./chat.js")');
assert(modalImportsChat, 'Modal should use dynamic imports from chat.js');
});
// ===== SECTION 10: SPECIFIC SESSION ERROR REGRESSION TESTS =====
console.log('\n🚨 Section 10: Session Error Regression Tests');
test('Async generator wrapper functions are correctly implemented', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Test for "sendMessageWithCustomHistory(...) is not iterable" error prevention
const generatorPattern = /async function\* sendMessageWithCustomHistory/;
const yieldPattern = /yield\* sendMessageWithCustomHistory/;
assert(
generatorPattern.test(modalContent) && yieldPattern.test(modalContent),
'sendMessageWithCustomHistory must be async generator with yield* delegation'
);
// Ensure no plain async function pattern for generators
const badAsyncPattern = /async function sendMessageWithCustomHistory\([^)]*\)\s*{[^}]*return[^}]*}/s;
assert(
!badAsyncPattern.test(modalContent),
'sendMessageWithCustomHistory should not be plain async function returning value'
);
});
test('Dynamic import wrapper functions handle all error cases', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Check that all wrapper functions properly await imports
const dynamicImportFunctions = [
'fetchModelsFromEndpoints',
'loadConversationHistory',
'clearConversationHistory',
'saveConversationHistory',
'getChatHistory',
'updateChatHistory',
'getSelectedModel'
];
dynamicImportFunctions.forEach(funcName => {
const functionPattern = new RegExp(`async function ${funcName}`);
const importPattern = new RegExp(`await import\\(.*\\)`);
assert(
functionPattern.test(modalContent) && importPattern.test(modalContent),
`${funcName} wrapper function must properly await dynamic import`
);
});
});
test('Module import paths match actual exports', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Check that required imports exist (simplified patterns)
const requiredImports = [
{ module: 'models.js', func: 'fetchModelsFromEndpoints' },
{ module: 'storage.js', func: 'loadConversationHistory' },
{ module: 'storage.js', func: 'clearConversationHistory' },
{ module: 'storage.js', func: 'saveConversationHistory' },
{ module: 'models.js', func: 'getSelectedModel' },
{ module: 'chat.js', func: 'sendMessageWithCustomHistory' }
];
requiredImports.forEach(({ module, func }) => {
const modulePattern = new RegExp(`await import\\(.*${module}.*\\)`);
const funcPattern = new RegExp(`async function.*${func}`);
assert(
modulePattern.test(modalContent) && funcPattern.test(modalContent),
`Missing import: ${func} from ${module}`
);
});
});
test('Variable scope isolation prevents undefined reference errors', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Check for direct variable access that should use function calls
const lines = modalContent.split('\n');
const problematicPatterns = [
{ pattern: /^[^\/]*chatHistory\s*\./, desc: 'Direct chatHistory access' },
{ pattern: /^[^\/]*chatHistory\s*\[/, desc: 'Direct chatHistory array access' },
{ pattern: /^[^\/]*chatHistory\s*=/, desc: 'Direct chatHistory assignment' }
];
let violations = [];
lines.forEach((line, index) => {
problematicPatterns.forEach(({ pattern, desc }) => {
if (pattern.test(line) &&
!line.includes('getChatHistory') &&
!line.includes('updateChatHistory') &&
!line.includes('const') &&
!line.includes('function') &&
!line.includes('//')) {
violations.push(`Line ${index + 1}: ${desc} - ${line.trim()}`);
}
});
});
assert(violations.length === 0, `Variable scope violations found:\n${violations.join('\n')}`);
});
test('Runtime function availability is guaranteed', () => {
const uiContent = readFile('ui.js');
const modalContent = readFile('uncloseai-embed-modal.js');
// Functions that modal expects to be available at runtime
const runtimeFunctions = ['openTTSModal', 'openTranslateModal', 'toggleUncloseaiEmbeddedModal'];
runtimeFunctions.forEach(func => {
// Check that ui.js imports the function
const importPattern = new RegExp(`import.*${func}.*from`, 'g');
const exportPattern = new RegExp(`window\\.${func}\\s*=`, 'g');
if (func !== 'toggleUncloseaiEmbeddedModal') {
assert(
importPattern.test(uiContent),
`ui.js should import ${func} for global export`
);
}
assert(
exportPattern.test(uiContent),
`ui.js should export ${func} to window for modal access`
);
});
});
test('Module exports are consistent with their usage', () => {
const modules = [
{ file: 'chat.js', exports: ['sendMessage', 'sendMessageWithCustomHistory', 'getChatHistory', 'updateChatHistory'] },
{ file: 'storage.js', exports: ['loadConversationHistory', 'saveConversationHistory', 'clearConversationHistory'] },
{ file: 'models.js', exports: ['fetchModelsFromEndpoints', 'getSelectedModel'] },
{ file: 'tts-modal.js', exports: ['openTTSModal'] },
{ file: 'translate-modal.js', exports: ['openTranslateModal'] }
];
modules.forEach(({ file, exports: expectedExports }) => {
const content = readFile(file);
expectedExports.forEach(exportName => {
const exportPattern = new RegExp(`export\\s+(?:async\\s+)?(?:function\\*?|const|let)\\s+${exportName}`, 'g');
assert(
exportPattern.test(content),
`${file} should export ${exportName} as expected by importing modules`
);
});
});
});
test('Async/await patterns are consistently applied', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Find all dynamic import calls and ensure they're properly awaited
const importCallPattern = /await import\("[^"]*"\)/g;
const importCalls = modalContent.match(importCallPattern) || [];
assert(importCalls.length > 0, 'Modal should have dynamic imports');
// Simple check for destructuring patterns in the file
const hasDestructuring = modalContent.includes('const {') || modalContent.includes('const{');
assert(hasDestructuring, 'Modal should use destructuring for imports');
});
test('Error boundary patterns prevent cascading failures', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
// Check for error handling around critical operations
const criticalOperations = [
'loadHistory',
'addIntroMessage',
'loadModels',
'openUncloseaiEmbeddedModalNew'
];
criticalOperations.forEach(operation => {
const functionPattern = new RegExp(`(async\\s+)?function\\s+${operation}[^{]*{([^{}]*{[^{}]*})*[^}]*}`, 's');
const match = modalContent.match(functionPattern);
if (match) {
const functionBody = match[0];
// Should have some form of error handling
const hasErrorHandling =
functionBody.includes('try') ||
functionBody.includes('catch') ||
functionBody.includes('console.error') ||
functionBody.includes('console.warn') ||
functionBody.includes('Failed to');
assert(
hasErrorHandling,
`Function ${operation} should have error handling to prevent cascading failures`
);
}
});
});
test('Generator function delegation is properly implemented', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
const chatContent = readFile('chat.js');
// Verify that chat.js exports a generator function
const chatGeneratorPattern = /export\s+async\s+function\*\s+sendMessageWithCustomHistory/;
assert(
chatGeneratorPattern.test(chatContent),
'chat.js should export sendMessageWithCustomHistory as async generator'
);
// Verify that modal properly delegates to it
const hasGeneratorFunction = modalContent.includes('async function* sendMessageWithCustomHistory');
const hasYieldDelegation = modalContent.includes('yield* sendMessageWithCustomHistory');
assert(
hasGeneratorFunction && hasYieldDelegation,
'Modal should properly delegate to imported generator with yield*'
);
});
test('Function signature compatibility across modules', () => {
const modalContent = readFile('uncloseai-embed-modal.js');
const chatContent = readFile('chat.js');
// Simple check that both files have the sendMessageWithCustomHistory function
const modalHasFunction = modalContent.includes('function* sendMessageWithCustomHistory');
const chatHasFunction = chatContent.includes('function* sendMessageWithCustomHistory');
assert(
modalHasFunction && chatHasFunction,
'Both modal and chat should have sendMessageWithCustomHistory function'
);
});
// Cleanup
dom.window.close();
// ===== FINAL SUMMARY =====
console.log('\n📈 Integration Test Summary:');
console.log('='.repeat(50));
console.log(`Total tests: ${tests}`);
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Warnings: ${warnings} ⚠️`);
if (failed === 0) {
console.log('\n🎉 All integration tests passed!');
console.log('📦 The refactored modules are properly structured and functional.');
console.log('🚀 Safe to deploy!');
process.exit(0);
} else {
console.log('\n💥 Some tests failed. Please fix the issues above before deploying.');
console.log('🔧 Check the failed tests and resolve any import/export issues.');
process.exit(1);
}

379
journal.rst Normal file
View file

@ -0,0 +1,379 @@
.. This is free software for the public good of a permacomputer hosted at
.. permacomputer.com, an always-on computer by the people, for the people.
.. One which is durable, easy to repair, & distributed like tap water
.. for machine learning intelligence.
..
.. The permacomputer is community-owned infrastructure optimized around
.. four values:
..
.. TRUTH First principles, math & science, open source code freely distributed
.. FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
.. HARMONY Minimal waste, self-renewing systems with diverse thriving connections
.. LOVE Be yourself without hurting others, cooperation through natural law
..
.. This software contributes to that vision by making machine learning
.. accessible to everyone through a free, open, embeddable chat interface.
.. Code is seeds to sprout on any abandoned technology.
====================
Development Journal
====================
July 2, 2025
============
Token Estimator Implementation
-------------------------------
Built `token_estimator.js` - a simplified BPE-based token counter for translation ETA estimation.
**Not compatible with tiktoken** - this is estimation-only, ~85% accuracy. Good enough for progress bars, not for exact tokenization.
**NVIDIA 4090 Performance Metrics:**
- Prompt processing: 3,144 tokens/sec
- Generation: 121 tokens/sec
- Translation estimates: 1K tokens ≈ 11 seconds
The estimator uses real production metrics from our Hermes machine to provide realistic translation timing.
July 3, 2025
============
Complete Translation Infrastructure Overhaul
--------------------------------------------
**Modular Language System Implementation**
Restructured the entire translation system to use individual language files instead of one monolithic object. Each of the 19 supported languages now has its own dedicated JS file in `/src/languages/`.
**Language Coverage:**
- 🇺🇸 English (en.js) - 46 translation keys
- 🇨🇳 Chinese Simplified (zh.js)
- 🇹🇼 Chinese Traditional (zh-tw.js)
- 🇮🇳 Hindi (hi.js)
- 🇪🇸 Spanish (es.js)
- 🇫🇷 French (fr.js)
- 🇸🇦 Arabic (ar.js)
- 🇧🇩 Bengali (bn.js)
- 🇷🇺 Russian (ru.js)
- 🇧🇷 Portuguese (pt.js)
- 🇵🇰 Urdu (ur.js)
- 🇮🇩 Indonesian (id.js)
- 🇩🇪 German (de.js)
- 🇯🇵 Japanese (ja.js)
- 🇰🇪 Swahili (sw.js)
- 🇮🇳 Marathi (mr.js)
- 🇮🇳 Telugu (te.js)
- 🇹🇷 Turkish (tr.js)
- 🇰🇷 Korean (ko.js)
**Dynamic UI Refresh System**
Implemented `refreshUILanguage()` function that updates all UI text without requiring browser refresh:
- **Declarative translations**: Elements with `data-i18n` attributes automatically update
- **Parameter support**: `data-i18n-params` for dynamic text with variables like `{lang}` and `{error}`
- **Smart element detection**: Handles inputs, buttons, placeholders, and modal titles
- **Event system**: Dispatches `languageChanged` custom events for component integration
- **Backward compatibility**: Maintains `window.refreshUILanguage` for existing code
**Translation Verification System**
Created `verify-translations.js` - a Node.js script that ensures translation consistency:
- **Key extraction**: Parses JS files to extract translation object keys
- **Completeness verification**: Compares all languages against English reference
- **Missing/extra key detection**: Reports inconsistencies with colored console output
- **Exit codes**: Returns 0 for success, 1 for issues (CI/CD friendly)
**Adding New Languages - Workflow:**
1. **Before adding**: Run verification to ensure current state is clean
```bash
cd /home/fox/git/ai.unturf.com/src/languages
node verify-translations.js
```
Should show: "🎉 All languages have complete translations!"
2. **Create new language file**: Copy structure from `en.js`
```bash
cp en.js new-lang.js # Replace 'new-lang' with actual language code
```
3. **Translate all values**: Keep the same keys, translate only the string values
4. **Update index.js**: Add import and export for the new language
```javascript
import { newLang } from "./new-lang.js";
// Add to UI_TRANSLATIONS object
```
5. **Verify completeness**: Run script again to check for issues
```bash
node verify-translations.js
```
6. **Fix any issues**: Script will show missing/extra keys in red
- Missing keys: Add them with proper translations
- Extra keys: Remove them or add to English reference if needed
7. **Final verification**: Run until you see the success message again
**File Structure:**
```
src/languages/
├── index.js # Aggregates all translations
├── en.js # English reference (46 keys)
├── [18 other languages] # Complete translations
└── verify-translations.js # Sanity check script
```
**Benefits:**
- **Maintainability**: Each language isolated in its own file
- **Consistency**: 100% key coverage verified across all 19 languages
- **Real-time UX**: Language switching without page refresh
- **Developer experience**: Automated verification prevents translation drift
- **Scalability**: Easy to add new languages following the established pattern
This system supports the full user journey from language detection to dynamic interface updates, ensuring a seamless multilingual experience for all Hermes AI users.
July 9, 2025
============
Modal CSS Architecture Overhaul
-------------------------------
**The Problem**
Users reported multiple issues with the UncloseAI modal experience:
1. **Desktop Issue**: PicoCSS-powered sites displayed full-screen modals on large laptop displays instead of centered dialogs
2. **Translation Pages**: Broken CSS in translated pages opened in new windows made them unusable
3. **Mobile Experience**: After initial fixes, mobile devices lost their full-screen modal experience
**Root Cause Analysis**
Three interconnected issues created these problems:
1. **Inconsistent CSS Loading**: Different modal files used different detection methods and URL patterns
2. **PicoCSS Overrides**: The framework's aggressive dialog styling required careful specificity management
3. **Breakpoint Mismatch**: Mobile detection used 480px while many tablets needed full-screen at 768px
**Solution: Complete CSS Architecture Refactoring**
**Step 1: Dedicated CSS Files**
Created two purpose-built stylesheets to replace all inline styles:
- ``uncloseai-modal-builtin.css`` - Clean styles for standard blog sites
- ``uncloseai-modal-pico.css`` - Override styles with ``!important`` for PicoCSS sites
**Step 2: Unified CSS Loading Pattern**
Standardized all modal components to use the same detection and loading logic:
```javascript
// Detect if PicoCSS is actually present on the page
const hasPicoCSS = document.querySelector('link[href*="pico"]') !== null;
// Load appropriate CSS based on actual PicoCSS presence
const cssFile = hasPicoCSS ? 'uncloseai-modal-pico.css' : 'uncloseai-modal-builtin.css';
// Use absolute URLs for cross-domain compatibility
link.href = `https://uncloseai.com/src/${cssFile}`;
```
**Step 3: PicoCSS Override Strategy**
```css
/* Override PicoCSS dialog defaults with high specificity */
dialog#uncloseai-embedded-modal[data-theme] {
position: fixed !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
width: 70vw !important;
max-width: 800px !important;
}
/* Mobile/tablet full-screen experience */
@media (max-width: 768px) {
dialog#uncloseai-embedded-modal[data-theme] {
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
}
}
```
**Step 4: Translation Page CSS Fix**
- Added ``<base href="${baseUrl}">`` tag to resolve relative URLs in new windows
- Ensured all modal CSS uses absolute URLs for cross-window compatibility
- Maintained original page styling while adding AI functionality
**Technical Implementation**
**Files Modified:**
- ``uncloseai-embed-modal.js`` - Main modal with PicoCSS detection
- ``translate-modal.js`` - Translation modal with absolute URLs
- ``tts-modal.js`` - TTS modal with consistent loading
- ``widget-library.js`` - Widget components with proper CSS
- ``translation.js`` - Base tag injection for new windows
- ``uncloseai-modal-builtin.css`` - Standard site styles
- ``uncloseai-modal-pico.css`` - PicoCSS override styles
**Responsive Breakpoints:**
- **Desktop (>768px)**: Centered modal (70vw × 90vh, max 800px width)
- **Mobile/Tablet (≤768px)**: Full-screen modal (100vw × 100vh)
**Results**
**Desktop Experience**: Properly centered modals on large displays
**Mobile Experience**: Full-screen modals on phones and tablets
**Translation Pages**: Functional CSS in new window contexts
**Code Architecture**: Clean separation between JS logic and CSS styling
**Cross-Site Compatibility**: Consistent behavior across all site types
**Maintainability**: Single source of truth for modal styling
This comprehensive refactoring established a robust CSS architecture that handles all edge cases while providing an optimal user experience across all devices and contexts.
July 11, 2025
=============
Custom API Integration & UI Overhaul
-------------------------------------
**Major Features Implemented:**
Custom API Endpoint Support
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implemented comprehensive custom OpenAI-compatible API support allowing users to integrate their own AI providers (Groq, OpenAI, Claude, etc.) into the chat system:
- **Centralized Configuration System**: Created ``getAPIConfig()`` in ``config.js`` to manage API settings
- **Universal API Call Patching**: All chat functions now check for custom configuration automatically
- **Dynamic Model Discovery**: System fetches available models from custom endpoints via ``/v1/models``
- **Intelligent Max Tokens**: Automatically detects context limits from model metadata:
- VLLM: ``max_model_len`` (e.g., 82,000 tokens)
- OpenAI: ``max_tokens`` (e.g., 8,192 tokens)
- Groq: ``context_length`` (e.g., 128,000 tokens)
- **Seamless Fallback**: Falls back to default Hermes endpoints when custom API unavailable
**Settings Panel Complete Redesign**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Restructured the settings interface for better organization and usability:
- **Two-Column Grid Layout**: Left column (Model, Voice, Language), Right column (API Configuration)
- **Quick Actions Header**: Action buttons span both columns at the top for prominence
- **Mobile Responsive**: Automatically stacks to single column on screens ≤768px
- **Label Cleanup**: Removed redundant section headers (``AI Model:``, ``TTS Voice:``, etc.) for cleaner look
- **Grid Override Fixes**: Added ``!important`` declarations to overcome PicoCSS conflicts
**Translation System Improvements**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Enhanced the multilingual system with new keys and cleanup:
- **Chat Audio Downloads**: Added ``downloadChatAudio`` key for chat message audio
- **Custom API Description**: Added ``useCustomAPI`` across all 19 languages: "Use a custom openai compatible endpoint."
- **Translation Cleanup**: Removed 5 unused keys (``modelLabel``, ``voiceLabel``, ``languageLabel``, ``quickActions``, ``apiConfigLabel``)
- **Validator Updates**: Enhanced verification scripts to handle translation changes
**Critical Bug Fixes:**
Model Selection System Overhaul
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Fixed fundamental issues with model selection that were causing API mismatches:
**Issue**: Users selected Groq models but system sent requests to Hermes models
**Root Cause**: Modal model dropdown had no HTML ID, causing ``getSelectedModel()`` to fall back to defaults
**Solution**:
- Added ``id="hermes-model-selection"`` to modal dropdown
- Enhanced debug logging to trace model selection flow
- Fixed model registry to properly store endpoint relationships
**Cache System Enhancement**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Fixed refresh models button not working when custom API settings changed:
**Issue**: Clicking refresh didn't fetch new models after enabling custom API
**Root Cause**: Cache key only considered ``VLLM_ENDPOINTS`` array, not custom API configuration
**Solution**:
- Include custom API settings in cache hash key
- Cache now properly invalidates when toggling custom API
- Test case: Checking/unchecking custom API produces different model lists
**Error Handling & Debugging**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Added comprehensive error tracking and debugging tools:
- **API Error Details**: Full HTTP status codes and response bodies logged
- **Model Selection Tracing**: Debug messages show exactly which model is selected and why
- **Configuration Verification**: Clear console indicators for custom vs default API usage
- **Max Tokens Logging**: Shows token limits being used for each request type
**Technical Implementation Details:**
**Files Modified:**
- ``src/config.js`` - Central API configuration with ``getAPIConfig()``
- ``src/models.js`` - Enhanced model selection, caching, and max_tokens support
- ``src/chat.js`` - Dynamic max_tokens for all chat functions
- ``src/translation.js`` - Custom API support with dynamic token limits
- ``src/ui.js`` - Intro generation API integration
- ``src/uncloseai-embed-modal.js`` - Settings UI redesign and model dropdown ID fix
- ``src/uncloseai-modal-*.css`` - Grid layouts and responsive design improvements
- ``src/languages/*.js`` - Translation updates across all 19 language files
**API Call Patching Strategy:**
All API functions now follow this pattern:
```javascript
const apiConfig = await getAPIConfig();
if (apiConfig.isCustom) {
// Use custom endpoint, API key, and selected model
apiUrl = `${apiConfig.endpoint}/chat/completions`;
headers = { "Authorization": `Bearer ${apiConfig.apiKey}` };
model = apiConfig.model;
} else {
// Use default Hermes configuration
apiUrl = `${getSelectedModelEndpoint()}/chat/completions`;
headers = { "Authorization": `Bearer ${API_KEY}` };
model = getSelectedModel();
}
```
**Dynamic Max Tokens Implementation:**
```javascript
const maxTokens = getSelectedModelMaxTokens(); // Reads from model registry
// Translation: Could be 70,000 for VLLM or 8,192 for Groq
// Chat: Adapts to whatever the selected model supports
```
**UI Flow Improvements:**
1. User configures custom API (base URL + API key)
2. Clicks "🔄 Refresh Models" → Fetches models from both Hermes + custom endpoint
3. Selects model from combined dropdown (e.g., ``https://api.groq.com/openai/v1-llama-3.1-8b-instant``)
4. All features (chat, translation) automatically use selected custom model
**Deployment Notes:**
- **Backward Compatible**: Existing users continue using Hermes by default
- **Progressive Enhancement**: Custom API is opt-in with fallback protection
- **Cache Management**: Model cache automatically refreshes when configuration changes
- **Cross-Platform**: Works on both modal interface and blog site integration
**Testing Completed:**
- ✅ Custom API integration with Groq, OpenAI endpoints
- ✅ Model selection working across modal and blog interfaces
- ✅ Dynamic max_tokens based on model capabilities (82k for VLLM, 8k for Groq)
- ✅ Cache invalidation when toggling custom API settings
- ✅ Mobile responsive 2-column grid layout
- ✅ Translation key cleanup across all 19 languages
This release transforms the AI chat system from a single-provider solution into a flexible, multi-provider platform while maintaining the clean user experience and adding significant debugging capabilities for troubleshooting.

2168
lib/un.js Normal file

File diff suppressed because it is too large Load diff

1251
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "ai.unturf.com",
"version": "1.0.0",
"type": "module",
"main": "integration_tests.js",
"scripts": {
"test": "node integration_tests.js",
"test:simple": "node simple_tests.js"
},
"devDependencies": {
"esbuild": "^0.27.3",
"highlight.js": "^11.11.1",
"jsdom": "^22.0.0",
"marked": "^17.0.3"
},
"repository": {
"type": "git",
"url": "ssh://git@git.unturf.com:2222/engineering/unturf/ai.unturf.com.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

291
public/browser-toys.html Normal file
View file

@ -0,0 +1,291 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Browser Toys - Machine Learning on Every Page | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html" class="active">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>Browser Toys - Machine Learning on Every Page</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="intro">Browser Toys</h2>
<p>Browser extensions that add the <strong>uncloseai.</strong> floating button to every web page you visit. Lightweight wrappers around <a href="/uncloseai-js.html">uncloseai.js</a> — no server, no backend, no accounts. The extension injects the script tag, and everything else runs client-side in your browser.</p>
<p><strong>Current release:</strong> <a href="https://git.unturf.com/engineering/unturf/uncloseai-browser-toys/-/releases/v4.2.0" target="_blank">v4.2.0</a></p>
<h3 id="extensions">Available Extensions</h3>
<h4 id="chrome">Chrome / Edge / Brave / Vivaldi / Arc</h4>
<p>Manifest V3 extension for all Chromium-based browsers.</p>
<p><a href="/downloads/uncloseai-browser-toy-chrome.zip" role="button" download>Download for Chrome (~238 KB)</a></p>
<ol>
<li>Download and unzip <code>uncloseai-browser-toy-chrome.zip</code></li>
<li>Verify the download (see <a href="#checksums">checksums</a> below)</li>
<li>Open <code>chrome://extensions</code> and enable <strong>Developer mode</strong> (toggle in top right)</li>
<li>Click <strong>Load unpacked</strong> and select the unzipped folder</li>
<li>The <strong>uncloseai.</strong> button appears on every page you visit</li>
</ol>
<h4 id="firefox">Firefox</h4>
<p>Manifest V2 extension for Firefox 109+ and Firefox-based browsers (LibreWolf, Waterfox).</p>
<p><a href="https://addons.mozilla.org/en-US/firefox/addon/uncloseai/" role="button">Install from Firefox Add-ons</a></p>
<p><a href="/downloads/uncloseai-browser-toy-firefox.zip" role="button" class="secondary" download>Direct download (~238 KB)</a></p>
<details>
<summary>Manual install from direct download</summary>
<ol>
<li>Download <code>uncloseai-browser-toy-firefox.zip</code></li>
<li>Verify the download (see <a href="#checksums">checksums</a> below)</li>
<li>Open <code>about:debugging#/runtime/this-firefox</code></li>
<li>Click <strong>Load Temporary Add-on</strong> and select the downloaded zip file</li>
<li>The <strong>uncloseai.</strong> button appears on every page you visit</li>
</ol>
</details>
<h4 id="safari">Safari</h4>
<p>Safari Web Extension for macOS and iOS. Requires Xcode conversion.</p>
<p><a href="/downloads/uncloseai-browser-toy-safari.zip" role="button" download>Download Safari source (~238 KB)</a></p>
<ol>
<li>Download and unzip <code>uncloseai-browser-toy-safari.zip</code></li>
<li>Verify the download (see <a href="#checksums">checksums</a> below)</li>
<li>Convert to Xcode project:
<pre><code class="language-bash">xcrun safari-web-extension-converter /path/to/unzipped-folder \
--project-location ./safari-xcode \
--app-name "uncloseai" \
--bundle-identifier com.uncloseai.browser-toy \
--no-open</code></pre></li>
<li>Open <code>safari-xcode/uncloseai.xcodeproj</code>, set your signing team, build (Cmd+R)</li>
<li>Enable in Safari <strong>Settings &gt; Extensions</strong></li>
</ol>
<h4 id="checksums">Verify your download</h4>
<p>Always verify downloads before installing. Checksum files are generated by the build and published alongside the zips.</p>
<p><a href="/downloads/SHA256SUMS">SHA256SUMS</a> | <a href="/downloads/MD5SUMS">MD5SUMS</a></p>
<h5>v1.0.0 SHA-256</h5>
<pre><code>2550bbdd34941b95e7d8944bbf5140b6ef16848c77ce4862f8e52aa9c2091087 uncloseai-browser-toy-chrome.zip
9b931277112df639fdec830bc43ea86f23b650980dec4c3c012bdec3735f3abc uncloseai-browser-toy-firefox.zip
02546524bc9bdab7337274749f4a59caeb1e8b310514ec41d8a7cb54b5661a8e uncloseai-browser-toy-safari.zip</code></pre>
<pre><code class="language-bash"># Download checksums and verify all zips at once
curl -sO https://uncloseai.com/downloads/SHA256SUMS
sha256sum -c SHA256SUMS
# Or verify a single file
curl -s https://uncloseai.com/downloads/SHA256SUMS | grep chrome | sha256sum -c</code></pre>
<h3 id="how-it-works">How it works</h3>
<ol>
<li>The content script injects <code>&lt;script src="https://uncloseai.com/uncloseai.js" type="module"&gt;</code> into every page</li>
<li>If uncloseai.js is already on the page, it skips injection (no duplicates)</li>
<li>The floating <strong>uncloseai.</strong> button appears in the bottom-right corner</li>
<li>Click it to chat with Hermes about the current page</li>
<li>Toggle the extension on or off from the toolbar popup</li>
</ol>
<h3 id="storage">Storage and privacy</h3>
<p>These extensions have no backend. Everything runs client-side in your browser.</p>
<ul>
<li><strong>Chat history</strong> stays in your browser's localStorage, scoped per domain and encrypted by UncloseVault. Conversations on <code>example.com</code> are separate from conversations on <code>github.com</code>. Each site has its own vault with its own password.</li>
<li><strong>Settings</strong> (model selection, language preference, voice) are encrypted in the vault.</li>
<li><strong>Secrets</strong> (custom API keys, unsandbox keys) are encrypted in the vault.</li>
<li><strong>The extension itself</strong> stores one boolean value: enabled on/off. That is the only data the extension touches.</li>
</ul>
<p>No data is written to localStorage unless encrypted with your password. No data leaves your browser except the messages you choose to send to the public machine learning endpoints at <code>hermes.ai.unturf.com</code>, <code>qwen.ai.unturf.com</code>, and <code>speech.ai.unturf.com</code>.</p>
<h3 id="encryption">Mandatory Vault Encryption</h3>
<p>UncloseVault is mandatory. When you first open the chat, you are prompted to create a vault password before anything else. No conversations, settings, or keys are stored without encryption.</p>
<h4>First launch</h4>
<ol>
<li>Click the <strong>uncloseai.</strong> button on any page</li>
<li>The vault gate appears: choose a password (minimum 8 characters)</li>
<li>Chat loads after the vault is created</li>
<li>All data is encrypted with AES-256 before writing to localStorage</li>
</ol>
<h4>Returning</h4>
<ol>
<li>Click the <strong>uncloseai.</strong> button on any page</li>
<li>Enter your password to unlock the vault</li>
<li>Your encrypted conversations and settings are restored</li>
</ol>
<h4>What gets encrypted</h4>
<ul>
<li><strong>Conversation history</strong> — all chat messages, per domain, encrypted before writing to localStorage</li>
<li><strong>Settings</strong> — model, voice, language preferences</li>
<li><strong>API keys</strong> — custom endpoint credentials</li>
<li><strong>Unsandbox keys</strong> — code execution credentials</li>
</ul>
<h4>How it works</h4>
<ul>
<li>AES-256 encryption via CryptoJS</li>
<li>Password-derived key with device-specific salt (SHA-256)</li>
<li>Each website has its own vault; you can use a different password per site</li>
<li>Session persistence across page navigations (encrypted session key in localStorage)</li>
<li>Lock the vault at any time from Settings to end your session</li>
</ul>
<p>Without the vault password, conversation history and settings are unreadable. If you forget your password, the encrypted data cannot be recovered.</p>
<h3 id="security">Security</h3>
<p>The extension code has been audited across 6 security categories with 49 automated tests. Findings: 0 critical, 0 high, 0 medium, 0 low, 3 informational.</p>
<ul>
<li>No <code>innerHTML</code>, <code>eval()</code>, <code>document.write</code>, or <code>new Function()</code></li>
<li>No network requests from extension code (all networking is in the injected uncloseai.js)</li>
<li>Minimal permissions: <code>storage</code> only (plus host access for injection)</li>
<li>Content Security Policy restricts extension pages to <code>script-src 'self'</code></li>
<li>Extension stores exactly one value: <code>{enabled: true|false}</code></li>
</ul>
<h3 id="source">Source Code</h3>
<p>📦 <a href="https://git.unturf.com/engineering/unturf/uncloseai-browser-toys" target="_blank">git.unturf.com/engineering/unturf/uncloseai-browser-toys</a></p>
<p>Releases: <a href="https://git.unturf.com/engineering/unturf/uncloseai-browser-toys/-/releases" target="_blank">all releases</a></p>
<p>Build from source:</p>
<pre><code class="language-bash">git clone https://git.unturf.com/engineering/unturf/uncloseai-browser-toys.git
cd uncloseai-browser-toys
node scripts/build.js # outputs zips to dist/</code></pre>
<p>Licensed under <a href="https://git.unturf.com/engineering/unturf/uncloseai-browser-toys/-/blob/master/LICENSE" target="_blank">AGPL-3.0-only</a>.</p>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2026</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#intro" class="active">Overview</a></li>
<li><a href="#extensions">Extensions</a>
<ul>
<li><a href="#chrome">Chrome / Edge</a></li>
<li><a href="#firefox">Firefox</a></li>
<li><a href="#safari">Safari</a></li>
<li><a href="#checksums">Checksums</a></li>
</ul>
</li>
<li><a href="#how-it-works">How it works</a></li>
<li><a href="#storage">Storage & Privacy</a></li>
<li><a href="#encryption">Mandatory Vault</a></li>
<li><a href="#security">Security Audit</a></li>
<li><a href="#source">Source Code</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

407
public/c-examples.html Normal file
View file

@ -0,0 +1,407 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>C Examples | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html" class="active">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>C Examples - Free LLM & TTS AI Service</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="c-intro">C Examples</h2>
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with C using libcurl. All examples use the OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
<p><strong>Available Endpoints:</strong></p>
<ul>
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
</ul>
<h3 id="c-setup">Setup & Dependencies</h3>
<p>Install libcurl development libraries:</p>
<pre><code class="bash"># Ubuntu/Debian
sudo apt-get install libcurl4-openssl-dev
# macOS
brew install curl
# Alpine
apk add curl-dev</code></pre>
<p>Compile with:</p>
<pre><code class="bash">gcc -o chat chat.c -lcurl</code></pre>
<h2 id="c-non-streaming">Non-Streaming Examples</h2>
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
<h3 id="c-hermes-non-stream">Using Hermes (General Purpose)</h3>
<pre><code class="c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;curl/curl.h&gt;
struct Memory {
char *response;
size_t size;
};
static size_t write_callback(void *data, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
struct Memory *mem = (struct Memory *)userp;
char *ptr = realloc(mem->response, mem->size + realsize + 1);
if (!ptr) return 0;
mem->response = ptr;
memcpy(&amp;(mem->response[mem->size]), data, realsize);
mem->size += realsize;
mem->response[mem->size] = 0;
return realsize;
}
int main(void) {
CURL *curl = curl_easy_init();
if (!curl) return 1;
struct Memory chunk = {.response = malloc(1), .size = 0};
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
const char *payload =
"{"
"\"model\": \"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic\","
"\"messages\": [{\"role\": \"user\", \"content\": \"Give a Python Fizzbuzz solution in one line of code?\"}],"
"\"max_tokens\": 150"
"}";
curl_easy_setopt(curl, CURLOPT_URL, "https://hermes.ai.unturf.com/v1/chat/completions");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&amp;chunk);
CURLcode res = curl_easy_perform(curl);
if (res == CURLE_OK) {
printf("Response:\n%s\n", chunk.response);
}
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
free(chunk.response);
return 0;
}</code></pre>
<h3 id="c-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;curl/curl.h&gt;
// ... same write_callback and Memory struct as above ...
int main(void) {
CURL *curl = curl_easy_init();
if (!curl) return 1;
struct Memory chunk = {.response = malloc(1), .size = 0};
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
const char *payload =
"{"
"\"model\": \"hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M\","
"\"messages\": [{\"role\": \"user\", \"content\": \"Explain quicksort in C with code\"}],"
"\"max_tokens\": 500"
"}";
curl_easy_setopt(curl, CURLOPT_URL, "https://qwen.ai.unturf.com/v1/chat/completions");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&amp;chunk);
CURLcode res = curl_easy_perform(curl);
if (res == CURLE_OK) {
printf("Response:\n%s\n", chunk.response);
}
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
free(chunk.response);
return 0;
}</code></pre>
<h2 id="c-streaming">Streaming Examples</h2>
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
<h3 id="c-hermes-stream">Streaming with Hermes</h3>
<pre><code class="c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;curl/curl.h&gt;
static size_t stream_callback(void *data, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
char *chunk = (char *)data;
// Process SSE data lines
char *line = strtok(chunk, "\n");
while (line) {
if (strncmp(line, "data: ", 6) == 0) {
const char *json = line + 6;
if (strcmp(json, "[DONE]") != 0) {
// Extract content from: {"choices":[{"delta":{"content":"..."}}]}
const char *content_start = strstr(json, "\"content\":\"");
if (content_start) {
content_start += 11;
const char *content_end = strchr(content_start, '"');
if (content_end) {
fwrite(content_start, 1, content_end - content_start, stdout);
fflush(stdout);
}
}
}
}
line = strtok(NULL, "\n");
}
return realsize;
}
int main(void) {
CURL *curl = curl_easy_init();
if (!curl) return 1;
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
const char *payload =
"{"
"\"model\": \"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic\","
"\"messages\": [{\"role\": \"user\", \"content\": \"Write a short poem about coding\"}],"
"\"max_tokens\": 200,"
"\"stream\": true"
"}";
curl_easy_setopt(curl, CURLOPT_URL, "https://hermes.ai.unturf.com/v1/chat/completions");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, stream_callback);
printf("Streaming response:\n");
curl_easy_perform(curl);
printf("\n");
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return 0;
}</code></pre>
<h2 id="c-tts">Text-to-Speech Examples</h2>
<p>Generate audio from text using the TTS endpoint.</p>
<h3 id="c-tts-basic">Basic TTS</h3>
<pre><code class="c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;curl/curl.h&gt;
static size_t write_file(void *data, size_t size, size_t nmemb, void *userp) {
return fwrite(data, size, nmemb, (FILE *)userp);
}
int main(void) {
CURL *curl = curl_easy_init();
if (!curl) return 1;
FILE *fp = fopen("output.mp3", "wb");
if (!fp) {
curl_easy_cleanup(curl);
return 1;
}
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
const char *payload =
"{"
"\"model\": \"tts-1\","
"\"input\": \"Hello! This is a test of text to speech.\","
"\"voice\": \"alloy\""
"}";
curl_easy_setopt(curl, CURLOPT_URL, "https://speech.ai.unturf.com/v1/audio/speech");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
CURLcode res = curl_easy_perform(curl);
if (res == CURLE_OK) {
printf("Audio saved to output.mp3\n");
}
fclose(fp);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return 0;
}</code></pre>
<p><strong>Available voices:</strong> <code>alloy</code>, <code>echo</code>, <code>fable</code>, <code>onyx</code>, <code>nova</code>, <code>shimmer</code></p>
<h2 id="c-source">Full SDK Implementation</h2>
<p>For a complete SDK with model discovery and advanced features, see the full implementations in the source repository:</p>
<ul>
<li><a href="/languages/c/curl/" target="_blank">C with libcurl</a></li>
<li><a href="/languages/c/libsoup/" target="_blank">C with libsoup (GNOME)</a></li>
<li><a href="/languages/c/nghttp2/" target="_blank">C with nghttp2 (HTTP/2)</a></li>
</ul>
<script>hljs.highlightAll();</script>
<footer>
<small>&copy; uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#c-intro" class="active">Introduction</a></li>
<li><a href="#c-setup">Setup</a></li>
<li><a href="#c-non-streaming">Non-Streaming</a>
<ul>
<li><a href="#c-hermes-non-stream">Hermes</a></li>
<li><a href="#c-qwen-non-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#c-streaming">Streaming</a>
<ul>
<li><a href="#c-hermes-stream">Hermes</a></li>
</ul>
</li>
<li><a href="#c-tts">Text-to-Speech</a></li>
<li><a href="#c-source">Full SDK</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

426
public/cli.html Normal file
View file

@ -0,0 +1,426 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>uncloseai-cli: Local LLM Agent | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html" class="active">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>uncloseai-cli: Local LLM Agent</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="overview">Three Tools for Growing Machine Learning from Seed</h2>
<p>The <strong>uncloseai-cli</strong> repository contains three tools that form a complete local ML pipeline: agent harness, model training, and dataset curation. Zero external dependencies. Pure Python. Public domain.</p>
<ul>
<li><strong><a href="#agent">uncloseai-cli</a></strong>: ReAct agent harness powered by Unclose Machine Learning (Llama 3.1 8B)</li>
<li><strong><a href="#microgpt">microgpt</a></strong>: Pure-Python GPT trainer and inference, zero dependencies</li>
<li><strong><a href="#garden">garden.mk</a></strong>: Smol model garden. Pull datasets, grow models, track origins</li>
</ul>
<h3 id="install">Install</h3>
<pre><code class="language-bash">git clone https://git.unturf.com/engineering/unturf/uncloseai-cli.git
cd uncloseai-cli
make install</code></pre>
<p>Creates <code>uncloseai-cli</code>, <code>unclose</code>, <code>u</code>, <code>microgpt-cli</code>, <code>microgpt</code> in <code>/usr/local/bin</code>.</p>
<p>Source: <a href="https://git.unturf.com/engineering/unturf/uncloseai-cli" target="_blank">git.unturf.com/engineering/unturf/uncloseai-cli</a></p>
<hr>
<h2 id="agent">uncloseai-cli: Local LLM Agent</h2>
<p>A minimal "Claude Code" style tool-calling agent powered by a local 8B parameter LLM. Every request flows through a todo system:</p>
<ol>
<li><strong>Plan</strong>: LLM breaks request into numbered tasks</li>
<li><strong>Trim</strong>: Python removes fluff (open/read, report/inform), caps at 5</li>
<li><strong>Execute</strong>: each task runs through a ReAct loop with tool access</li>
<li><strong>Forward</strong>: prior task results flow to later tasks as context</li>
</ol>
<h3 id="usage">Usage</h3>
<pre><code class="language-bash"># Run a task
unclose "pull and sync"
# Multi-step: agent plans and executes each step
unclose "what time is it in EST? also run ddate"
# Print-only mode (minimal output, just final answer)
unclose -p "summarize SYSTEM-PROMPT.md"
# Verbose mode (show turn numbers)
unclose -v "deploy"
# Interactive REPL
unclose -i</code></pre>
<h3 id="config">Configuration</h3>
<p>All configuration via environment variables. No config files needed:</p>
<table>
<thead>
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>UNCLOSE_BASE</code></td><td><code>https://hermes.ai.unturf.com/v1</code></td><td>OpenAI-compatible API base URL</td></tr>
<tr><td><code>UNCLOSE_MODEL</code></td><td><em>(auto-detected)</em></td><td>Model name from /v1/models</td></tr>
<tr><td><code>UNCLOSE_KEY</code></td><td><code>permacomputer</code></td><td>API key</td></tr>
<tr><td><code>UNCLOSE_MAX_TURNS</code></td><td><code>15</code></td><td>Max ReAct turns per task</td></tr>
<tr><td><code>UNCLOSE_MAX_RESULT</code></td><td><code>12000</code></td><td>Max chars per tool result</td></tr>
</tbody>
</table>
<p>Point it at any OpenAI-compatible endpoint: vLLM, Ollama, or the free <a href="/inference.html">uncloseai inference endpoints</a>. By default it connects to <a href="https://hermes.ai.unturf.com/v1/models" target="_blank">hermes.ai.unturf.com</a>. See <a href="/inference.html#model-discovery">Model Discovery</a> for all available endpoints and how to query current model IDs.</p>
<h3 id="tools">Tools</h3>
<p>The agent has access to 8 built-in tools:</p>
<table>
<thead>
<tr><th>Tool</th><th>Args</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>bash</code></td><td>command</td><td>Run a shell command (60s timeout, dangerous commands blocked)</td></tr>
<tr><td><code>read</code></td><td>path</td><td>Read a file (truncated at 12K chars)</td></tr>
<tr><td><code>write</code></td><td>path, content</td><td>Create or overwrite a file</td></tr>
<tr><td><code>edit</code></td><td>path, old, new</td><td>Replace exact string in file</td></tr>
<tr><td><code>glob</code></td><td>pattern, path</td><td>Find files by glob pattern</td></tr>
<tr><td><code>grep</code></td><td>pattern, path</td><td>Search file contents with regex</td></tr>
<tr><td><code>fetch</code></td><td>url, depth, keywords</td><td>Fetch web page, extract text (async crawler)</td></tr>
<tr><td><code>todo_add</code></td><td>content, activeForm</td><td>Add task to live todo list during execution</td></tr>
</tbody>
</table>
<h3 id="fetch">Web Fetching</h3>
<p>The <code>fetch</code> tool wraps a production-grade async web crawler:</p>
<ul>
<li><strong>Single page</strong>: fetches, strips HTML, returns readable text + links</li>
<li><strong>Deep crawl</strong>: keyword-aware multi-level crawl (up to 10 pages)</li>
<li><strong>Ethical</strong>: respects robots.txt, crawl delays, clear user-agent string</li>
<li><strong>Progressive</strong>: fetches target page first, auto-crawls deeper only if keywords aren't covered</li>
</ul>
<h3 id="session-logging">Session Logging</h3>
<p>Every session is logged as JSONL to <code>~/.uncloseai/sessions/</code>. Use <code>unclose-snoop</code> to parse session logs into a readable feed:</p>
<pre><code class="language-bash">cat ~/.uncloseai/sessions/*.jsonl | unclose-snoop</code></pre>
<hr>
<h2 id="microgpt">microgpt: Pure-Python GPT</h2>
<p>Minimal GPT training &amp; inference with <strong>zero external dependencies</strong>: only <code>os</code>, <code>math</code>, <code>random</code>, <code>json</code>, <code>argparse</code>. Based on <a href="https://gist.github.com/karpathy/8627fe009c40f57531cb18360106ce95" target="_blank">Karpathy's microgpt</a>.</p>
<p>Implements: scalar autograd (Value class), multi-head attention, RMSNorm, MLP, Adam optimizer with linear LR decay, temperature-controlled sampling. JSON model persistence with full metadata.</p>
<h3 id="microgpt-usage">Usage</h3>
<pre><code class="language-bash"># Train on a text dataset (one document per line)
microgpt train --dataset names.txt --steps 1024 --save model.json
# Generate samples from a trained model
microgpt generate --load model.json --samples 10 --temperature 0.5
# Train &amp; generate in one shot
microgpt run --dataset names.txt --steps 1024 --samples 10
# Inspect a saved model
microgpt info model.json</code></pre>
<h3 id="microgpt-flags">Flags</h3>
<table>
<thead>
<tr><th>Flag</th><th>Default</th><th>Subcommands</th></tr>
</thead>
<tbody>
<tr><td><code>--dataset PATH</code></td><td>auto-download names.txt</td><td>train, run</td></tr>
<tr><td><code>--save PATH</code></td><td>microgpt-model.json</td><td>train, run</td></tr>
<tr><td><code>--load PATH</code></td><td>(required)</td><td>generate</td></tr>
<tr><td><code>--steps N</code></td><td>1000</td><td>train, run</td></tr>
<tr><td><code>--lr FLOAT</code></td><td>0.01</td><td>train, run</td></tr>
<tr><td><code>--n-embd N</code></td><td>16</td><td>train, run</td></tr>
<tr><td><code>--n-head N</code></td><td>4</td><td>train, run</td></tr>
<tr><td><code>--n-layer N</code></td><td>1</td><td>train, run</td></tr>
<tr><td><code>--block-size N</code></td><td>16</td><td>train, run</td></tr>
<tr><td><code>--samples N</code></td><td>20</td><td>generate, run</td></tr>
<tr><td><code>--temperature FLOAT</code></td><td>0.5</td><td>generate, run</td></tr>
<tr><td><code>--seed N</code></td><td>42</td><td>all</td></tr>
<tr><td><code>--quiet</code></td><td>false</td><td>train, run</td></tr>
</tbody>
</table>
<p>Also ships as <code>microgpt.h</code>, a single-header C library for model inference.</p>
<hr>
<h2 id="garden">garden.mk: Smol Model Garden</h2>
<p>Pull 15 curated character-level datasets, grow GPT models at 3 sizes. Every dataset tracks its upstream origin. Mirrored to <a href="https://huggingface.co/datasets/russellbal/smol-seeds" target="_blank">HuggingFace: russellbal/smol-seeds</a>.</p>
<h3 id="datasets">Datasets</h3>
<table>
<thead>
<tr><th>Seed</th><th>Lines</th><th>Origin</th><th>Freshness</th></tr>
</thead>
<tbody>
<tr><td>names</td><td>32K</td><td>karpathy/makemore</td><td>Dormant (2022)</td></tr>
<tr><td>words</td><td>97K</td><td>dwyl/english-words</td><td>Stable (2025)</td></tr>
<tr><td>pokemon</td><td>1K</td><td>sindresorhus/pokemon</td><td>Stable (2024)</td></tr>
<tr><td>dinosaurs</td><td>1.5K</td><td>brunoklein99/deep-learning-notes</td><td>Fossil (2018)</td></tr>
<tr><td>hex-colors</td><td>32K</td><td>xkcd + meodai/color-names</td><td>Active</td></tr>
<tr><td>color-names</td><td>31K</td><td>meodai/color-names</td><td>Active</td></tr>
<tr><td>chords</td><td>1K</td><td>tombatossals/chords-db</td><td>Quarterly</td></tr>
<tr><td>json-keys</td><td>4K</td><td>GitHub OpenAPI spec</td><td>Active</td></tr>
<tr><td>css-classes</td><td>2K</td><td>twbs/bootstrap</td><td>Quarterly</td></tr>
<tr><td>make-targets</td><td>291</td><td>scraped from major repos</td><td>Active</td></tr>
<tr><td>commit-msgs</td><td>20K</td><td>angular/angular</td><td>Active</td></tr>
<tr><td>haiku</td><td>143K</td><td>docmarionum1/haikurnn</td><td>Fossil (2018)</td></tr>
<tr><td>variable-names</td><td>3.8K</td><td>GitHub repo trees</td><td>Active</td></tr>
<tr><td>last-names</td><td>225K</td><td>sacrificialpancakes/synthetic_demographics_seed</td><td>Stable</td></tr>
<tr><td>occupations</td><td>831</td><td>sacrificialpancakes/synthetic_demographics_seed</td><td>Stable</td></tr>
</tbody>
</table>
<h3 id="garden-usage">Usage</h3>
<pre><code class="language-bash"># Download all datasets
make -f garden.mk pull
# Train all models at embedding size 64
make -f garden.mk grow-64
# Train all at all sizes (64, 128, 512)
make -f garden.mk grow
# Pull + grow one model family
make -f garden.mk names
# List upstream sources
make -f garden.mk origins
# Check upstream freshness
make -f garden.mk freshness
# Sample from a trained model
make -f garden.mk sample MODEL=pokemon-64 N=10
# Show trained model inventory
make -f garden.mk inventory
# Push datasets to HuggingFace mirror
make -f garden.mk mirror</code></pre>
<h3 id="model-sizes">Model Sizes</h3>
<table>
<thead>
<tr><th>Size</th><th>n_embd</th><th>n_head</th><th>n_layer</th><th>Use</th></tr>
</thead>
<tbody>
<tr><td>64</td><td>64</td><td>4</td><td>1</td><td>Fast, good for testing</td></tr>
<tr><td>128</td><td>128</td><td>8</td><td>2</td><td>Balanced</td></tr>
<tr><td>512</td><td>512</td><td>8</td><td>4</td><td>Slow in pure Python, best quality</td></tr>
</tbody>
</table>
<hr>
<h2 id="architecture">Architecture</h2>
<p>The agent orchestration follows a plan-execute-forward pattern:</p>
<pre><code>User Message
Plan Phase (LLM call with planning-only prompt)
JSON task array
Trim Phase (Python, no LLM)
• drop fluff (report/inform)
• drop prep (open/read)
Save to ~/.uncloseai/todos/{session}.json
┌─→ Any pending tasks? ─── no → Display final todo list ✓
│ ↓ yes
│ Mark in_progress
│ ↓
│ ReAct Loop (tool calls until done or stuck)
│ ↓
│ Mark completed → Save → Append result (≤500 chars)
└───────┘</code></pre>
<p>Each task in the ReAct loop can call tools, receive results, and iterate up to <code>UNCLOSE_MAX_TURNS</code> times. Stuck-loop detection bails out after 2 identical consecutive tool calls.</p>
<p>Prior task results are forwarded as context to later tasks, so multi-step requests build on earlier work without re-executing commands.</p>
<h3 id="read-only">Read-Only Mode</h3>
<p>When the agent detects a read-only intent (questions, searches, inspections), it automatically disables <code>write</code> and <code>edit</code> tools. This prevents accidental file modifications on information-gathering requests.</p>
<hr>
<h2 id="license">License</h2>
<p>Public domain. Knowledge unbound by gatekeepers.</p>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#overview" class="active">Overview</a></li>
<li><a href="#agent">uncloseai-cli</a></li>
<li><a href="#tools">Tools</a></li>
<li><a href="#microgpt">microgpt</a></li>
<li><a href="#garden">garden.mk</a></li>
<li><a href="#datasets">Datasets</a></li>
<li><a href="#architecture">Architecture</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

268
public/crawler.html Normal file
View file

@ -0,0 +1,268 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Our Ethical Web Crawler | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/crawler.html" class="active">Our Crawler</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>Our Ethical Web Crawler</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="intro">About Our Web Crawler</h2>
<p>Our team uses an ethical web crawler for research and development on the open web. We believe in responsible data collection that respects website owners and follows industry best practices.</p>
<h3 id="user-agent">User Agent</h3>
<p>Our crawler identifies itself with the following user agent:</p>
<pre><code>uncloseai.com/1.42 (ethical web crawler; +https://uncloseai.com)</code></pre>
<p>This clearly identifies who we are and provides a link back to this page for more information.</p>
<h3 id="what-we-do">What We Do</h3>
<p>Our crawler is used by our AI systems to:</p>
<ul>
<li><strong>Answer questions</strong> - When users ask about websites or specific information, our bot can fetch and analyze web content to provide accurate answers</li>
<li><strong>Research & development</strong> - We study how information is structured on the web to improve our AI models and tools</li>
<li><strong>Content aggregation</strong> - We help users discover and understand information across multiple web pages</li>
</ul>
<h3 id="ethical-practices">Our Ethical Practices</h3>
<p>We follow strict ethical guidelines to ensure we're good web citizens:</p>
<h4>1. Robots.txt Compliance</h4>
<p>We <strong>always respect robots.txt</strong> files. If your site's robots.txt disallows our crawler, we won't access those pages.</p>
<pre><code># Example: Block our crawler from specific paths
User-agent: uncloseai.com
Disallow: /private/
Disallow: /admin/</code></pre>
<h4>2. Crawl Delays</h4>
<p>We respect crawl delays specified in robots.txt. Our default is <strong>2 seconds between requests</strong> to the same domain, but we'll honor any delay you specify:</p>
<pre><code># Example: Set custom crawl delay
User-agent: uncloseai.com
Crawl-delay: 5</code></pre>
<h4>3. Intelligent Caching</h4>
<p>We cache fetched content for <strong>7 days</strong> by default. This means:</p>
<ul>
<li>We won't repeatedly hit your server for the same content</li>
<li>Reduces load on your infrastructure</li>
<li>Faster responses for our users</li>
</ul>
<h4>4. Smart Depth Crawling</h4>
<p>We use intelligent depth-based crawling:</p>
<ul>
<li><strong>Depth 0</strong> - Just the homepage</li>
<li><strong>Depth 1</strong> - Homepage plus directly linked pages</li>
<li><strong>Depth 2</strong> - Goes two levels deep when needed</li>
</ul>
<p>Our crawler automatically decides how deep to go based on whether it found relevant information, so we don't waste resources fetching unnecessary pages.</p>
<h4>5. Query-Aware Relevance</h4>
<p>We score pages based on relevance to the user's question, prioritizing:</p>
<ul>
<li>Pages that contain keywords from the user's query</li>
<li>Clean, readable URLs over dynamic query strings</li>
<li>Pages with substantial, unique content</li>
<li>Descriptive titles that match the topic</li>
</ul>
<h3 id="transparency">Transparency</h3>
<p>We believe in being open about our crawling practices:</p>
<ul>
<li><strong>Clear identification</strong> - Our user agent clearly identifies us</li>
<li><strong>Contact information</strong> - This page provides details on how to reach us</li>
<li><strong>Open source approach</strong> - Our crawler follows public best practices</li>
<li><strong>No stealth crawling</strong> - We never hide our identity or bypass restrictions</li>
</ul>
<h3 id="blocking">How to Block Our Crawler</h3>
<p>If you don't want our crawler accessing your site, you can block it using robots.txt:</p>
<pre><code># Block uncloseai.com crawler entirely
User-agent: uncloseai.com
Disallow: /</code></pre>
<p>Or use your server's firewall/WAF to block our user agent string.</p>
<h3 id="contact">Questions or Concerns?</h3>
<p>If you have questions about our crawler or want to discuss our access to your site, please reach out:</p>
<ul>
<li><strong>Website</strong>: <a href="https://uncloseai.com">uncloseai.com</a></li>
<li><strong>Discord</strong>: Join our community at <a href="https://discord.com/invite/TZPmQdPj8a" target="_blank">discord.com/invite/TZPmQdPj8a</a></li>
<li><strong>Email</strong>: Contact us through our website</li>
</ul>
<h3 id="discord-bot">Try Our Discord Bot</h3>
<p>Our ethical web crawler powers the research capabilities in our Discord bot. The bot can:</p>
<ul>
<li>Answer questions by fetching and analyzing web content</li>
<li>Execute code in 42+ programming languages</li>
<li>Provide intelligent summaries of web pages</li>
<li>Respect user privacy and website policies</li>
</ul>
<p><strong>Want to try it?</strong> Check out our <a href="https://git.unturf.com/engineering/unturf/discord.agents.ai.unturf.com" target="_blank">Discord bot source code</a> to see how our ethical crawler powers the bot's research capabilities.</p>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id], h4[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#intro" class="active">About</a></li>
<li><a href="#user-agent">User Agent</a></li>
<li><a href="#what-we-do">What We Do</a></li>
<li><a href="#ethical-practices">Ethical Practices</a></li>
<li><a href="#transparency">Transparency</a></li>
<li><a href="#blocking">How to Block</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#discord-bot">Discord Bot</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

446
public/csharp-examples.html Normal file
View file

@ -0,0 +1,446 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>C# Examples | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html" class="active">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>C# Examples - Free LLM & TTS AI Service</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="csharp-intro">C# Examples</h2>
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with C#. Examples are provided for both <strong>.NET 10</strong> (latest) and <strong>Mono</strong> (cross-platform) runtimes.</p>
<p><strong>Available Endpoints:</strong></p>
<ul>
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
</ul>
<h2 id="runtimes">Runtime Options</h2>
<table>
<thead>
<tr>
<th>Runtime</th>
<th>Best For</th>
<th>Setup</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>.NET 10</strong></td>
<td>Latest features, best performance, Windows/Linux/macOS</td>
<td><code>dotnet new console</code></td>
</tr>
<tr>
<td><strong>Mono</strong></td>
<td>Legacy systems, embedded, wide platform support</td>
<td><code>mcs Program.cs</code></td>
</tr>
</tbody>
</table>
<!-- .NET 10 SECTION -->
<h2 id="dotnet10">.NET 10 Examples</h2>
<p>Using the official OpenAI .NET SDK with .NET 10. This provides the cleanest API with full async/await support.</p>
<h3 id="dotnet10-setup">Setup</h3>
<pre><code class="bash"># Create new project
dotnet new console -n UncloseAI
cd UncloseAI
# Add OpenAI package
dotnet add package OpenAI --version 2.5.0</code></pre>
<h3 id="dotnet10-non-stream">Non-Streaming (Hermes)</h3>
<pre><code class="csharp">using OpenAI.Chat;
var client = new ChatClient(
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
apiKey: "choose-any-value",
new OpenAIClientOptions
{
Endpoint = new Uri("https://hermes.ai.unturf.com/v1")
}
);
ChatCompletion completion = await client.CompleteChatAsync(
new List&lt;ChatMessage&gt;
{
new UserChatMessage("Give a Python Fizzbuzz solution in one line of code?")
},
new ChatCompletionOptions
{
Temperature = 0.5f,
MaxTokens = 150
}
);
Console.WriteLine(completion.Content[0].Text);</code></pre>
<h3 id="dotnet10-stream">Streaming (Hermes)</h3>
<pre><code class="csharp">using OpenAI.Chat;
var client = new ChatClient(
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
apiKey: "choose-any-value",
new OpenAIClientOptions
{
Endpoint = new Uri("https://hermes.ai.unturf.com/v1")
}
);
var streamingUpdates = client.CompleteChatStreamingAsync(
new List&lt;ChatMessage&gt;
{
new UserChatMessage("Write a short poem about coding")
},
new ChatCompletionOptions
{
Temperature = 0.5f,
MaxTokens = 200
}
);
await foreach (var update in streamingUpdates)
{
foreach (var contentPart in update.ContentUpdate)
{
Console.Write(contentPart.Text);
}
}</code></pre>
<h3 id="dotnet10-tts">Text-to-Speech</h3>
<pre><code class="csharp">using OpenAI.Audio;
var client = new AudioClient(
model: "tts-1",
apiKey: "choose-any-value",
new OpenAIClientOptions
{
Endpoint = new Uri("https://speech.ai.unturf.com/v1")
}
);
BinaryData speech = await client.GenerateSpeechAsync(
"Hello! This is a test of text to speech.",
GeneratedSpeechVoice.Alloy,
new SpeechGenerationOptions { Speed = 1.0f }
);
await File.WriteAllBytesAsync("speech.mp3", speech.ToArray());
Console.WriteLine("Audio saved to speech.mp3");</code></pre>
<!-- MONO SECTION -->
<h2 id="mono">Mono Examples</h2>
<p>Using HttpClient directly with Mono runtime. No external packages required - works with the built-in System.Net.Http.</p>
<h3 id="mono-setup">Setup</h3>
<pre><code class="bash"># Install Mono (Ubuntu/Debian)
sudo apt install mono-complete
# Compile
mcs -r:System.Net.Http.dll Program.cs
# Run
mono Program.exe</code></pre>
<h3 id="mono-non-stream">Non-Streaming (Hermes)</h3>
<pre><code class="csharp">using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program
{
static readonly HttpClient client = new HttpClient();
static async Task Main()
{
var payload = @"{
""model"": ""adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"",
""messages"": [{""role"": ""user"", ""content"": ""Give a Python Fizzbuzz solution in one line of code?""}],
""max_tokens"": 150
}";
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
"https://hermes.ai.unturf.com/v1/chat/completions",
content
);
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}
}</code></pre>
<h3 id="mono-stream">Streaming (Hermes)</h3>
<pre><code class="csharp">using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program
{
static readonly HttpClient client = new HttpClient();
static async Task Main()
{
var payload = @"{
""model"": ""adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"",
""messages"": [{""role"": ""user"", ""content"": ""Write a short poem about coding""}],
""max_tokens"": 200,
""stream"": true
}";
var request = new HttpRequestMessage(HttpMethod.Post, "https://hermes.ai.unturf.com/v1/chat/completions");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (line?.StartsWith("data: ") == true)
{
var json = line.Substring(6);
if (json != "[DONE]")
{
// Extract content from JSON (simplified)
var start = json.IndexOf("\"content\":\"") + 11;
if (start > 10)
{
var end = json.IndexOf("\"", start);
if (end > start)
{
Console.Write(json.Substring(start, end - start));
}
}
}
}
}
}
}</code></pre>
<h3 id="mono-tts">Text-to-Speech</h3>
<pre><code class="csharp">using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program
{
static readonly HttpClient client = new HttpClient();
static async Task Main()
{
var payload = @"{
""model"": ""tts-1"",
""input"": ""Hello! This is a test of text to speech."",
""voice"": ""alloy""
}";
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
"https://speech.ai.unturf.com/v1/audio/speech",
content
);
var audioBytes = await response.Content.ReadAsByteArrayAsync();
File.WriteAllBytes("speech.mp3", audioBytes);
Console.WriteLine("Audio saved to speech.mp3");
}
}</code></pre>
<p><strong>Available voices:</strong> <code>alloy</code>, <code>echo</code>, <code>fable</code>, <code>onyx</code>, <code>nova</code>, <code>shimmer</code></p>
<h2 id="source">Full SDK Implementations</h2>
<p>For complete SDK implementations with model discovery and advanced features:</p>
<ul>
<li><a href="/languages/csharp/dotnet10/" target="_blank">.NET 10 SDK</a></li>
<li><a href="/languages/csharp/mono/" target="_blank">Mono SDK</a></li>
<li><a href="/languages/csharp/http/" target="_blank">HttpClient (generic .NET)</a></li>
<li><a href="/languages/csharp/openai/" target="_blank">OpenAI SDK wrapper</a></li>
</ul>
<script>hljs.highlightAll();</script>
<footer>
<small>&copy; uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#csharp-intro" class="active">Overview</a></li>
<li><a href="#runtimes">Runtime Options</a></li>
<li><a href="#dotnet10">.NET 10</a>
<ul>
<li><a href="#dotnet10-setup">Setup</a></li>
<li><a href="#dotnet10-non-stream">Non-Streaming</a></li>
<li><a href="#dotnet10-stream">Streaming</a></li>
<li><a href="#dotnet10-tts">TTS</a></li>
</ul>
</li>
<li><a href="#mono">Mono</a>
<ul>
<li><a href="#mono-setup">Setup</a></li>
<li><a href="#mono-non-stream">Non-Streaming</a></li>
<li><a href="#mono-stream">Streaming</a></li>
<li><a href="#mono-tts">TTS</a></li>
</ul>
</li>
<li><a href="#source">Full SDK</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

View file

@ -0,0 +1,29 @@
/*
* This is free software for the public good of a permacomputer hosted at
* permacomputer.com, an always-on computer by the people, for the people.
* One which is durable, easy to repair, & distributed like tap water
* for machine learning intelligence.
*
* The permacomputer is community-owned infrastructure optimized around
* four values:
*
* TRUTH First principles, math & science, open source code freely distributed
* FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
* HARMONY Minimal waste, self-renewing systems with diverse thriving connections
* LOVE Be yourself without hurting others, cooperation through natural law
*
* This software contributes to that vision by making machine learning
* accessible to everyone through a free, open, embeddable chat interface.
* Code is seeds to sprout on any abandoned technology.
*/
/*! Generated by Font Squirrel (https://www.fontsquirrel.com) on October 4, 2021 */
@font-face {
font-family: "chunkfiveregular";
src:
url("chunkfive-regular-webfont.woff2") format("woff2"),
url("chunkfive-regular-webfont.woff") format("woff");
font-weight: normal;
font-style: normal;
}

2611
public/css/pico.classless.min.css vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,326 @@
/*
* This is free software for the public good of a permacomputer hosted at
* permacomputer.com, an always-on computer by the people, for the people.
* One which is durable, easy to repair, & distributed like tap water
* for machine learning intelligence.
*
* The permacomputer is community-owned infrastructure optimized around
* four values:
*
* TRUTH First principles, math & science, open source code freely distributed
* FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
* HARMONY Minimal waste, self-renewing systems with diverse thriving connections
* LOVE Be yourself without hurting others, cooperation through natural law
*
* This software contributes to that vision by making machine learning
* accessible to everyone through a free, open, embeddable chat interface.
* Code is seeds to sprout on any abandoned technology.
*/
/* CSS Grid-based sidebar navigation theme for PicoCSS */
:root {
--sidebar-width: 280px;
--content-max-width: 1200px;
--base-font-size: 16px;
--heading-scale: 1.125;
}
/* Adjust default font sizes */
html {
font-size: var(--base-font-size);
}
body {
font-size: 1rem;
line-height: 1.6;
}
h1 { font-size: calc(1rem * var(--heading-scale) * var(--heading-scale) * var(--heading-scale) * var(--heading-scale)); }
h2 { font-size: calc(1rem * var(--heading-scale) * var(--heading-scale) * var(--heading-scale)); }
h3 { font-size: calc(1rem * var(--heading-scale) * var(--heading-scale)); }
h4 { font-size: calc(1rem * var(--heading-scale)); }
h5, h6 { font-size: 1rem; }
/* Main grid layout */
body {
display: grid;
grid-template-columns: var(--sidebar-width) 1fr var(--sidebar-width);
grid-template-areas:
"sidebar main sidebar-right";
gap: 2rem;
min-height: 100vh;
margin: 0;
padding: 1rem;
/* Ensure body doesn't expand beyond viewport */
max-width: 100vw;
overflow-x: auto;
}
/* Sidebar navigation */
aside.sidebar {
grid-area: sidebar;
background: var(--card-background-color);
border-right: 1px solid var(--muted-border-color);
padding: 2rem;
position: sticky;
top: 1rem;
height: calc(100vh - 2rem);
overflow-y: auto;
overflow-x: hidden;
border-radius: 0.5rem;
}
/* Right sidebar navigation */
aside.sidebar-right {
grid-area: sidebar-right;
background: var(--card-background-color);
border-left: 1px solid var(--muted-border-color);
padding: 2rem;
position: sticky;
top: 1rem;
height: calc(100vh - 2rem);
overflow-y: auto;
overflow-x: hidden;
border-radius: 0.5rem;
}
/* Main content area */
main {
grid-area: main;
padding: 2rem;
max-width: 960px !important;
margin: 0 auto;
/* Prevent content overflow */
overflow-x: auto;
overflow-y: visible;
}
/* Responsive main content */
@media (min-width: 769px) and (max-width: 1024px) {
main {
max-width: 100% !important;
margin-right: 2rem;
}
}
@media (min-width: 1025px) and (max-width: 1440px) {
main {
max-width: 900px !important;
margin-right: 3rem;
}
}
@media (min-width: 1441px) {
main {
max-width: 960px !important;
margin-right: 4rem;
}
}
/* Large monitor adjustments - bring sidebar closer to content */
@media (min-width: 1600px) {
body {
grid-template-columns: minmax(var(--sidebar-width), auto) minmax(0, 960px) minmax(var(--sidebar-width), auto);
grid-template-areas:
"sidebar main sidebar-right";
justify-content: center;
gap: 3rem 2rem;
}
main {
margin: 0;
}
}
/* Table of contents styling */
.table-of-contents {
font-size: 0.875rem;
}
.table-of-contents h2 {
font-size: 1.125rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--muted-border-color);
}
.table-of-contents nav ul {
list-style: none;
padding-left: 0;
margin: 0;
}
.table-of-contents nav ul ul {
padding-left: 1.25rem;
margin-top: 0.25rem;
}
.table-of-contents nav li {
margin: 0.25rem 0;
}
.table-of-contents nav a {
text-decoration: none;
color: var(--color);
display: block;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.2s;
}
.table-of-contents nav a:hover {
background-color: var(--secondary-focus);
}
.table-of-contents nav a.active {
background-color: var(--primary-focus);
color: var(--primary-inverse);
}
/* Responsive behavior */
@media (max-width: 768px) {
body {
grid-template-columns: 1fr;
grid-template-areas:
"main";
padding: 0;
/* Reset max-width for mobile */
max-width: none;
}
main {
max-width: 100% !important;
margin-right: 0;
padding: 1rem;
/* Ensure proper width on mobile */
width: 100%;
overflow-x: visible;
}
aside.sidebar {
position: fixed;
left: -100%;
width: var(--sidebar-width);
z-index: 1000;
transition: left 0.3s ease;
background: var(--card-background-color);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
height: 100vh;
top: 0;
}
aside.sidebar.open {
left: 0;
box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.7);
}
/* Hide right sidebar on mobile */
aside.sidebar-right {
display: none;
}
/* Overlay effect when sidebar is open */
aside.sidebar.open::before {
content: '';
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
z-index: -1;
}
/* Mobile menu toggle */
.sidebar-toggle {
display: block;
position: fixed;
top: 1rem;
left: 1rem;
z-index: 1001;
background: var(--primary);
color: var(--primary-inverse);
border: none;
padding: 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
}
}
@media (min-width: 769px) {
.sidebar-toggle {
display: none;
}
}
/* Sidebar scrollbar styling */
aside.sidebar::-webkit-scrollbar,
aside.sidebar-right::-webkit-scrollbar {
width: 6px;
}
aside.sidebar::-webkit-scrollbar-track,
aside.sidebar-right::-webkit-scrollbar-track {
background: transparent;
}
aside.sidebar::-webkit-scrollbar-thumb,
aside.sidebar-right::-webkit-scrollbar-thumb {
background: var(--muted-color);
border-radius: 3px;
}
aside.sidebar::-webkit-scrollbar-thumb:hover,
aside.sidebar-right::-webkit-scrollbar-thumb:hover {
background: var(--color);
}
/* Make pre tags scrollable instead of expanding the page */
pre {
overflow-x: auto;
max-width: 100%;
}
/* Prevent horizontal scroll on mobile */
@media (max-width: 768px) {
/* Don't use overflow-x: hidden on body to preserve pinch-to-zoom */
html, body {
overflow-x: auto;
}
pre, code {
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
}
/* More specific selectors to prevent breaking zoom */
main > *,
aside > * {
max-width: 100%;
box-sizing: border-box;
}
/* Make header nav use full width on mobile */
header nav {
width: 100%;
}
header nav ul {
width: 100%;
display: flex;
justify-content: space-around;
}
header nav ul li {
flex: 1;
text-align: center;
}
header nav ul li a {
display: block;
width: 100%;
padding: 0.75rem 0.5rem;
}
}

349
public/dart-examples.html Normal file
View file

@ -0,0 +1,349 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Dart Examples | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html" class="active">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>Dart Examples - Free LLM & TTS AI Service</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="dart-intro">Dart Examples</h2>
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Dart/Flutter using the dart_openai community library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
<p><strong>Available Endpoints:</strong></p>
<ul>
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
</ul>
<h3 id="dart-client">Dart Client Installation</h3>
<p>Add dart_openai to your <code>pubspec.yaml</code>:</p>
<pre><code class="yaml">dependencies:
dart_openai: ^5.1.0</code></pre>
<h2 id="dart-non-streaming">Non-Streaming Examples</h2>
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
<h3 id="dart-hermes-non-stream">Using Hermes (General Purpose)</h3>
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
void main() async {
OpenAI.apiKey = "choose-any-value";
OpenAI.baseUrl = "https://hermes.ai.unturf.com/v1";
final chatCompletion = await OpenAI.instance.chat.create(
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
messages: [
OpenAIChatCompletionChoiceMessageModel(
role: OpenAIChatMessageRole.user,
content: [
OpenAIChatCompletionChoiceMessageContentItemModel.text(
"Give a Python Fizzbuzz solution in one line of code?"
),
],
),
],
temperature: 0.5,
maxTokens: 150,
);
print(chatCompletion.choices.first.message.content?.first.text);
}
</code></pre>
<h3 id="dart-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
void main() async {
OpenAI.apiKey = "choose-any-value";
OpenAI.baseUrl = "https://qwen.ai.unturf.com/v1";
final chatCompletion = await OpenAI.instance.chat.create(
model: "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M",
messages: [
OpenAIChatCompletionChoiceMessageModel(
role: OpenAIChatMessageRole.user,
content: [
OpenAIChatCompletionChoiceMessageContentItemModel.text(
"Give a Python Fizzbuzz solution in one line of code?"
),
],
),
],
temperature: 0.5,
maxTokens: 150,
);
print(chatCompletion.choices.first.message.content?.first.text);
}
</code></pre>
<h2 id="dart-streaming">Streaming Examples</h2>
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
<h3 id="dart-hermes-stream">Using Hermes (General Purpose)</h3>
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
import 'dart:io';
void main() async {
OpenAI.apiKey = "choose-any-value";
OpenAI.baseUrl = "https://hermes.ai.unturf.com/v1";
final chatStream = OpenAI.instance.chat.createStream(
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
messages: [
OpenAIChatCompletionChoiceMessageModel(
role: OpenAIChatMessageRole.user,
content: [
OpenAIChatCompletionChoiceMessageContentItemModel.text(
"Give a Python Fizzbuzz solution in one line of code?"
),
],
),
],
temperature: 0.5,
maxTokens: 150,
);
await for (final chunk in chatStream) {
final content = chunk.choices.first.delta.content;
if (content != null && content.isNotEmpty) {
stdout.write(content.first.text);
}
}
}
</code></pre>
<h3 id="dart-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
import 'dart:io';
void main() async {
OpenAI.apiKey = "choose-any-value";
OpenAI.baseUrl = "https://qwen.ai.unturf.com/v1";
final chatStream = OpenAI.instance.chat.createStream(
model: "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M",
messages: [
OpenAIChatCompletionChoiceMessageModel(
role: OpenAIChatMessageRole.user,
content: [
OpenAIChatCompletionChoiceMessageContentItemModel.text(
"Give a Python Fizzbuzz solution in one line of code?"
),
],
),
],
temperature: 0.5,
maxTokens: 150,
);
await for (final chunk in chatStream) {
final content = chunk.choices.first.delta.content;
if (content != null && content.isNotEmpty) {
stdout.write(content.first.text);
}
}
}
</code></pre>
<h2 id="dart-tts">Text-to-Speech Example</h2>
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
import 'dart:io';
void main() async {
OpenAI.apiKey = "YOLO";
OpenAI.baseUrl = "https://speech.ai.unturf.com/v1";
final speech = await OpenAI.instance.audio.createSpeech(
model: "tts-1",
input: "I think so therefore, Today is a wonderful day to grow something people love!",
voice: "alloy",
speed: 0.9,
);
final file = File("speech.mp3");
await file.writeAsBytes(speech);
print("Audio saved to: ${file.path}");
}
</code></pre>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#dart-intro" class="active">Overview</a></li>
<li><a href="#dart-client">Installation</a></li>
<li><a href="#dart-non-streaming">Non-Streaming</a>
<ul>
<li><a href="#dart-hermes-non-stream">Hermes</a></li>
<li><a href="#dart-qwen-non-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#dart-streaming">Streaming</a>
<ul>
<li><a href="#dart-hermes-stream">Hermes</a></li>
<li><a href="#dart-qwen-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#dart-tts">Text-to-Speech</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

997
public/demo.html Normal file
View file

@ -0,0 +1,997 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Demo Free LLM & Text To Speech Artificial Intelligence Service | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<style>
body {
max-width: 960px;
margin: 0 auto;
}
</style>
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
// Disable the floating button
window.UNCLOSEAI_FLOATING_BUTTON = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
<!-- Demo: System Message Customization -->
<script type="module">
import { setSystemMessageAppend } from './src/config.js';
setSystemMessageAppend(`
Additional context for this demo page:
- You're helping users explore and test AI features on uncloseai.com
- This is a demonstration page showing various AI capabilities
- Encourage users to try different features like TTS, translation, and the Hermes chat
- Be enthusiastic and helpful when explaining how the features work
- If users ask about implementation, reference the code examples on this page
`);
</script>
<!--
<script defer data-domain="ai.unturf.com" src="https://analytics.unturf.com/js/plausible.js"></script>
-->
</head>
<body>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>Welcome to uncloseai.com - Free LLM & Text To Speech Artificial Intelligence Service</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<main>
<h2>Hermes AI Integration Demo</h2>
<p>This page demonstrates all the ways you can integrate Hermes AI into your website using uncloseai.js. Each example is isolated and can be used independently.</p>
<!-- Table of Contents -->
<section>
<h3>📋 Table of Contents</h3>
<nav>
<ol>
<li><a href="#basic-installation">Basic Installation</a></li>
<li><a href="#configuration-options">Configuration Options</a></li>
<li><a href="#system-message-customization">System Message Customization</a></li>
<li><a href="#full-interface">Full AI Interface</a></li>
<li><a href="#chat-only">Chat Interface Only</a></li>
<li><a href="#feature-buttons">Feature Buttons Only</a></li>
<li><a href="#tts-only">Text-to-Speech Options</a></li>
<li><a href="#translate-only">Translation Options</a></li>
<li><a href="#translation">Translation API</a></li>
<li><a href="#read-page">Read Page Functionality</a></li>
<li><a href="#direct-api">Direct API Usage</a></li>
<li><a href="#custom-examples">Custom API Examples</a></li>
</ol>
</nav>
</section>
<!-- 1. Basic Installation -->
<section id="basic-installation">
<h3>1. 📦 Basic Installation</h3>
<p>The simplest way to add AI features to your website:</p>
<h4>📄 Code Example</h4>
<details open>
<summary><strong>Installation Code</strong></summary>
<p>Add this single line to your HTML to get a floating AI button:</p>
<pre><code class="html">&lt;script src="https://uncloseai.com/uncloseai.js" type="module"&gt;&lt;/script&gt;</code></pre>
</details>
<h4>🎯 Live Example</h4>
<p>This page demonstrates the basic installation - notice the floating AI button in the bottom right corner!</p>
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<p><strong>✅ Active Demo:</strong> The floating Hermes button on this page is created by the basic installation code above.</p>
<p>Try clicking the floating button to test the AI chat functionality!</p>
</div>
</section>
<!-- 2. Configuration Options -->
<section id="configuration-options">
<h3>2. ⚙️ Configuration Options</h3>
<p>Customize the behavior before loading the script:</p>
<h4>📄 Code Example</h4>
<details open>
<summary><strong>Configuration Code</strong></summary>
<p>Customize the behavior before loading the script:</p>
<pre><code class="html">&lt;script&gt;
// Disable custom styling (use your CSS framework)
window.UNCLOSEAI_CUSTOM_STYLING = false;
// Disable floating button (API functions only)
window.UNCLOSEAI_FLOATING_BUTTON = false;
&lt;/script&gt;
&lt;script src="https://uncloseai.com/uncloseai.js" type="module"&gt;&lt;/script&gt;</code></pre>
</details>
<h4>🎯 Live Example</h4>
<p>This page uses these configuration options:</p>
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<p><strong>✅ Active Demo:</strong> This page has:</p>
<ul>
<li><code>UNCLOSEAI_CUSTOM_STYLING = false</code> - Uses PicoCSS styling instead</li>
<li><code>UNCLOSEAI_FLOATING_BUTTON = false</code> - Floating button disabled for embedded examples</li>
</ul>
</div>
</section>
<!-- 3. System Message Customization -->
<section id="system-message-customization">
<h3>3. 💬 System Message Customization</h3>
<p>Customize the AI's behavior by appending additional instructions:</p>
<h4>📄 Code Example</h4>
<details open>
<summary><strong>System Message Customization Code</strong></summary>
<p>Customize the AI's behavior by appending instructions:</p>
<pre><code class="html">&lt;script type="module"&gt;
import { setSystemMessageAppend } from './src/config.js';
setSystemMessageAppend(`
Additional context for this demo page:
- You're helping users test AI features
- Provide clear, helpful examples
- Be encouraging about trying different features
`);
&lt;/script&gt;</code></pre>
</details>
<h4>🎯 Live Example</h4>
<p>This page uses custom system message instructions:</p>
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<p><strong>✅ Active Demo:</strong> The AI on this page has been customized to:</p>
<ul>
<li>Help users explore and test AI features</li>
<li>Provide clear, helpful examples</li>
<li>Be encouraging about trying different features</li>
</ul>
<p>Try asking the AI about the features on this page to see the customization in action!</p>
</div>
</section>
<!-- 4. Full AI Interface -->
<section id="full-interface">
<h3>4. 🤖 Full AI Interface</h3>
<p>Complete AI chat with all features enabled:</p>
<h4>📄 Code Example</h4>
<details open>
<summary><strong>Full Interface Code</strong></summary>
<p>Embed a complete AI interface with all features:</p>
<pre><code class="html">&lt;div class="uncloseai" data-features="full"&gt;&lt;/div&gt;</code></pre>
</details>
<h4>🎯 Live Example</h4>
<p>Try the full AI interface below:</p>
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<div class="uncloseai" data-features="full" id="demo-full"></div>
</div>
</section>
<!-- 5. Chat Interface Only -->
<section id="chat-only">
<h3>5. 💬 Chat Interface Only</h3>
<p>Pure chat interface without extra buttons:</p>
<h4>📄 Code Example</h4>
<details open>
<summary><strong>Chat Only Code</strong></summary>
<p>Embed just the chat interface:</p>
<pre><code class="html">&lt;div class="uncloseai" data-features="chat"&gt;&lt;/div&gt;</code></pre>
</details>
<h4>🎯 Live Example</h4>
<p>Try the chat-only interface below:</p>
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<div class="uncloseai" data-features="chat" id="demo-chat"></div>
</div>
</section>
<!-- 6. Feature Buttons Only -->
<section id="feature-buttons">
<h3>6. 🔧 Feature Buttons Only</h3>
<p>Add specific feature buttons without the full chat interface:</p>
<h4>📄 Code Example</h4>
<details open>
<summary><strong>How to add specific feature buttons</strong></summary>
<p>Mix and match any features you want:</p>
<pre><code class="html">&lt;!-- TTS and Read buttons only --&gt;
&lt;div class="uncloseai" data-features="tts,read"&gt;&lt;/div&gt;
&lt;!-- Just TTS button --&gt;
&lt;div class="uncloseai" data-features="tts"&gt;&lt;/div&gt;
&lt;!-- Just translation button --&gt;
&lt;div class="uncloseai" data-features="translate"&gt;&lt;/div&gt;
&lt;!-- Multiple features --&gt;
&lt;div class="uncloseai" data-features="tts,translate,read"&gt;&lt;/div&gt;</code></pre>
<p><strong>Available features:</strong> <code>chat</code>, <code>tts</code>, <code>translate</code>, <code>read</code>, <code>full</code></p>
</details>
<h4>🎯 Live Example</h4>
<p>Try the feature buttons below:</p>
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<div class="uncloseai" data-features="tts,read" id="demo-buttons"></div>
<p><strong>Available features:</strong> Text-to-Speech and Read Page buttons</p>
</div>
</section>
<!-- 7. Text-to-Speech Options -->
<section id="tts-only">
<h3>7. 🔊 Text-to-Speech Options</h3>
<p>Two ways to add TTS functionality to your website:</p>
<h4>📄 Code Examples</h4>
<details open>
<summary><strong>How to implement both TTS options</strong></summary>
<h5>Option 1: TTS Widget Button</h5>
<pre><code class="html">&lt;div class="uncloseai" data-features="tts"&gt;&lt;/div&gt;</code></pre>
<h5>Option 2: TTS Modal Button</h5>
<pre><code class="html">&lt;button onclick="openTTSModal()"&gt;🔊 Text to Speech&lt;/button&gt;</code></pre>
<p><strong>Note:</strong> Both require the uncloseai.js script to be loaded first:</p>
<pre><code class="html">&lt;script src="https://uncloseai.com/uncloseai.js" type="module"&gt;&lt;/script&gt;</code></pre>
</details>
<h4>🎯 Live Examples</h4>
<div style="display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
<!-- TTS Widget Button -->
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>🔊 TTS Widget Button</h5>
<p>Embedded TTS button for any content:</p>
<div class="uncloseai" data-features="tts" id="demo-tts"></div>
<p><strong>Best for:</strong> Adding TTS to specific content areas</p>
</div>
<!-- TTS Modal Button -->
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>🎤 TTS Anything Modal</h5>
<p>Standalone modal for any text input:</p>
<button onclick="openTTSModal()" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">🔊 Open TTS Modal</button>
<p><strong>Best for:</strong> General-purpose text-to-speech tool</p>
</div>
</div>
</section>
<!-- 8. Translation Options -->
<section id="translate-only">
<h3>8. 🌐 Translation Options</h3>
<p>Two ways to add translation functionality to your website:</p>
<h4>📄 Code Examples</h4>
<details open>
<summary><strong>How to implement both translation options</strong></summary>
<h5>Option 1: Translation Widget Button</h5>
<pre><code class="html">&lt;div class="uncloseai" data-features="translate"&gt;&lt;/div&gt;</code></pre>
<h5>Option 2: Translation Modal Button</h5>
<pre><code class="html">&lt;button onclick="openTranslateModal()"&gt;🌐 Translate&lt;/button&gt;</code></pre>
<p><strong>Translation Features:</strong></p>
<ul>
<li>📝 Custom text translation into 19 languages</li>
<li>📄 Full page translation with preserved formatting</li>
<li>⚙️ Advanced options and language selection</li>
<li>📋 Copy and preview translated content</li>
</ul>
<p><strong>Note:</strong> Both require the uncloseai.js script to be loaded first:</p>
<pre><code class="html">&lt;script src="https://uncloseai.com/uncloseai.js" type="module"&gt;&lt;/script&gt;</code></pre>
</details>
<h4>🎯 Live Examples</h4>
<div style="display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
<!-- Translation Widget Button -->
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>🌐 Translation Widget Button</h5>
<p>Embedded translation button for content:</p>
<div class="uncloseai" data-features="translate" id="demo-translate-modal"></div>
<p><strong>Best for:</strong> Adding translation to specific content areas</p>
</div>
<!-- Translation Modal Button -->
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>🔄 Translation Modal</h5>
<p>Standalone modal for translation tasks:</p>
<button onclick="openTranslateModal()" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">🌐 Open Translation Modal</button>
<p><strong>Best for:</strong> General-purpose translation tool</p>
</div>
</div>
</section>
<!--
Smart Dropdown section commented out - not working yet
<div style="display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>🌐 Translation Modal</h5>
<p>Traditional modal interface for translations:</p>
<div class="uncloseai" data-features="translate" id="demo-translate-modal"></div>
</div>
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>🤖 Smart AI Dropdown</h5>
<p>AI-powered quick translation:</p>
<div class="uncloseai" data-features="smart-translate" id="demo-translate-smart"></div>
</div>
</div>
-->
</section>
<!-- 9. Translation -->
<section id="translation">
<h3>9. 🌐 Translation</h3>
<p>Translate page content or custom text into 19 languages with code block preservation:</p>
<h4>📄 Code Example</h4>
<details open>
<summary><strong>Translation Code</strong></summary>
<p>Add translation capabilities to any interface:</p>
<pre><code class="javascript">// Import translation functions
import { translateText, translateCurrentPage, SUPPORTED_LANGUAGES } from './src/translation.js';
// Translate custom text
const translatedText = await translateText('Hello world!', 'es'); // Spanish
// Translate current page content
const translatedPage = await translateCurrentPage('fr'); // French
// Available languages
console.log(SUPPORTED_LANGUAGES);
// { 'en': 'English', 'es': 'Spanish', 'fr': 'French', ... }</code></pre>
</details>
<h4>🎯 Live Example</h4>
<p>Try the translation feature below - it preserves code blocks and formatting:</p>
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<div class="uncloseai" data-features="full" id="demo-translation"></div>
<p><strong>💡 Tip:</strong> The translation feature automatically detects and preserves:</p>
<ul>
<li>Code blocks (```code```)</li>
<li>Inline code (`code`)</li>
<li>URLs and links</li>
<li>HTML tags</li>
<li>Technical formatting</li>
</ul>
</div>
</section>
<!-- 10. Read Page Functionality -->
<section id="read-page">
<h3>10. 📖 Read Page Functionality</h3>
<p>Let AI read the entire webpage aloud with natural voice synthesis:</p>
<h4>📄 Code Examples</h4>
<details open>
<summary><strong>How to add read page functionality</strong></summary>
<h5>Option 1: Read Page Button in Widget</h5>
<pre><code class="html">&lt;div class="uncloseai" data-features="read"&gt;&lt;/div&gt;</code></pre>
<h5>Option 2: Standalone Read Page Button</h5>
<pre><code class="html">&lt;button onclick="readPageWithHermes()"&gt;📖 Read Page&lt;/button&gt;</code></pre>
<h5>Option 3: Custom JavaScript Integration</h5>
<pre><code class="javascript">// Call read page function programmatically
await readPageWithHermes();
// Or import and use directly
import { readPageWithHermes } from './src/page-reader.js';
await readPageWithHermes();</code></pre>
<p><strong>Note:</strong> Requires the uncloseai.js script to be loaded first:</p>
<pre><code class="html">&lt;script src="https://uncloseai.com/uncloseai.js" type="module"&gt;&lt;/script&gt;</code></pre>
</details>
<h4>🎯 Live Example</h4>
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<button onclick="readPageWithHermes()" style="padding: 15px 30px; background: #4CAF50; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: bold;">📖 Read This Page Aloud</button>
<p><strong>What it does:</strong></p>
<ul>
<li>🤖 AI processes page content for optimal speech</li>
<li>🔊 Converts to natural-sounding audio</li>
<li>⏯️ Provides play/pause controls</li>
<li>💾 Option to download MP3 file</li>
<li>📱 Works on mobile and desktop</li>
</ul>
</div>
</section>
<!-- 11. Direct API Usage -->
<section id="direct-api">
<h3>11. 🛠️ Direct API Usage</h3>
<p>Call uncloseai.js functions from your own JavaScript:</p>
<h4>📄 Code Example</h4>
<details open>
<summary><strong>Direct API Code</strong></summary>
<p>Call uncloseai.js functions from your own JavaScript:</p>
<pre><code class="javascript">// Send a message to AI
for await (const chunk of sendMessage('Your question here')) {
console.log(chunk);
}
// Convert text to speech
const result = await speakText('Hello world', 'alloy', 0.9);
document.body.appendChild(result.audio);
// Upload and analyze files - COMMENTED OUT: Not working
// const response = await uploadFile(fileInput.files[0]);
// console.log(response);
// Read page content with AI
await readPageWithHermes();</code></pre>
</details>
<h4>🎯 Live Example</h4>
<p>The custom API examples below demonstrate direct API usage:</p>
</section>
<!-- 12. Custom API Examples -->
<section id="custom-examples">
<h3>12. 🎨 Custom API Examples</h3>
<p>Build your own interface using uncloseai.js functions:</p>
<h4>📄 Code Examples</h4>
<details open>
<summary><strong>How to build custom interfaces with Direct API</strong></summary>
<h5>Custom Chat Interface</h5>
<pre><code class="javascript">// Custom chat implementation with isolated state
async function customChatExample() {
const input = document.getElementById('custom-chat-input');
const display = document.getElementById('custom-chat-display');
const message = input.value.trim();
if (!message) return;
// Add user message to display
display.innerHTML += `&lt;div&gt;&lt;strong&gt;You:&lt;/strong&gt; ${message}&lt;/div&gt;`;
input.value = '';
// Add AI thinking indicator
const thinkingDiv = document.createElement('div');
thinkingDiv.innerHTML = '&lt;strong&gt;Hermes:&lt;/strong&gt; &lt;em&gt;thinking...&lt;/em&gt;';
display.appendChild(thinkingDiv);
try {
// Import and use sendMessage function
const { sendMessage } = await import('./src/chat.js');
let response = '';
for await (const chunk of sendMessage(message)) {
response += chunk;
thinkingDiv.innerHTML = `&lt;strong&gt;Hermes:&lt;/strong&gt; ${response}`;
}
} catch (error) {
thinkingDiv.innerHTML = `&lt;strong&gt;Error:&lt;/strong&gt; ${error.message}`;
}
}</code></pre>
<h5>Custom TTS Interface</h5>
<pre><code class="javascript">// Custom TTS with voice selection
async function customTTSExample() {
const text = document.getElementById('custom-tts-text').value.trim();
const voice = document.getElementById('voice-select').value;
const resultDiv = document.getElementById('custom-tts-result');
if (!text) {
alert('Please enter some text first!');
return;
}
try {
// Import and use speakTextDirect function
const { speakTextDirect } = await import('./src/tts.js');
const result = await speakTextDirect(text, voice, 1.0);
// Create custom audio controls
const playPauseBtn = document.createElement('button');
playPauseBtn.textContent = '⏸️ Pause';
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '💾 Download';
downloadBtn.onclick = () => {
const a = document.createElement('a');
a.href = URL.createObjectURL(result.blob);
a.download = `custom-tts-${Date.now()}.mp3`;
a.click();
};
// Auto-play and handle controls
result.audio.play();
playPauseBtn.onclick = () => {
if (result.audio.paused) {
result.audio.play();
playPauseBtn.textContent = '⏸️ Pause';
} else {
result.audio.pause();
playPauseBtn.textContent = '▶️ Play';
}
};
resultDiv.appendChild(playPauseBtn);
resultDiv.appendChild(downloadBtn);
} catch (error) {
resultDiv.innerHTML = `&lt;strong&gt;Error:&lt;/strong&gt; ${error.message}`;
}
}</code></pre>
<h5>Custom Page Analysis</h5>
<pre><code class="javascript">// Custom page analysis with user prompt
async function customPageAnalysis() {
const prompt = document.getElementById('page-analysis-prompt').value.trim();
const resultDiv = document.getElementById('page-analysis-result');
if (!prompt) {
alert('Please enter a question about this page!');
return;
}
resultDiv.style.display = 'block';
resultDiv.innerHTML = '&lt;em&gt;Analyzing page...&lt;/em&gt;';
try {
// Get page content and combine with user prompt
const { sendMessage } = await import('./src/chat.js');
const pageContent = document.body.innerText.substring(0, 3000);
const fullPrompt = `Based on this page content: "${pageContent}"\\n\\nUser question: ${prompt}`;
let response = '';
for await (const chunk of sendMessage(fullPrompt)) {
response += chunk;
resultDiv.innerHTML = `&lt;strong&gt;Analysis:&lt;/strong&gt;&lt;br&gt;&lt;div&gt;${response}&lt;/div&gt;`;
}
} catch (error) {
resultDiv.innerHTML = `&lt;strong&gt;Error:&lt;/strong&gt; ${error.message}`;
}
}</code></pre>
<h5>Custom Translation Interface</h5>
<pre><code class="javascript">// Custom translation with code preservation
async function customTranslateExample() {
const text = document.getElementById('custom-translate-text').value.trim();
const targetLanguage = document.getElementById('translate-language').value;
const resultDiv = document.getElementById('custom-translate-result');
if (!text) {
alert('Please enter some text to translate!');
return;
}
resultDiv.style.display = 'block';
resultDiv.textContent = 'Translating...';
try {
// Import and use translation function
const { translateText } = await import('./src/translation.js');
const translatedText = await translateText(text, targetLanguage);
resultDiv.textContent = translatedText;
} catch (error) {
resultDiv.textContent = `Translation failed: ${error.message}`;
}
}</code></pre>
<p><strong>Available API Functions:</strong></p>
<ul>
<li><code>sendMessage(message)</code> - Chat with AI (returns async generator)</li>
<li><code>speakTextDirect(text, voice, speed)</code> - Convert text to speech</li>
<li><code>translateText(text, targetLanguage)</code> - Translate text</li>
<li><code>readPageWithHermes()</code> - Read page content aloud</li>
<li><code>openTTSModal()</code> - Open TTS modal</li>
<li><code>openTranslateModal()</code> - Open translation modal</li>
</ul>
<p><strong>Note:</strong> All functions require uncloseai.js to be loaded first:</p>
<pre><code class="html">&lt;script src="https://uncloseai.com/uncloseai.js" type="module"&gt;&lt;/script&gt;</code></pre>
</details>
<h4>🎯 Live Examples</h4>
<p>Try these custom implementations built with the Direct API:</p>
<div style="display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
<!-- Custom Chat Example -->
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>💬 Custom AI Chat</h5>
<p>Your own chat interface:</p>
<div style="border: 1px solid #eee; height: 200px; padding: 10px; margin: 10px 0; overflow-y: auto; background: #fafafa;" id="custom-chat-display"></div>
<input type="text" id="custom-chat-input" placeholder="Ask anything..." style="width: 100%; margin: 5px 0;">
<button onclick="customChatExample()" style="width: 100%;">Send Message</button>
</div>
<!-- Custom TTS Example -->
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>🔊 Custom Text-to-Speech</h5>
<p>Your own TTS interface:</p>
<textarea id="custom-tts-text" placeholder="Enter text to speak..." style="width: 100%; height: 80px; margin: 5px 0;"></textarea>
<select id="voice-select" style="width: 100%; margin: 5px 0;">
<option value="alloy">Alloy</option>
<option value="echo">Echo</option>
<option value="fable">Fable</option>
<option value="onyx">Onyx</option>
<option value="nova">Nova</option>
<option value="shimmer">Shimmer</option>
</select>
<button onclick="customTTSExample()" style="width: 100%;">Generate Speech</button>
<div id="custom-tts-result" style="margin: 10px 0;"></div>
</div>
<!-- Custom File Upload Example - DISABLED: Upload functionality under development -->
<div style="border: 2px dashed #ccc; padding: 20px; border-radius: 8px; background: rgba(128, 128, 128, 0.05); opacity: 0.6;">
<h5>📁 Custom File Analysis <span style="color: #999; font-size: 0.8em;">(Under Development)</span></h5>
<p>File upload interface (temporarily disabled):</p>
<input type="file" id="custom-file-input" style="width: 100%; margin: 5px 0;" disabled>
<button onclick="alert('File upload feature is under development')" style="width: 100%;" disabled>Analyze File (Coming Soon)</button>
<div id="custom-file-result" style="margin: 10px 0; padding: 10px; background: #f9f9f9; border-radius: 5px; display: none;"></div>
</div>
<!-- Page Analysis Example -->
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>📖 Custom Page Analysis</h5>
<p>Analyze page content with custom prompts:</p>
<input type="text" id="page-analysis-prompt" placeholder="What would you like to know about this page?" style="width: 100%; margin: 5px 0;">
<button onclick="customPageAnalysis()" style="width: 100%;">Analyze Page</button>
<div id="page-analysis-result" style="margin: 10px 0; padding: 10px; background: #f9f9f9; border-radius: 5px; display: none;"></div>
</div>
<!-- Custom Translation Example -->
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
<h5>🌐 Custom Translation</h5>
<p>Translate text with code preservation:</p>
<textarea id="custom-translate-text" placeholder="Enter text to translate (try including code like `function test()` or ```code blocks```)" style="width: 100%; height: 80px; margin: 5px 0;"></textarea>
<select id="translate-language" style="width: 100%; margin: 5px 0;">
<option value="es">Spanish</option>
<option value="zh">Chinese (Simplified)</option>
<option value="hi">Hindi</option>
<option value="fr">French</option>
<option value="ar">Arabic</option>
<option value="bn">Bengali</option>
<option value="ru">Russian</option>
<option value="pt">Portuguese</option>
<option value="ur">Urdu</option>
<option value="id">Indonesian</option>
<option value="de">German</option>
<option value="ja">Japanese</option>
<option value="sw">Swahili</option>
<option value="mr">Marathi</option>
<option value="te">Telugu</option>
<option value="tr">Turkish</option>
<option value="zh-tw">Chinese (Traditional)</option>
<option value="ko">Korean</option>
</select>
<button onclick="customTranslateExample()" style="width: 100%;">Translate Text</button>
<div id="custom-translate-result" style="margin: 10px 0; padding: 10px; background: #f9f9f9; border-radius: 5px; display: none; white-space: pre-wrap;"></div>
</div>
</div>
</section>
<script>
// Custom chat implementation with isolated state
async function customChatExample() {
const input = document.getElementById('custom-chat-input');
const display = document.getElementById('custom-chat-display');
const message = input.value.trim();
if (!message) return;
// Add user message
display.innerHTML += `<div style="margin: 5px 0; padding: 5px; background: #e3f2fd; border-radius: 3px;"><strong>You:</strong> ${message}</div>`;
input.value = '';
// Add AI thinking indicator
const thinkingDiv = document.createElement('div');
thinkingDiv.style.cssText = 'margin: 5px 0; padding: 5px; background: #f5f5f5; border-radius: 3px;';
thinkingDiv.innerHTML = '<strong>Hermes:</strong> <em>thinking...</em>';
display.appendChild(thinkingDiv);
display.scrollTop = display.scrollHeight;
try {
const { sendMessage } = await import('./src/chat.js');
let response = '';
for await (const chunk of sendMessage(message)) {
response += chunk;
thinkingDiv.innerHTML = `<strong>Hermes:</strong> ${response}`;
display.scrollTop = display.scrollHeight;
}
} catch (error) {
thinkingDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
}
}
// Custom TTS with voice selection
async function customTTSExample() {
const text = document.getElementById('custom-tts-text').value.trim();
const voice = document.getElementById('voice-select').value;
const resultDiv = document.getElementById('custom-tts-result');
const button = event.target;
if (!text) {
alert('Please enter some text first!');
return;
}
button.textContent = 'Processing...';
button.disabled = true;
resultDiv.innerHTML = '<em>Generating speech...</em>';
try {
const { speakTextDirect } = await import('./src/tts.js');
const result = await speakTextDirect(text, voice, 1.0);
// Create play/pause button
const playPauseBtn = document.createElement('button');
playPauseBtn.textContent = '⏸️ Pause';
playPauseBtn.style.margin = '2px';
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '💾 Download';
downloadBtn.style.margin = '2px';
downloadBtn.onclick = () => downloadCustomAudio();
const controls = document.createElement('div');
controls.style.margin = '10px 0';
controls.appendChild(playPauseBtn);
controls.appendChild(downloadBtn);
resultDiv.innerHTML = '';
resultDiv.appendChild(controls);
// Auto-play the audio
result.audio.play();
// Handle play/pause button
playPauseBtn.onclick = () => {
if (result.audio.paused) {
result.audio.play();
playPauseBtn.textContent = '⏸️ Pause';
} else {
result.audio.pause();
playPauseBtn.textContent = '▶️ Play';
}
};
// Handle audio end
result.audio.onended = () => {
playPauseBtn.textContent = '▶️ Play';
};
window.customAudioBlob = result.blob;
button.textContent = 'Generate Speech';
button.disabled = false;
} catch (error) {
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
button.textContent = 'Generate Speech';
button.disabled = false;
}
}
// Custom file upload with isolated state
async function customFileExample() {
const fileInput = document.getElementById('custom-file-input');
const resultDiv = document.getElementById('custom-file-result');
if (!fileInput.files[0]) {
alert('Please select a file first!');
return;
}
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<em>Analyzing file...</em>';
try {
const { uploadFile } = await import('./src/file-upload.js');
const response = await uploadFile(fileInput.files[0]);
resultDiv.innerHTML = `<strong>Analysis Result:</strong><br><div style="white-space: pre-wrap;">${response}</div>`;
fileInput.value = '';
} catch (error) {
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
}
}
// Custom page analysis with user prompt
async function customPageAnalysis() {
const promptInput = document.getElementById('page-analysis-prompt');
const resultDiv = document.getElementById('page-analysis-result');
const prompt = promptInput.value.trim();
if (!prompt) {
alert('Please enter a question about this page!');
return;
}
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<em>Analyzing page...</em>';
try {
const { sendMessage } = await import('./src/chat.js');
const pageContent = document.body.innerText.substring(0, 3000);
const fullPrompt = `Based on this page content: "${pageContent}"\n\nUser question: ${prompt}`;
let response = '';
for await (const chunk of sendMessage(fullPrompt)) {
response += chunk;
resultDiv.innerHTML = `<strong>Analysis:</strong><br><div style="white-space: pre-wrap;">${response}</div>`;
}
} catch (error) {
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
}
}
// Utility functions
function downloadCustomAudio() {
if (window.customAudioBlob) {
const a = document.createElement('a');
a.href = URL.createObjectURL(window.customAudioBlob);
a.download = `custom-tts-${Date.now()}.mp3`;
a.click();
}
}
// Custom translation example
async function customTranslateExample() {
const textArea = document.getElementById('custom-translate-text');
const languageSelect = document.getElementById('translate-language');
const resultDiv = document.getElementById('custom-translate-result');
const button = event.target;
const text = textArea.value.trim();
const targetLanguage = languageSelect.value;
if (!text) {
alert('Please enter some text to translate!');
return;
}
button.textContent = 'Processing...';
button.disabled = true;
resultDiv.style.display = 'block';
resultDiv.textContent = 'Translating...';
try {
const { translateText } = await import('./src/translation.js');
const translatedText = await translateText(text, targetLanguage);
resultDiv.textContent = translatedText;
} catch (error) {
resultDiv.textContent = `Translation failed: ${error.message}`;
} finally {
button.textContent = 'Translate Text';
button.disabled = false;
}
}
// Add Enter key support for chat input
document.addEventListener('DOMContentLoaded', function() {
const chatInput = document.getElementById('custom-chat-input');
if (chatInput) {
chatInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
customChatExample();
}
});
}
const pageInput = document.getElementById('page-analysis-prompt');
if (pageInput) {
pageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
customPageAnalysis();
}
});
}
const translateInput = document.getElementById('custom-translate-text');
if (translateInput) {
translateInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && e.ctrlKey) {
customTranslateExample();
}
});
}
});
// Ensure all modal and widget functions are available for demos
document.addEventListener('DOMContentLoaded', async function() {
try {
// Import and make all widget functions available globally
const { openTranslateModal } = await import('./src/translate-modal.js');
const { openTTSModal } = await import('./src/tts-modal.js');
const { readPageWithHermes } = await import('./src/page-reader.js');
window.openTranslateModal = openTranslateModal;
window.openTTSModal = openTTSModal;
window.readPageWithHermes = readPageWithHermes;
console.log('All modal and widget functions loaded successfully');
} catch (error) {
console.error('Failed to load widget functions:', error);
}
});
</script>
<h2>Open Source & Public Domain</h2>
<p><strong>uncloseai.</strong> is completely <strong>public domain</strong> - free as in beer, free as in freedom. AI for all, internet for all, open web for everyone.</p>
<p>📦 <strong>Source Code:</strong> <a href="https://git.unturf.com/engineering/unturf/uncloseai.com" target="_blank">https://git.unturf.com/engineering/unturf/uncloseai.com</a></p>
<p>📄 <strong>Full Documentation:</strong> See README.rst in the repository for complete technical details, architecture, and contribution guidelines.</p>
</main>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
</body>
</html>

3
public/downloads/MD5SUMS Normal file
View file

@ -0,0 +1,3 @@
75432b900e068e62b48a993d4204f3e1 uncloseai-browser-toy-chrome.zip
dedcdc258bb4b4cc658064a27b3df311 uncloseai-browser-toy-firefox.zip
3bfe6d53bebe93f7b1bffc771d51ac52 uncloseai-browser-toy-safari.zip

View file

@ -0,0 +1,3 @@
37d6dc79215e7ecf3008589689c22a3e0073418dcdec3d93418b19a51d6deca7 uncloseai-browser-toy-chrome.zip
c3b488ead0b1581e8d18f20f0e238918de0c00e45b61389e408c11b568769aba uncloseai-browser-toy-firefox.zip
cc051ba6c6b92fbcca2ca842adc7715b2633cf07623c2a7a9cd315af579763e3 uncloseai-browser-toy-safari.zip

Binary file not shown.

Binary file not shown.

Binary file not shown.

310
public/elixir-examples.html Normal file
View file

@ -0,0 +1,310 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Elixir Examples | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html" class="active">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>Elixir Examples - Free LLM & TTS AI Service</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="elixir-intro">Elixir Examples</h2>
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Elixir using the openai_ex community library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
<p><strong>Available Endpoints:</strong></p>
<ul>
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
</ul>
<h3 id="elixir-client">Elixir Client Installation</h3>
<p>Add openai_ex to your <code>mix.exs</code> dependencies:</p>
<pre><code class="elixir">def deps do
[
{:openai_ex, "~> 0.9.18"}
]
end</code></pre>
<h2 id="elixir-non-streaming">Non-Streaming Examples</h2>
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
<h3 id="elixir-hermes-non-stream">Using Hermes (General Purpose)</h3>
<pre><code class="elixir">config = %OpenaiEx.Config{
api_key: "choose-any-value",
api_url: "https://hermes.ai.unturf.com/v1"
}
{:ok, response} = OpenaiEx.ChatCompletion.create(
config,
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
messages: [
%{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"}
],
temperature: 0.5,
max_tokens: 150
)
IO.puts(response["choices"] |> List.first() |> get_in(["message", "content"]))
</code></pre>
<h3 id="elixir-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="elixir">config = %OpenaiEx.Config{
api_key: "choose-any-value",
api_url: "https://qwen.ai.unturf.com/v1"
}
{:ok, response} = OpenaiEx.ChatCompletion.create(
config,
model: "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M",
messages: [
%{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"}
],
temperature: 0.5,
max_tokens: 150
)
IO.puts(response["choices"] |> List.first() |> get_in(["message", "content"]))
</code></pre>
<h2 id="elixir-streaming">Streaming Examples</h2>
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
<h3 id="elixir-hermes-stream">Using Hermes (General Purpose)</h3>
<pre><code class="elixir">config = %OpenaiEx.Config{
api_key: "choose-any-value",
api_url: "https://hermes.ai.unturf.com/v1"
}
OpenaiEx.ChatCompletion.create_stream(
config,
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
messages: [
%{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"}
],
temperature: 0.5,
max_tokens: 150
)
|> Stream.each(fn chunk ->
content = get_in(chunk, ["choices", Access.at(0), "delta", "content"])
if content, do: IO.write(content)
end)
|> Stream.run()
</code></pre>
<h3 id="elixir-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="elixir">config = %OpenaiEx.Config{
api_key: "choose-any-value",
api_url: "https://qwen.ai.unturf.com/v1"
}
OpenaiEx.ChatCompletion.create_stream(
config,
model: "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M",
messages: [
%{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"}
],
temperature: 0.5,
max_tokens: 150
)
|> Stream.each(fn chunk ->
content = get_in(chunk, ["choices", Access.at(0), "delta", "content"])
if content, do: IO.write(content)
end)
|> Stream.run()
</code></pre>
<h2 id="elixir-tts">Text-to-Speech Example</h2>
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
<pre><code class="elixir">config = %OpenaiEx.Config{
api_key: "YOLO",
api_url: "https://speech.ai.unturf.com/v1"
}
{:ok, audio_data} = OpenaiEx.Audio.Speech.create(
config,
model: "tts-1",
voice: "alloy",
input: "I think so therefore, Today is a wonderful day to grow something people love!",
speed: 0.9
)
File.write!("speech.mp3", audio_data)
</code></pre>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#elixir-intro" class="active">Overview</a></li>
<li><a href="#elixir-client">Installation</a></li>
<li><a href="#elixir-non-streaming">Non-Streaming</a>
<ul>
<li><a href="#elixir-hermes-non-stream">Hermes</a></li>
<li><a href="#elixir-qwen-non-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#elixir-streaming">Streaming</a>
<ul>
<li><a href="#elixir-hermes-stream">Hermes</a></li>
<li><a href="#elixir-qwen-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#elixir-tts">Text-to-Speech</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

View file

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before After
Before After

391
public/go-examples.html Normal file
View file

@ -0,0 +1,391 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Go Examples | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html" class="active">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>Go Examples - Free LLM & TTS AI Service</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="go-intro">Go Examples</h2>
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Go using the official OpenAI Go client library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
<p><strong>Available Endpoints:</strong></p>
<ul>
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
</ul>
<h3 id="go-client">Go Client Installation</h3>
<p>To install the official OpenAI Go package, use <code>go get</code>:</p>
<pre><code class="bash">go get github.com/openai/openai-go/v3@v3.3.0</code></pre>
<h2 id="go-non-streaming">Non-Streaming Examples</h2>
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
<h3 id="go-hermes-non-stream">Using Hermes (General Purpose)</h3>
<pre><code class="go">package main
import (
"context"
"fmt"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
func main() {
client := openai.NewClient(
option.WithBaseURL("https://hermes.ai.unturf.com/v1"),
option.WithAPIKey("choose-any-value"),
)
response, err := client.Chat.Completions.New(context.Background(), openai.ChatCompletionNewParams{
Model: openai.F("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"),
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Give a Python Fizzbuzz solution in one line of code?"),
}),
Temperature: openai.Float(0.5),
MaxTokens: openai.Int(150),
})
if err != nil {
panic(err)
}
fmt.Println(response.Choices[0].Message.Content)
}
</code></pre>
<h3 id="go-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="go">package main
import (
"context"
"fmt"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
func main() {
client := openai.NewClient(
option.WithBaseURL("https://qwen.ai.unturf.com/v1"),
option.WithAPIKey("choose-any-value"),
)
response, err := client.Chat.Completions.New(context.Background(), openai.ChatCompletionNewParams{
Model: openai.F("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"),
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Give a Python Fizzbuzz solution in one line of code?"),
}),
Temperature: openai.Float(0.5),
MaxTokens: openai.Int(150),
})
if err != nil {
panic(err)
}
fmt.Println(response.Choices[0].Message.Content)
}
</code></pre>
<h2 id="go-streaming">Streaming Examples</h2>
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
<h3 id="go-hermes-stream">Using Hermes (General Purpose)</h3>
<pre><code class="go">package main
import (
"context"
"fmt"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
func main() {
client := openai.NewClient(
option.WithBaseURL("https://hermes.ai.unturf.com/v1"),
option.WithAPIKey("choose-any-value"),
)
stream := client.Chat.Completions.NewStreaming(context.Background(), openai.ChatCompletionNewParams{
Model: openai.F("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"),
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Give a Python Fizzbuzz solution in one line of code?"),
}),
Temperature: openai.Float(0.5),
MaxTokens: openai.Int(150),
})
for stream.Next() {
chunk := stream.Current()
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
fmt.Print(chunk.Choices[0].Delta.Content)
}
}
if err := stream.Err(); err != nil {
panic(err)
}
}
</code></pre>
<h3 id="go-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="go">package main
import (
"context"
"fmt"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
func main() {
client := openai.NewClient(
option.WithBaseURL("https://qwen.ai.unturf.com/v1"),
option.WithAPIKey("choose-any-value"),
)
stream := client.Chat.Completions.NewStreaming(context.Background(), openai.ChatCompletionNewParams{
Model: openai.F("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"),
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Give a Python Fizzbuzz solution in one line of code?"),
}),
Temperature: openai.Float(0.5),
MaxTokens: openai.Int(150),
})
for stream.Next() {
chunk := stream.Current()
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
fmt.Print(chunk.Choices[0].Delta.Content)
}
}
if err := stream.Err(); err != nil {
panic(err)
}
}
</code></pre>
<h2 id="go-tts">Text-to-Speech Example</h2>
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
<pre><code class="go">package main
import (
"context"
"io"
"os"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
func main() {
client := openai.NewClient(
option.WithBaseURL("https://speech.ai.unturf.com/v1"),
option.WithAPIKey("YOLO"),
)
response, err := client.Audio.Speech.New(context.Background(), openai.AudioSpeechNewParams{
Model: openai.F(openai.SpeechModelTTS1),
Voice: openai.F(openai.AudioSpeechNewParamsVoiceAlloy),
Input: openai.F("I think so therefore, Today is a wonderful day to grow something people love!"),
Speed: openai.Float(0.9),
})
if err != nil {
panic(err)
}
defer response.Body.Close()
file, err := os.Create("speech.mp3")
if err != nil {
panic(err)
}
defer file.Close()
_, err = io.Copy(file, response.Body)
if err != nil {
panic(err)
}
}
</code></pre>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#go-intro" class="active">Overview</a></li>
<li><a href="#go-client">Installation</a></li>
<li><a href="#go-non-streaming">Non-Streaming</a>
<ul>
<li><a href="#go-hermes-non-stream">Hermes</a></li>
<li><a href="#go-qwen-non-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#go-streaming">Streaming</a>
<ul>
<li><a href="#go-hermes-stream">Hermes</a></li>
<li><a href="#go-qwen-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#go-tts">Text-to-Speech</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

View file

@ -0,0 +1 @@
google-site-verification: google2dd95a728d9c0d9f.html

297
public/index.html Normal file
View file

@ -0,0 +1,297 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Using Free LLM & Text To Speech Artificial Intelligence Service | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
// Reload Remarkbox iframe with new theme if it exists
var iframe = document.getElementById('remarkbox-iframe');
if (iframe && window.buildRbSrc) {
iframe.src = buildRbSrc();
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
<!--
<script defer data-domain="ai.unturf.com" src="https://analytics.unturf.com/js/plausible.js"></script>
-->
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/" class="active">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>Welcome to uncloseai.com - Free LLM & Text To Speech Artificial Intelligence Service</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="introduction">Introducing uncloseai & Hermes & Qwen & TTS Speech Endpoints</h2>
<p>At <strong>uncloseai.</strong>, we offer free AI services powered by multiple AI models and a TTS (Text-to-Speech) endpoint. Our mission is to provide accessible AI tools for everyone, embodying the principles of both free as in beer & free as in freedom. You can interact with our models without any cost, and you are encouraged to contribute and build upon the open-source code & models that we use.</p>
<p><strong>Available Endpoints:</strong></p>
<ul>
<li><a href="https://hermes.ai.unturf.com/v1/models" target="_blank">hermes.ai.unturf.com/v1</a> - General purpose conversational AI</li>
<li><a href="https://qwen.ai.unturf.com/v1/models" target="_blank">qwen.ai.unturf.com/v1</a> - Specialized coding model</li>
<li><a href="https://speech.ai.unturf.com/v1/models" target="_blank">speech.ai.unturf.com/v1</a> - Text-to-speech</li>
</ul>
<p>See our <a href="/inference.html#model-discovery">Model Discovery docs</a> to query the current model IDs being hosted.</p>
<p>We intend to be a drop in replacement, you can use the existing open source OpenAI client to communicate with us.</p>
<h2 id="client-side">uncloseai.js - Client-Side AI for Everyone</h2>
<p>This page includes the <a href="/uncloseai-js.html">uncloseai.js</a> script which adds a floating "uncloseai." button in the bottom right corner. Click it to interact with Hermes AI directly from your browser.</p>
<p>This is a <strong>web client-only solution</strong> - no server required. Add AI capabilities to any static site or CDN with just one line of code.</p>
<h3 id="installation">✨ One-Line Installation</h3>
<pre><code class="html">&lt;script src="https://uncloseai.com/uncloseai.js" type="module"&gt;&lt;/script&gt;</code></pre>
<p><a href="/uncloseai-js.html"><strong>📖 Read the full uncloseai.js documentation →</strong></a></p>
<h3 id="browser-toys">🧩 Browser Toys</h3>
<p>Want the uncloseai. button on <strong>every</strong> web page without adding any code? Install a browser extension. Lightweight wrappers that inject uncloseai.js into every page you visit. No server, no accounts. Chat history stays in your browser's localStorage, scoped per domain.</p>
<ul>
<li><strong>Chrome / Edge / Brave / Vivaldi / Arc</strong> — Manifest V3</li>
<li><strong>Firefox</strong> — Manifest V2</li>
<li><strong>Safari</strong> — Web Extension (via Xcode)</li>
</ul>
<p><a href="/browser-toys.html"><strong>🧩 Get Browser Toys →</strong></a></p>
<h2 id="examples">Code Examples</h2>
<p>We provide comprehensive code examples for using the <strong>uncloseai.</strong> API with popular programming languages. Each example demonstrates both streaming and non-streaming modes with Hermes (general purpose AI) and Qwen 3 Coder (specialized coding model).</p>
<h3>Quick Start Examples:</h3>
<ul>
<li><a href="/python-examples.html"><strong>Python Examples</strong></a> - Complete examples using the OpenAI Python client library</li>
<li><a href="/nodejs-examples.html"><strong>Node.js Examples</strong></a> - Complete examples using the OpenAI Node.js client library</li>
</ul>
<h3>42 Programming Languages:</h3>
<p>We provide SDK implementations with streaming support for 42 programming languages! Browse all language examples:</p>
<p>🔗 <a href="/languages" target="_blank"><strong>Browse All 42 Language Examples →</strong></a></p>
<p>Includes: AWK, Bash, C, C++, C#, Clojure, COBOL, Common Lisp, Crystal, D, Dart, Deno, Elixir, Erlang, F#, Fortran, Go, Groovy, Haskell, Java, JavaScript, Julia, Kotlin, Lua, Mojo*, Nim, OCaml, Odin, Perl, PHP, PowerShell, Prolog, Python, R, Ruby, Rust, Scala, Scheme, Tcl, V, VB.NET, and Zig!</p>
<p><small>* Mojo implementation is non-streaming only (Lightbug HTTP library doesn't yet support SSE streaming as of 2025-10-15)</small></p>
<h3>uncloseai. book - Machine Learning Reference Guide:</h3>
<p>📚 <a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank"><strong>Purchase the uncloseai. book on unturf.com Shop →</strong></a></p>
<p>Comprehensive reference guide covering inference clients, streaming implementations, and best practices for all 42 programming languages.</p>
<p>All examples use the OpenAI-compatible API interface, making it easy to integrate with existing code. Simply change the <code>base_url</code> to point to our endpoints. See our <a href="/inference.html#model-discovery">Model Discovery docs</a> for endpoint URLs and how to query current model IDs.</p>
<h2 id="inference">How we run inference</h2>
<p>Want to contribute idle GPU time or reproduce everything in your own cluster?</p>
<p><a href="/inference.html"><strong>📖 Read the full inference setup documentation →</strong></a></p>
<h2 id="open-source">Open Source & Public Domain</h2>
<p><strong>uncloseai.</strong> is completely <strong>public domain</strong> - free as in beer, free as in freedom. AI for all, internet for all, open web for everyone.</p>
<p>📦 <strong>Source Code:</strong> <a href="https://git.unturf.com/engineering/unturf/uncloseai.com" target="_blank">https://git.unturf.com/engineering/unturf/uncloseai.com</a></p>
<p>📄 <strong>Full Documentation:</strong> See README.rst in the repository for complete technical details, architecture, and contribution guidelines.</p>
<h2 id="discussions">Questions & Comments & Discussions</h2>
Use the <a href="https://www.remarkbox.com" target="_blank">Remarkbox</a> below to tell us what you think!
<div id="remarkbox-div">
<noscript>
<iframe id=remarkbox-iframe src="https://my.remarkbox.com/embed?nojs=true" style="height:600px;width:100%;border:none!important" tabindex=0></iframe>
</noscript>
</div>
<script src="https://my.remarkbox.com/static/js/iframe-resizer/iframeResizer.min.js"></script>
<script>
var rb_owner_key = "944c8dfa-8b2b-11ef-af0e-29ab4fb285a0";
var thread_uri = window.location.href;
var thread_title = window.document.title;
var thread_fragment = window.location.hash;
function getCurrentTheme() {
// Check if data-theme is set on documentElement
var theme = document.documentElement.getAttribute('data-theme');
if (theme) {
return theme; // 'light' or 'dark'
}
// If auto, detect system preference
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function buildRbSrc() {
var mode = getCurrentTheme();
return "https://my.remarkbox.com/embed" +
"?rb_owner_key=" + rb_owner_key +
"&thread_title=" + encodeURI(thread_title) +
"&thread_uri=" + encodeURIComponent(thread_uri) +
"&mode=" + mode +
thread_fragment;
}
var rb_src = buildRbSrc();
function create_remarkbox_iframe() {
var ifrm = document.createElement("iframe");
ifrm.setAttribute("id", "remarkbox-iframe");
ifrm.setAttribute("scrolling", "no");
ifrm.setAttribute("src", rb_src);
ifrm.setAttribute("frameborder", "0");
ifrm.setAttribute("tabindex", "0");
ifrm.setAttribute("title", "Remarkbox");
ifrm.style.width = "100%";
document.getElementById("remarkbox-div").appendChild(ifrm);
}
create_remarkbox_iframe();
iFrameResize(
{
checkOrigin: ["https://my.remarkbox.com"],
inPageLinks: true,
initCallback: function(e) { e.iFrameResizer.moveToAnchor(thread_fragment) }
},
document.getElementById("remarkbox-iframe")
);
</script>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
</body>
</html>

298
public/inference.html Normal file
View file

@ -0,0 +1,298 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>How We Run Inference | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html" class="active">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>How We Run Inference</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="inference-intro">How we run inference</h2>
<p>This section is optional. This is only if you wanted to try to contribute idle GPU time to the project or if you wanted to reproduce everything in your own cluster.</p>
<h3 id="vllm-setup">vLLM Setup</h3>
<p>We use vLLM to run models, generally with full f16 safetensors. We make sure to use a virtualenv to hold the dependencies.</p>
<p><strong>Note:</strong> For Hermes, we use an FP8 quant by adamo1139 (adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic), which is optimized for 4090 and 3090 GPUs.</p>
<p><strong>Tool Calling:</strong> The <code>--enable-auto-tool-choice</code> and <code>--tool-call-parser hermes</code> flags are required for Hermes 3 to support function calling via the OpenAI-compatible API. Without these flags, tool calling requests will fail.</p>
<p>We are considering supporting ollama for better quant support.</p>
<p>Stand up a replica cluster on a new domain.</p>
<pre><code>sudo apt-get install gcc python3.12-dev python3.12-venv
cd ~
python3 -m venv env
source env/bin/activate
pip install vllm
python -m vllm.entrypoints.openai.api_server --model adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic --host 0.0.0.0 --port 18888 --max-model-len 82000 --enable-auto-tool-choice --tool-call-parser hermes
</code></pre>
<h3 id="proxy-setup">Proxy Setup</h3>
<p>If you want to see how we setup the proxy, check out <a href="https://git.unturf.com/-/snippets/3" target="_blank">/etc/caddy/Caddyfile</a></p>
<pre><code>hermes.ai.unturf.com {
reverse_proxy &lt;removed&gt;:18888
log {
output file /var/log/caddy/hermes.ai.unturf.com.log {
roll_size 50mb
roll_keep 5
}
}
tls {
on_demand
}
}
qwen.ai.unturf.com {
reverse_proxy &lt;removed&gt;:18889
log {
output file /var/log/caddy/qwen.ai.unturf.com.log {
roll_size 50mb
roll_keep 5
}
}
tls {
on_demand
}
}
qwen-vl.ai.unturf.com {
reverse_proxy &lt;removed&gt;:18890
log {
output file /var/log/caddy/qwen-vl.ai.unturf.com.log {
roll_size 50mb
roll_keep 5
}
}
tls {
on_demand
}
}
speech.ai.unturf.com {
reverse_proxy &lt;removed&gt;:8000
log {
output file /var/log/caddy/speech.ai.unturf.com.log {
roll_size 50mb
roll_keep 5
}
}
tls {
on_demand
}
}
</code></pre>
<h3 id="model-discovery">Model Discovery</h3>
<p>vLLM provides an OpenAI-compatible API with built-in documentation. You can discover available models and explore the full API using these endpoints:</p>
<h4>Swagger Documentation</h4>
<p>Access the interactive API docs at the <code>/docs</code> endpoint:</p>
<p><a href="https://hermes.ai.unturf.com/docs" target="_blank">hermes.ai.unturf.com/docs</a> - Hermes endpoint Swagger docs</p>
<p><a href="https://qwen.ai.unturf.com/docs" target="_blank">qwen.ai.unturf.com/docs</a> - Qwen endpoint Swagger docs</p>
<p><a href="https://qwen-vl.ai.unturf.com/docs" target="_blank">qwen-vl.ai.unturf.com/docs</a> - Qwen VL endpoint Swagger docs</p>
<p>The Swagger UI lets you explore all available endpoints, see request/response schemas, and test API calls directly in your browser.</p>
<h4>Model Discovery</h4>
<p>To get the current model ID being hosted, query the <code>/v1/models</code> endpoint:</p>
<ul>
<li><a href="https://hermes.ai.unturf.com/v1/models" target="_blank">hermes.ai.unturf.com/v1/models</a> - Hermes models</li>
<li><a href="https://qwen.ai.unturf.com/v1/models" target="_blank">qwen.ai.unturf.com/v1/models</a> - Qwen models</li>
<li><a href="https://qwen-vl.ai.unturf.com/v1/models" target="_blank">qwen-vl.ai.unturf.com/v1/models</a> - Qwen VL (vision) models</li>
</ul>
<p>Or via curl:</p>
<pre><code class="language-bash">curl https://hermes.ai.unturf.com/v1/models
curl https://qwen.ai.unturf.com/v1/models
curl https://qwen-vl.ai.unturf.com/v1/models
</code></pre>
<p>Example response:</p>
<pre><code class="language-json">{
"object": "list",
"data": [
{
"id": "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
"object": "model",
"created": 1735689600,
"owned_by": "vllm",
"root": "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
"max_model_len": 82000
}
]
}
</code></pre>
<p>The <code>id</code> field contains the model name you should use in your API calls. The <code>max_model_len</code> field tells you the maximum context length supported.</p>
<p><strong>Tip:</strong> Always query <code>/v1/models</code> programmatically rather than hardcoding model names. This ensures your code works even when models are updated or swapped.</p>
<h3 id="rate-limiting">Rate Limiting</h3>
<p>Rate limiting is configured based on client IP address: 3 requests per second per IP per endpoint.</p>
<h3 id="next-steps">Next Steps</h3>
<p>Ready to add text-to-speech to your application?</p>
<p><a href="/text-to-speech.html"><strong>📖 Read the Text-to-Speech documentation →</strong></a></p>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#inference-intro" class="active">Overview</a></li>
<li><a href="#vllm-setup">vLLM Setup</a></li>
<li><a href="#proxy-setup">Proxy Setup</a></li>
<li><a href="#model-discovery">Model Discovery</a></li>
<li><a href="#rate-limiting">Rate Limiting</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

384
public/java-examples.html Normal file
View file

@ -0,0 +1,384 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Java Examples | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html" class="active">Java Examples</a></li>
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>Java Examples - Free LLM & TTS AI Service</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="java-intro">Java Examples</h2>
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Java using the official OpenAI Java library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
<p><strong>Available Endpoints:</strong></p>
<ul>
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
</ul>
<h3 id="java-client">Java Client Installation</h3>
<p>Add the official OpenAI Java library to your Maven <code>pom.xml</code>:</p>
<pre><code class="xml">&lt;dependency&gt;
&lt;groupId&gt;com.openai&lt;/groupId&gt;
&lt;artifactId&gt;openai-java&lt;/artifactId&gt;
&lt;version&gt;4.3.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<p>Or for Gradle:</p>
<pre><code class="groovy">implementation 'com.openai:openai-java:4.3.0'</code></pre>
<h2 id="java-non-streaming">Non-Streaming Examples</h2>
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
<h3 id="java-hermes-non-stream">Using Hermes (General Purpose)</h3>
<pre><code class="java">import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.*;
import java.util.List;
public class HermesExample {
public static void main(String[] args) {
OpenAIClient client = OpenAIOkHttpClient.builder()
.apiKey("choose-any-value")
.baseURL("https://hermes.ai.unturf.com/v1")
.build();
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
.addMessage(ChatCompletionMessageParam.ofChatCompletionUserMessageParam(
ChatCompletionUserMessageParam.builder()
.role(ChatCompletionUserMessageParam.Role.USER)
.content(ChatCompletionUserMessageParam.Content.ofTextContent(
"Give a Python Fizzbuzz solution in one line of code?"
))
.build()
))
.temperature(0.5)
.maxTokens(150L)
.build();
ChatCompletion completion = client.chat().completions().create(params);
System.out.println(completion.choices().get(0).message().content().get());
}
}
</code></pre>
<h3 id="java-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="java">import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.*;
import java.util.List;
public class QwenExample {
public static void main(String[] args) {
OpenAIClient client = OpenAIOkHttpClient.builder()
.apiKey("choose-any-value")
.baseURL("https://qwen.ai.unturf.com/v1")
.build();
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
.addMessage(ChatCompletionMessageParam.ofChatCompletionUserMessageParam(
ChatCompletionUserMessageParam.builder()
.role(ChatCompletionUserMessageParam.Role.USER)
.content(ChatCompletionUserMessageParam.Content.ofTextContent(
"Give a Python Fizzbuzz solution in one line of code?"
))
.build()
))
.temperature(0.5)
.maxTokens(150L)
.build();
ChatCompletion completion = client.chat().completions().create(params);
System.out.println(completion.choices().get(0).message().content().get());
}
}
</code></pre>
<h2 id="java-streaming">Streaming Examples</h2>
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
<h3 id="java-hermes-stream">Using Hermes (General Purpose)</h3>
<pre><code class="java">import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.*;
public class HermesStreamExample {
public static void main(String[] args) {
OpenAIClient client = OpenAIOkHttpClient.builder()
.apiKey("choose-any-value")
.baseURL("https://hermes.ai.unturf.com/v1")
.build();
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
.addMessage(ChatCompletionMessageParam.ofChatCompletionUserMessageParam(
ChatCompletionUserMessageParam.builder()
.role(ChatCompletionUserMessageParam.Role.USER)
.content(ChatCompletionUserMessageParam.Content.ofTextContent(
"Give a Python Fizzbuzz solution in one line of code?"
))
.build()
))
.temperature(0.5)
.maxTokens(150L)
.build();
client.chat().completions().createStreaming(params)
.forEach(chunk -> {
String content = chunk.choices().get(0).delta().content().orElse("");
if (!content.isEmpty()) {
System.out.print(content);
}
});
}
}
</code></pre>
<h3 id="java-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="java">import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.*;
public class QwenStreamExample {
public static void main(String[] args) {
OpenAIClient client = OpenAIOkHttpClient.builder()
.apiKey("choose-any-value")
.baseURL("https://qwen.ai.unturf.com/v1")
.build();
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
.addMessage(ChatCompletionMessageParam.ofChatCompletionUserMessageParam(
ChatCompletionUserMessageParam.builder()
.role(ChatCompletionUserMessageParam.Role.USER)
.content(ChatCompletionUserMessageParam.Content.ofTextContent(
"Give a Python Fizzbuzz solution in one line of code?"
))
.build()
))
.temperature(0.5)
.maxTokens(150L)
.build();
client.chat().completions().createStreaming(params)
.forEach(chunk -> {
String content = chunk.choices().get(0).delta().content().orElse("");
if (!content.isEmpty()) {
System.out.print(content);
}
});
}
}
</code></pre>
<h2 id="java-tts">Text-to-Speech Example</h2>
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
<pre><code class="java">import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.*;
import java.io.FileOutputStream;
import java.io.IOException;
public class TTSExample {
public static void main(String[] args) throws IOException {
OpenAIClient client = OpenAIOkHttpClient.builder()
.apiKey("YOLO")
.baseURL("https://speech.ai.unturf.com/v1")
.build();
SpeechCreateParams params = SpeechCreateParams.builder()
.model(SpeechModel.TTS_1)
.voice(SpeechCreateParams.Voice.ALLOY)
.input("I think so therefore, Today is a wonderful day to grow something people love!")
.speed(0.9)
.build();
byte[] audioBytes = client.audio().speech().create(params);
try (FileOutputStream fos = new FileOutputStream("speech.mp3")) {
fos.write(audioBytes);
}
}
}
</code></pre>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#java-intro" class="active">Overview</a></li>
<li><a href="#java-client">Installation</a></li>
<li><a href="#java-non-streaming">Non-Streaming</a>
<ul>
<li><a href="#java-hermes-non-stream">Hermes</a></li>
<li><a href="#java-qwen-non-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#java-streaming">Streaming</a>
<ul>
<li><a href="#java-hermes-stream">Hermes</a></li>
<li><a href="#java-qwen-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#java-tts">Text-to-Speech</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

375
public/kotlin-examples.html Normal file
View file

@ -0,0 +1,375 @@
<!--
This is free software for the public good of a permacomputer hosted at
permacomputer.com, an always-on computer by the people, for the people.
One which is durable, easy to repair, & distributed like tap water
for machine learning intelligence.
The permacomputer is community-owned infrastructure optimized around
four values:
TRUTH First principles, math & science, open source code freely distributed
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
LOVE Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by making machine learning
accessible to everyone through a free, open, embeddable chat interface.
Code is seeds to sprout on any abandoned technology.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Kotlin Examples | uncloseai.com</title>
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
<!-- ChunkFive Font -->
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
<!-- Sidebar Theme -->
<link rel="stylesheet" href="/css/sidebar-theme.css">
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Theme Switcher Script -->
<script>
function switchTheme(theme) {
if (theme === "auto") {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Disable custom styling for uncloseai.js - use PicoCSS instead
window.UNCLOSEAI_CUSTOM_STYLING = false;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head>
<body>
<!-- Mobile menu toggle -->
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
</button>
<!-- Left sidebar - Main Navigation -->
<aside class="sidebar">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/c-examples.html">C Examples</a></li>
<li><a href="/csharp-examples.html">C# Examples</a></li>
<li><a href="/dart-examples.html">Dart Examples</a></li>
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
<li><a href="/go-examples.html">Go Examples</a></li>
<li><a href="/java-examples.html">Java Examples</a></li>
<li><a href="/kotlin-examples.html" class="active">Kotlin Examples</a></li>
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
<li><a href="/php-examples.html">PHP Examples</a></li>
<li><a href="/python-examples.html">Python Examples</a></li>
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
<li><a href="/rust-examples.html">Rust Examples</a></li>
<li><a href="/swift-examples.html">Swift Examples</a></li>
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
<li><a href="/cli.html">uncloseai-cli</a></li>
<li><a href="/browser-toys.html">Browser Toys</a></li>
<li><a href="/inference.html">Inference Setup</a></li>
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
<li><a href="/crawler.html">Our Crawler</a></li>
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
</ul>
</nav>
</div>
</aside>
<!-- Main content -->
<main>
<header>
<hgroup>
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
<p>Kotlin Examples - Free LLM & TTS AI Service</p>
</hgroup>
<nav>
<ul>
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
</ul>
</nav>
</header>
<h2 id="kotlin-intro">Kotlin Examples</h2>
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Kotlin using the openai-kotlin community library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
<p><strong>Available Endpoints:</strong></p>
<ul>
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
</ul>
<h3 id="kotlin-client">Kotlin Client Installation</h3>
<p>Add openai-kotlin to your <code>build.gradle.kts</code>:</p>
<pre><code class="kotlin">dependencies {
implementation("com.aallam.openai:openai-client:3.8.2")
implementation("io.ktor:ktor-client-okhttp:2.3.12")
}</code></pre>
<h2 id="kotlin-non-streaming">Non-Streaming Examples</h2>
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
<h3 id="kotlin-hermes-non-stream">Using Hermes (General Purpose)</h3>
<pre><code class="kotlin">import com.aallam.openai.api.chat.*
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
suspend fun main() {
val openAI = OpenAI(
OpenAIConfig(
token = "choose-any-value",
host = OpenAIHost("https://hermes.ai.unturf.com/v1")
)
)
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"),
messages = listOf(
ChatMessage(
role = ChatRole.User,
content = "Give a Python Fizzbuzz solution in one line of code?"
)
),
temperature = 0.5,
maxTokens = 150
)
val completion = openAI.chatCompletion(chatCompletionRequest)
println(completion.choices[0].message.content)
}
</code></pre>
<h3 id="kotlin-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="kotlin">import com.aallam.openai.api.chat.*
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
suspend fun main() {
val openAI = OpenAI(
OpenAIConfig(
token = "choose-any-value",
host = OpenAIHost("https://qwen.ai.unturf.com/v1")
)
)
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"),
messages = listOf(
ChatMessage(
role = ChatRole.User,
content = "Give a Python Fizzbuzz solution in one line of code?"
)
),
temperature = 0.5,
maxTokens = 150
)
val completion = openAI.chatCompletion(chatCompletionRequest)
println(completion.choices[0].message.content)
}
</code></pre>
<h2 id="kotlin-streaming">Streaming Examples</h2>
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
<h3 id="kotlin-hermes-stream">Using Hermes (General Purpose)</h3>
<pre><code class="kotlin">import com.aallam.openai.api.chat.*
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import kotlinx.coroutines.flow.collect
suspend fun main() {
val openAI = OpenAI(
OpenAIConfig(
token = "choose-any-value",
host = OpenAIHost("https://hermes.ai.unturf.com/v1")
)
)
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"),
messages = listOf(
ChatMessage(
role = ChatRole.User,
content = "Give a Python Fizzbuzz solution in one line of code?"
)
),
temperature = 0.5,
maxTokens = 150
)
openAI.chatCompletions(chatCompletionRequest).collect { chunk ->
chunk.choices.forEach { choice ->
choice.delta.content?.let { print(it) }
}
}
}
</code></pre>
<h3 id="kotlin-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
<pre><code class="kotlin">import com.aallam.openai.api.chat.*
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import kotlinx.coroutines.flow.collect
suspend fun main() {
val openAI = OpenAI(
OpenAIConfig(
token = "choose-any-value",
host = OpenAIHost("https://qwen.ai.unturf.com/v1")
)
)
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"),
messages = listOf(
ChatMessage(
role = ChatRole.User,
content = "Give a Python Fizzbuzz solution in one line of code?"
)
),
temperature = 0.5,
maxTokens = 150
)
openAI.chatCompletions(chatCompletionRequest).collect { chunk ->
chunk.choices.forEach { choice ->
choice.delta.content?.let { print(it) }
}
}
}
</code></pre>
<h2 id="kotlin-tts">Text-to-Speech Example</h2>
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
<pre><code class="kotlin">import com.aallam.openai.api.audio.SpeechRequest
import com.aallam.openai.api.audio.Voice
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import java.io.File
suspend fun main() {
val openAI = OpenAI(
OpenAIConfig(
token = "YOLO",
host = OpenAIHost("https://speech.ai.unturf.com/v1")
)
)
val speechRequest = SpeechRequest(
model = ModelId("tts-1"),
input = "I think so therefore, Today is a wonderful day to grow something people love!",
voice = Voice.Alloy,
speed = 0.9
)
val speech = openAI.speech(speechRequest)
File("speech.mp3").writeBytes(speech)
}
</code></pre>
<script>hljs.highlightAll();</script>
<footer>
<small>© uncloseai. 2025</small>
<br>
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
<br>
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
</footer>
<script>
// Simple active link highlighting
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function() {
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
this.classList.add('active');
});
});
// Highlight current section on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('.table-of-contents a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
});
</script>
</main>
<!-- Right sidebar - Page TOC -->
<aside class="sidebar-right">
<div class="table-of-contents">
<nav>
<ul>
<li><a href="#kotlin-intro" class="active">Overview</a></li>
<li><a href="#kotlin-client">Installation</a></li>
<li><a href="#kotlin-non-streaming">Non-Streaming</a>
<ul>
<li><a href="#kotlin-hermes-non-stream">Hermes</a></li>
<li><a href="#kotlin-qwen-non-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#kotlin-streaming">Streaming</a>
<ul>
<li><a href="#kotlin-hermes-stream">Hermes</a></li>
<li><a href="#kotlin-qwen-stream">Qwen Coder</a></li>
</ul>
</li>
<li><a href="#kotlin-tts">Text-to-Speech</a></li>
</ul>
</nav>
</div>
</aside>
</body>
</html>

View file

@ -0,0 +1,15 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# GNU AWK 5.3.2 (checked 2025-10-13: gawk 5.3.2-r2 is latest in Alpine edge)
FROM alpine:latest
RUN apk add --no-cache gawk curl ca-certificates
WORKDIR /app
COPY uncloseai.awk .
RUN chmod +x uncloseai.awk
CMD ["./uncloseai.awk"]

View file

@ -0,0 +1,18 @@
# Awk Implementation
OpenAI-compatible API client with streaming support.
## Features
- Chat completions (streaming & non-streaming)
- Text-to-speech generation
- Dynamic model discovery via environment variables
## Source Code
View the full implementation in the git repository:
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/awk
## Documentation
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.

View file

@ -0,0 +1,297 @@
#!/usr/bin/awk -f
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# UncloseAI AWK Library - OpenAI-compatible API client with streaming support
# Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
#
# Library Functions:
# uncloseai_init() - Initialize client with model discovery
# uncloseai_list_models() - List discovered models
# uncloseai_chat(messages, model) - Non-streaming chat completion
# uncloseai_chat_stream(messages, model) - Streaming chat completion
# uncloseai_tts(text, voice, model) - Text-to-speech generation
# Global client state
# client_models[i,"id|endpoint|max_tokens"] - Discovered models
# client_tts_endpoints[i] - TTS endpoints
# client_model_count - Number of discovered models
# client_tts_count - Number of TTS endpoints
function uncloseai_init( i, endpoint, cmd, response, model_id, max_tokens) {
# Initialize client state
client_model_count = 0
client_tts_count = 0
# Discover chat/code models from MODEL_ENDPOINT_N
for (i = 1; i <= 9999; i++) {
endpoint = ENVIRON["MODEL_ENDPOINT_" i]
if (endpoint == "") break
cmd = "curl -s " endpoint "/models"
response = ""
while ((cmd | getline line) > 0) {
response = response line
}
close(cmd)
# Parse models from JSON response
# Look for "id":"model-name" patterns
while (match(response, /"id":"([^"]+)"/, arr)) {
model_id = arr[1]
# Skip modelperm entries (they are permission tokens, not models)
if (index(model_id, "modelperm-") == 1) {
sub(/"id":"[^"]+"/, "", response)
continue
}
# Extract max_model_len if present (vLLM style)
if (match(response, /"max_model_len":([0-9]+)/, max_arr)) {
max_tokens = max_arr[1]
} else {
max_tokens = 8192 # Default for Ollama
}
client_model_count++
client_models[client_model_count,"id"] = model_id
client_models[client_model_count,"endpoint"] = endpoint
client_models[client_model_count,"max_tokens"] = max_tokens
# Remove this model from response to find next one
sub(/"id":"[^"]+"/, "", response)
}
}
# Discover TTS endpoints from TTS_ENDPOINT_N
for (i = 1; i <= 9999; i++) {
endpoint = ENVIRON["TTS_ENDPOINT_" i]
if (endpoint == "") break
client_tts_count++
client_tts_endpoints[client_tts_count] = endpoint
}
return client_model_count
}
function uncloseai_list_models( i) {
# Print all discovered models
for (i = 1; i <= client_model_count; i++) {
printf " - %s (max_tokens: %d)\n", \
client_models[i,"id"], \
client_models[i,"max_tokens"]
}
}
function uncloseai_get_model_idx(model_id, i) {
# Get model index by ID or return 1 for first model
if (model_id == "" && client_model_count > 0) {
return 1 # Return first model index
}
# Search for specific model
for (i = 1; i <= client_model_count; i++) {
if (client_models[i,"id"] == model_id) {
return i
}
}
return 0 # Model not found
}
function uncloseai_chat(messages_json, model_id, max_tokens, temperature, model_idx, cmd, response, content) {
# Non-streaming chat completion
# messages_json: JSON array string like '[{"role":"user","content":"..."}]'
# Returns: content string
model_idx = uncloseai_get_model_idx(model_id)
if (model_idx == 0) {
return "ERROR: Model not found"
}
if (max_tokens == "") max_tokens = 100
if (temperature == "") temperature = 0.7
cmd = "curl -s " client_models[model_idx,"endpoint"] "/chat/completions " \
"-H 'Content-Type: application/json' " \
"-d '{\"model\":\"" client_models[model_idx,"id"] "\"," \
"\"messages\":" messages_json "," \
"\"max_tokens\":" max_tokens "," \
"\"temperature\":" temperature "," \
"\"stream\":false}'"
# Execute curl and capture response
response = ""
while ((cmd | getline line) > 0) {
response = response line
}
close(cmd)
# Extract content field from JSON
if (match(response, /"content":"([^"\\]*(\\.[^"\\]*)*)"/, arr)) {
content = arr[1]
# Unescape common JSON escape sequences
gsub(/\\n/, "\n", content)
gsub(/\\"/, "\"", content)
gsub(/\\\\/, "\\", content)
return content
}
return "ERROR: No response content"
}
function uncloseai_chat_stream(messages_json, model_id, max_tokens, temperature, model_idx, cmd) {
# Streaming chat completion with SSE parsing
# Prints content chunks as they arrive
# messages_json: JSON array string like '[{"role":"user","content":"..."}]'
model_idx = uncloseai_get_model_idx(model_id)
if (model_idx == 0) {
print "ERROR: Model not found"
return 0
}
if (max_tokens == "") max_tokens = 500
if (temperature == "") temperature = 0.7
# Use curl with --no-buffer for line-by-line streaming
cmd = "curl -s --no-buffer " client_models[model_idx,"endpoint"] "/chat/completions " \
"-H 'Content-Type: application/json' " \
"-d '{\"model\":\"" client_models[model_idx,"id"] "\"," \
"\"messages\":" messages_json "," \
"\"max_tokens\":" max_tokens "," \
"\"temperature\":" temperature "," \
"\"stream\":true}'"
# Process SSE stream line by line
while ((cmd | getline line) > 0) {
# SSE format: "data: {...}"
if (match(line, /^data: (.+)$/, arr)) {
data = arr[1]
# Check for stream termination
if (data == "[DONE]") {
break
}
# Extract delta content from streaming chunk
# Format: {"choices":[{"delta":{"content":"..."}}]}
if (match(data, /"delta":\{[^}]*"content":"([^"\\]*(\\.[^"\\]*)*)"/, content_arr)) {
content = content_arr[1]
# Unescape JSON sequences
gsub(/\\n/, "\n", content)
gsub(/\\"/, "\"", content)
gsub(/\\\\/, "\\", content)
# Print chunk immediately (no newline for streaming effect)
printf "%s", content
fflush() # Flush output for real-time display
}
}
}
close(cmd)
return 1
}
function uncloseai_tts(text, voice, model_name, output_file, endpoint, cmd, size_cmd, file_size) {
# Text-to-speech generation
# Returns: file size in bytes (0 on error)
if (client_tts_count == 0) {
print "ERROR: No TTS endpoints available"
return 0
}
endpoint = client_tts_endpoints[1]
if (voice == "") voice = "alloy"
if (model_name == "") model_name = "tts-1"
if (output_file == "") output_file = "/tmp/speech.mp3"
cmd = "curl -s " endpoint "/audio/speech " \
"-H 'Content-Type: application/json' " \
"-d '{\"model\":\"" model_name "\"," \
"\"voice\":\"" voice "\"," \
"\"input\":\"" text "\"}' " \
"-o " output_file
system(cmd)
# Check file size
size_cmd = "stat -f%z " output_file " 2>/dev/null || stat -c%s " output_file " 2>/dev/null"
size_cmd | getline file_size
close(size_cmd)
return file_size + 0 # Convert to number
}
# Demo usage when run as script
BEGIN {
print "=== UncloseAI AWK Client (with Streaming) ===\n"
# Initialize client with model discovery
model_count = uncloseai_init()
if (model_count == 0) {
print "ERROR: No models discovered. Set environment variables:"
print " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc."
exit 1
}
print "Discovered " model_count " model(s)"
uncloseai_list_models()
print ""
# Non-streaming chat example
print "=== Non-Streaming Chat ==="
messages = "[{\"role\":\"system\",\"content\":\"You are a helpful AI assistant.\"}," \
"{\"role\":\"user\",\"content\":\"Explain quantum computing in one sentence.\"}]"
response = uncloseai_chat(messages, "", 100, 0.7)
print "Model: " client_models[1,"id"]
print "Response: " response
print ""
# Streaming chat example
print "=== Streaming Chat ==="
# Use second model if available, otherwise first
model_id = ""
if (client_model_count >= 2) {
model_id = client_models[2,"id"]
} else {
model_id = client_models[1,"id"]
}
print "Model: " model_id
print "Response: "
messages = "[{\"role\":\"system\",\"content\":\"You are a coding assistant.\"}," \
"{\"role\":\"user\",\"content\":\"Write a hello world function in AWK.\"}]"
uncloseai_chat_stream(messages, model_id, 200, 0.7)
print "\n"
# TTS example
if (client_tts_count > 0) {
print "=== TTS Speech Generation ==="
text = "Hello from UncloseAI AWK client! This demonstrates text to speech with streaming support."
output_file = "/tmp/speech.mp3"
file_size = uncloseai_tts(text, "alloy", "tts-1", output_file)
if (file_size > 0) {
printf "[OK] Speech file created: %s (%d bytes)\n\n", output_file, file_size
} else {
print "[ERROR] TTS generation failed\n"
}
}
print "=== Examples Complete ==="
exit
}

View file

@ -0,0 +1,13 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
FROM alpine:latest
RUN apk add --no-cache bash curl jq ca-certificates
WORKDIR /app
COPY uncloseai.sh .
RUN chmod +x uncloseai.sh
CMD ["./uncloseai.sh"]

View file

@ -0,0 +1,18 @@
# Bash Implementation
OpenAI-compatible API client with streaming support.
## Features
- Chat completions (streaming & non-streaming)
- Text-to-speech generation
- Dynamic model discovery via environment variables
## Source Code
View the full implementation in the git repository:
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/bash
## Documentation
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.

View file

@ -0,0 +1,300 @@
#!/bin/bash
# This is free software for the public good of a permacomputer hosted at
# permacomputer.com, an always-on computer by the people, for the people.
# One which is durable, easy to repair, & distributed like tap water
# for machine learning intelligence.
#
# The permacomputer is community-owned infrastructure optimized around
# four values:
#
# TRUTH First principles, math & science, open source code freely distributed
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
# LOVE Be yourself without hurting others, cooperation through natural law
#
# This software contributes to that vision by making machine learning
# accessible to everyone through a free, open, embeddable chat interface.
# Code is seeds to sprout on any abandoned technology.
# UncloseAI Bash Library - OpenAI-compatible API client with streaming support
# Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
#
# Library Functions:
# uncloseai_init - Initialize client with model discovery
# uncloseai_list_models - List discovered models
# uncloseai_chat <messages_json> - Non-streaming chat completion
# uncloseai_chat_stream <messages_json> - Streaming chat completion
# uncloseai_tts <text> <voice> - Text-to-speech generation
# Global client state
declare -a UNCLOSEAI_MODEL_IDS
declare -a UNCLOSEAI_MODEL_ENDPOINTS
declare -a UNCLOSEAI_MODEL_MAX_TOKENS
declare -a UNCLOSEAI_TTS_ENDPOINTS
# Initialize client and discover models
uncloseai_init() {
UNCLOSEAI_MODEL_IDS=()
UNCLOSEAI_MODEL_ENDPOINTS=()
UNCLOSEAI_MODEL_MAX_TOKENS=()
UNCLOSEAI_TTS_ENDPOINTS=()
# Discover chat/code models from MODEL_ENDPOINT_N
for i in {1..9999}; do
local var_name="MODEL_ENDPOINT_$i"
local endpoint="${!var_name}"
if [ -z "$endpoint" ]; then
break
fi
local response=$(curl -s "${endpoint}/models")
# Extract model IDs from JSON, filtering out modelperm entries
local model_ids=$(echo "$response" | grep -o '"id":"[^"]*"' | sed 's/"id":"//g' | sed 's/"//g' | grep -v "^modelperm-")
# Add each discovered model to arrays
while IFS= read -r model_id; do
if [ -n "$model_id" ]; then
# Try to extract max_model_len from vLLM response
local max_tokens=$(echo "$response" | grep -o '"max_model_len":[0-9]*' | head -1 | sed 's/"max_model_len"://g')
if [ -z "$max_tokens" ]; then
max_tokens=8192 # Default for Ollama
fi
UNCLOSEAI_MODEL_IDS+=("$model_id")
UNCLOSEAI_MODEL_ENDPOINTS+=("$endpoint")
UNCLOSEAI_MODEL_MAX_TOKENS+=("$max_tokens")
fi
done <<< "$model_ids"
done
# Discover TTS endpoints from TTS_ENDPOINT_N
for i in {1..9999}; do
local var_name="TTS_ENDPOINT_$i"
local endpoint="${!var_name}"
if [ -z "$endpoint" ]; then
break
fi
UNCLOSEAI_TTS_ENDPOINTS+=("$endpoint")
done
return ${#UNCLOSEAI_MODEL_IDS[@]}
}
# List all discovered models
uncloseai_list_models() {
for i in "${!UNCLOSEAI_MODEL_IDS[@]}"; do
echo " - ${UNCLOSEAI_MODEL_IDS[$i]} (max_tokens: ${UNCLOSEAI_MODEL_MAX_TOKENS[$i]})"
done
}
# Get model index by ID or return 0 for first model
uncloseai_get_model_idx() {
local model_id="$1"
if [ -z "$model_id" ]; then
echo "0"
return
fi
for i in "${!UNCLOSEAI_MODEL_IDS[@]}"; do
if [ "${UNCLOSEAI_MODEL_IDS[$i]}" == "$model_id" ]; then
echo "$i"
return
fi
done
echo "-1" # Not found
}
# Non-streaming chat completion
# Usage: uncloseai_chat '<messages_json>' [model_id] [max_tokens] [temperature]
uncloseai_chat() {
local messages_json="$1"
local model_id="${2:-}"
local max_tokens="${3:-100}"
local temperature="${4:-0.7}"
local model_idx=$(uncloseai_get_model_idx "$model_id")
if [ "$model_idx" == "-1" ]; then
echo "ERROR: Model not found"
return 1
fi
local endpoint="${UNCLOSEAI_MODEL_ENDPOINTS[$model_idx]}"
local model="${UNCLOSEAI_MODEL_IDS[$model_idx]}"
local response=$(curl -s -X POST "${endpoint}/chat/completions" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"${model}\",
\"messages\": ${messages_json},
\"max_tokens\": ${max_tokens},
\"temperature\": ${temperature},
\"stream\": false
}")
# Extract content using jq if available, otherwise use grep
if command -v jq &> /dev/null; then
echo "$response" | jq -r '.choices[0].message.content'
else
echo "$response" | grep -o '"content":"[^"]*"' | head -1 | sed 's/"content":"//g' | sed 's/"$//g'
fi
}
# Streaming chat completion with SSE parsing
# Usage: uncloseai_chat_stream '<messages_json>' [model_id] [max_tokens] [temperature]
uncloseai_chat_stream() {
local messages_json="$1"
local model_id="${2:-}"
local max_tokens="${3:-500}"
local temperature="${4:-0.7}"
local model_idx=$(uncloseai_get_model_idx "$model_id")
if [ "$model_idx" == "-1" ]; then
echo "ERROR: Model not found"
return 1
fi
local endpoint="${UNCLOSEAI_MODEL_ENDPOINTS[$model_idx]}"
local model="${UNCLOSEAI_MODEL_IDS[$model_idx]}"
# Use curl with --no-buffer for line-by-line streaming
curl -s --no-buffer -X POST "${endpoint}/chat/completions" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"${model}\",
\"messages\": ${messages_json},
\"max_tokens\": ${max_tokens},
\"temperature\": ${temperature},
\"stream\": true
}" | while IFS= read -r line; do
# SSE format: "data: {...}"
if [[ "$line" =~ ^data:\ (.+)$ ]]; then
local data="${BASH_REMATCH[1]}"
# Check for stream termination
if [ "$data" == "[DONE]" ]; then
break
fi
# Extract delta content from streaming chunk
# Use jq if available, otherwise grep
if command -v jq &> /dev/null; then
local content=$(echo "$data" | jq -r '.choices[0].delta.content // empty' 2>/dev/null)
else
local content=$(echo "$data" | grep -o '"content":"[^"\\]*\\*[^"]*"' | sed 's/"content":"//g' | sed 's/"$//g' | sed 's/\\n/\n/g' | sed 's/\\"/"/g')
fi
if [ -n "$content" ] && [ "$content" != "null" ]; then
printf "%s" "$content"
fi
fi
done
}
# Text-to-speech generation
# Usage: uncloseai_tts <text> [voice] [model] [output_file]
uncloseai_tts() {
local text="$1"
local voice="${2:-alloy}"
local model="${3:-tts-1}"
local output_file="${4:-/tmp/speech.mp3}"
if [ ${#UNCLOSEAI_TTS_ENDPOINTS[@]} -eq 0 ]; then
echo "ERROR: No TTS endpoints available"
return 1
fi
local endpoint="${UNCLOSEAI_TTS_ENDPOINTS[0]}"
curl -s -X POST "${endpoint}/audio/speech" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"${model}\",
\"voice\": \"${voice}\",
\"input\": \"${text}\"
}" \
--output "$output_file"
if [ -f "$output_file" ]; then
local file_size=$(stat -f%z "$output_file" 2>/dev/null || stat -c%s "$output_file" 2>/dev/null)
echo "$file_size"
return 0
else
return 1
fi
}
# Demo usage when run as script
if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
echo "=== UncloseAI Bash Client (with Streaming) ==="
echo ""
# Initialize client with model discovery
uncloseai_init
model_count=${#UNCLOSEAI_MODEL_IDS[@]}
if [ "$model_count" -eq 0 ]; then
echo "ERROR: No models discovered. Set environment variables:"
echo " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc."
exit 1
fi
echo "Discovered $model_count model(s)"
uncloseai_list_models
echo ""
# Non-streaming chat example
echo "=== Non-Streaming Chat ==="
messages='[{"role":"system","content":"You are a helpful AI assistant."},{"role":"user","content":"Explain quantum computing in one sentence."}]'
response=$(uncloseai_chat "$messages")
echo "Model: ${UNCLOSEAI_MODEL_IDS[0]}"
echo "Response: $response"
echo ""
# Streaming chat example
echo "=== Streaming Chat ==="
# Use second model if available, otherwise first
if [ "$model_count" -ge 2 ]; then
model_id="${UNCLOSEAI_MODEL_IDS[1]}"
else
model_id="${UNCLOSEAI_MODEL_IDS[0]}"
fi
echo "Model: $model_id"
echo "Response: "
messages='[{"role":"system","content":"You are a coding assistant."},{"role":"user","content":"Write a bash function to check if a port is open"}]'
uncloseai_chat_stream "$messages" "$model_id" 200
echo ""
echo ""
# TTS example
if [ ${#UNCLOSEAI_TTS_ENDPOINTS[@]} -gt 0 ]; then
echo "=== TTS Speech Generation ==="
text="Hello from UncloseAI Bash client! This demonstrates text to speech with streaming support."
output_file="/tmp/speech.mp3"
file_size=$(uncloseai_tts "$text" "alloy" "tts-1" "$output_file")
if [ $? -eq 0 ]; then
echo "[OK] Speech file created: $output_file ($file_size bytes)"
echo ""
else
echo "[ERROR] TTS generation failed"
echo ""
fi
fi
echo "=== Examples Complete ==="
fi

1
public/languages/c# Symbolic link
View file

@ -0,0 +1 @@
csharp

View file

@ -0,0 +1,18 @@
# C Implementation
OpenAI-compatible API client with streaming support.
## Features
- Chat completions (streaming & non-streaming)
- Text-to-speech generation
- Dynamic model discovery via environment variables
## Source Code
View the full implementation in the git repository:
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/c
## Documentation
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.

View file

@ -0,0 +1,25 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Pin to specific Alpine version (checked 2025-10-12: alpine:3.21 is latest stable)
FROM alpine:latest
# Install C compiler and libcurl development libraries
RUN apk --no-cache add \
gcc \
musl-dev \
curl-dev \
make \
ca-certificates
WORKDIR /app
COPY uncloseai.c .
COPY Makefile .
# Compile the application
RUN make
# Run the examples
CMD ["./uncloseai"]

View file

@ -0,0 +1,20 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
CC = gcc
CFLAGS = -Wall -Wextra -O2
LDFLAGS = -lcurl
TARGET = uncloseai
SRC = uncloseai.c
all: $(TARGET)
$(TARGET): $(SRC)
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
clean:
rm -f $(TARGET) speech.mp3
.PHONY: all clean

View file

@ -0,0 +1,467 @@
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com
/*
* UncloseAI C Library using libcurl
* OpenAI-compatible API client with streaming support
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#define MAX_ENDPOINTS 100
#define MAX_MODELS 100
#define MAX_URL_LEN 512
#define MAX_MODEL_LEN 256
#define MAX_CONTENT_LEN 1024
// Structure to hold response data
struct MemoryStruct {
char *memory;
size_t size;
};
// Structure to hold discovered model info
struct ModelInfo {
char id[MAX_MODEL_LEN];
char endpoint[MAX_URL_LEN];
int max_tokens;
};
// UncloseAI Client structure
typedef struct {
struct ModelInfo *models;
int model_count;
char tts_endpoints[MAX_ENDPOINTS][MAX_URL_LEN];
int tts_count;
int timeout;
} UncloseAIClient;
// Callback function type for streaming
typedef void (*StreamCallback)(const char *content, void *userdata);
// Structure for streaming context
struct StreamContext {
StreamCallback callback;
void *userdata;
char buffer[8192];
size_t buffer_pos;
};
/*************************************************************
* LIBRARY API - Callback function to capture response data
*************************************************************/
static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
struct MemoryStruct *mem = (struct MemoryStruct *)userp;
char *ptr = realloc(mem->memory, mem->size + realsize + 1);
if(!ptr) {
printf("Not enough memory (realloc returned NULL)\n");
return 0;
}
mem->memory = ptr;
memcpy(&(mem->memory[mem->size]), contents, realsize);
mem->size += realsize;
mem->memory[mem->size] = 0;
return realsize;
}
/*************************************************************
* LIBRARY API - Extract content from SSE data chunk
*************************************************************/
static void extract_sse_content(const char *data, char *content, size_t max_len) {
// Look for "content":"..." pattern
const char *content_marker = "\"content\":\"";
const char *start = strstr(data, content_marker);
if(!start) return;
start += strlen(content_marker);
const char *end = start;
// Find closing quote, handling escaped quotes
while(*end && *end != '"') {
if(*end == '\\' && *(end+1)) {
end += 2;
} else {
end++;
}
}
size_t len = end - start;
if(len > max_len - 1) len = max_len - 1;
strncpy(content, start, len);
content[len] = '\0';
}
/*************************************************************
* LIBRARY API - Streaming callback for curl
*************************************************************/
static size_t StreamWriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
struct StreamContext *ctx = (struct StreamContext *)userp;
// Append to buffer
char *data = (char *)contents;
for(size_t i = 0; i < realsize; i++) {
if(ctx->buffer_pos >= sizeof(ctx->buffer) - 1) {
// Buffer full, skip
continue;
}
ctx->buffer[ctx->buffer_pos++] = data[i];
// Check for line ending
if(data[i] == '\n' && ctx->buffer_pos >= 2 &&
ctx->buffer[ctx->buffer_pos-2] == '\n') {
ctx->buffer[ctx->buffer_pos] = '\0';
// Process SSE line
if(strncmp(ctx->buffer, "data: ", 6) == 0) {
const char *json_data = ctx->buffer + 6;
// Check for [DONE]
if(strncmp(json_data, "[DONE]", 6) == 0) {
ctx->buffer_pos = 0;
break;
}
// Extract content
char content[MAX_CONTENT_LEN];
extract_sse_content(json_data, content, sizeof(content));
if(strlen(content) > 0 && ctx->callback) {
ctx->callback(content, ctx->userdata);
}
}
ctx->buffer_pos = 0;
}
}
return realsize;
}
/*************************************************************
* LIBRARY API - Extract model IDs from JSON
*************************************************************/
static void extract_model_ids(UncloseAIClient *client, const char *json, const char *endpoint) {
const char *search = json;
const char *id_marker = "\"id\":\"";
while((search = strstr(search, id_marker)) != NULL &&
client->model_count < MAX_MODELS) {
search += strlen(id_marker);
const char *end = strchr(search, '"');
if(end) {
size_t len = end - search;
if(len < MAX_MODEL_LEN) {
// Skip modelperm-* entries
if(strncmp(search, "modelperm-", 10) == 0) {
search = end + 1;
continue;
}
strncpy(client->models[client->model_count].id, search, len);
client->models[client->model_count].id[len] = '\0';
strncpy(client->models[client->model_count].endpoint, endpoint, MAX_URL_LEN-1);
client->models[client->model_count].max_tokens = 8192;
client->model_count++;
}
}
search = end + 1;
}
}
/*************************************************************
* LIBRARY API - Initialize client and discover models
*************************************************************/
UncloseAIClient* uncloseai_init(int timeout) {
UncloseAIClient *client = (UncloseAIClient*)malloc(sizeof(UncloseAIClient));
if(!client) return NULL;
client->models = (struct ModelInfo*)malloc(MAX_MODELS * sizeof(struct ModelInfo));
if(!client->models) {
free(client);
return NULL;
}
client->model_count = 0;
client->tts_count = 0;
client->timeout = timeout;
printf("Initializing UncloseAI client...\n");
// Discover chat/code models
for(int i = 1; i < 10000; i++) {
char var_name[32];
snprintf(var_name, sizeof(var_name), "MODEL_ENDPOINT_%d", i);
char *endpoint = getenv(var_name);
if(!endpoint) break;
printf("Endpoint %d: %s\n", i, endpoint);
// Fetch models
char url[MAX_URL_LEN];
snprintf(url, sizeof(url), "%s/models", endpoint);
CURL *curl = curl_easy_init();
if(curl) {
struct MemoryStruct chunk = {NULL, 0};
chunk.memory = malloc(1);
chunk.size = 0;
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
CURLcode res = curl_easy_perform(curl);
if(res == CURLE_OK && chunk.memory) {
extract_model_ids(client, chunk.memory, endpoint);
}
free(chunk.memory);
curl_easy_cleanup(curl);
}
}
// Discover TTS endpoints
for(int i = 1; i < 10000; i++) {
char var_name[32];
snprintf(var_name, sizeof(var_name), "TTS_ENDPOINT_%d", i);
char *endpoint = getenv(var_name);
if(!endpoint) break;
strncpy(client->tts_endpoints[client->tts_count++], endpoint, MAX_URL_LEN-1);
}
printf("Discovered %d models, %d TTS endpoints\n\n", client->model_count, client->tts_count);
return client;
}
/*************************************************************
* LIBRARY API - Non-streaming chat completion
*************************************************************/
int uncloseai_chat(UncloseAIClient *client, int model_idx, const char *prompt,
struct MemoryStruct *response) {
if(model_idx >= client->model_count) return -1;
char url[MAX_URL_LEN];
char json[2048];
snprintf(url, sizeof(url), "%s/chat/completions",
client->models[model_idx].endpoint);
snprintf(json, sizeof(json),
"{\"model\":\"%s\","
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
"\"stream\":false,"
"\"temperature\":0.7,"
"\"max_tokens\":100}",
client->models[model_idx].id, prompt);
CURL *curl = curl_easy_init();
if(!curl) return -1;
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)client->timeout);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return (res == CURLE_OK) ? 0 : -1;
}
/*************************************************************
* LIBRARY API - Streaming chat completion
*************************************************************/
int uncloseai_chat_stream(UncloseAIClient *client, int model_idx, const char *prompt,
StreamCallback callback, void *userdata) {
if(model_idx >= client->model_count) return -1;
char url[MAX_URL_LEN];
char json[2048];
snprintf(url, sizeof(url), "%s/chat/completions",
client->models[model_idx].endpoint);
snprintf(json, sizeof(json),
"{\"model\":\"%s\","
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
"\"stream\":true,"
"\"temperature\":0.7,"
"\"max_tokens\":500}",
client->models[model_idx].id, prompt);
CURL *curl = curl_easy_init();
if(!curl) return -1;
struct StreamContext ctx;
ctx.callback = callback;
ctx.userdata = userdata;
ctx.buffer_pos = 0;
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, StreamWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&ctx);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)client->timeout);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return (res == CURLE_OK) ? 0 : -1;
}
/*************************************************************
* LIBRARY API - Text-to-speech generation
*************************************************************/
int uncloseai_tts(UncloseAIClient *client, const char *text, const char *voice,
const char *output_file) {
if(client->tts_count == 0) return -1;
char url[MAX_URL_LEN];
char json[2048];
snprintf(url, sizeof(url), "%s/audio/speech", client->tts_endpoints[0]);
snprintf(json, sizeof(json),
"{\"model\":\"tts-1\","
"\"voice\":\"%s\","
"\"input\":\"%s\"}",
voice, text);
CURL *curl = curl_easy_init();
if(!curl) return -1;
struct MemoryStruct chunk = {NULL, 0};
chunk.memory = malloc(1);
chunk.size = 0;
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)client->timeout);
CURLcode res = curl_easy_perform(curl);
int result = -1;
if(res == CURLE_OK) {
FILE *fp = fopen(output_file, "wb");
if(fp) {
fwrite(chunk.memory, 1, chunk.size, fp);
fclose(fp);
result = 0;
}
}
free(chunk.memory);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return result;
}
/*************************************************************
* LIBRARY API - Free client resources
*************************************************************/
void uncloseai_free(UncloseAIClient *client) {
if(client) {
if(client->models) free(client->models);
free(client);
}
}
/*************************************************************
* DEMO PROGRAM - Shows library usage
*************************************************************/
// Callback for streaming
void stream_callback(const char *content, void *userdata) {
printf("%s", content);
fflush(stdout);
}
int main(void) {
printf("=== UncloseAI C Client (with Streaming) ===\n\n");
curl_global_init(CURL_GLOBAL_ALL);
// Initialize client
UncloseAIClient *client = uncloseai_init(30);
if(!client || client->model_count == 0) {
printf("ERROR: No models discovered\n");
curl_global_cleanup();
return 1;
}
// Non-streaming chat example
printf("=== Non-Streaming Chat ===\n");
printf("Model: %s\n", client->models[0].id);
struct MemoryStruct response = {NULL, 0};
response.memory = malloc(1);
response.size = 0;
if(uncloseai_chat(client, 0, "Explain quantum computing in one sentence",
&response) == 0) {
printf("Response: (%zu bytes received)\n", response.size);
}
free(response.memory);
printf("\n");
// Streaming chat example
int model_idx = (client->model_count >= 2) ? 1 : 0;
printf("=== Streaming Chat ===\n");
printf("Model: %s\n", client->models[model_idx].id);
printf("Response: ");
uncloseai_chat_stream(client, model_idx,
"Write a hello world program in C",
stream_callback, NULL);
printf("\n\n");
// TTS example
if(client->tts_count > 0) {
printf("=== TTS Speech Generation ===\n");
printf("Model: tts-1\n");
if(uncloseai_tts(client, "Hello from UncloseAI C client!",
"alloy", "/tmp/speech.mp3") == 0) {
printf("Audio saved to /tmp/speech.mp3\n");
} else {
printf("TTS failed\n");
}
}
printf("\n=== Examples Complete ===\n");
uncloseai_free(client);
curl_global_cleanup();
return 0;
}

View file

@ -0,0 +1,26 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
FROM alpine:latest
# Install C compiler and libsoup development libraries
RUN apk --no-cache add \
gcc \
musl-dev \
libsoup-dev \
glib-dev \
make \
ca-certificates
WORKDIR /app
COPY uncloseai.c .
COPY Makefile .
# Compile the application
RUN make
# Run the examples
CMD ["./uncloseai"]

View file

@ -0,0 +1,20 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
CC = gcc
CFLAGS = -Wall -Wextra $(shell pkg-config --cflags libsoup-2.4)
LDFLAGS = $(shell pkg-config --libs libsoup-2.4)
TARGET = uncloseai
SRC = uncloseai.c
all: $(TARGET)
$(TARGET): $(SRC)
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
clean:
rm -f $(TARGET)
.PHONY: all clean

View file

@ -0,0 +1,349 @@
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com
/*
* UncloseAI C Client using libsoup (GNOME HTTP Library)
* OpenAI-compatible API client with streaming support
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libsoup/soup.h>
#define MAX_ENDPOINTS 100
#define MAX_MODELS 100
#define MAX_URL_LEN 512
#define MAX_MODEL_LEN 256
// Structure to hold discovered model info
struct ModelInfo {
char id[MAX_MODEL_LEN];
char endpoint[MAX_URL_LEN];
int max_tokens;
};
struct ModelInfo models[MAX_MODELS];
int model_count = 0;
char tts_endpoints[MAX_ENDPOINTS][MAX_URL_LEN];
int tts_count = 0;
// Simple JSON string extractor (finds "id":"value" patterns)
void extract_model_ids(const char *json, const char *endpoint) {
const char *search = json;
const char *id_marker = "\"id\":\"";
while((search = strstr(search, id_marker)) != NULL && model_count < MAX_MODELS) {
search += strlen(id_marker);
const char *end = strchr(search, '"');
if(end) {
size_t len = end - search;
if(len < MAX_MODEL_LEN) {
strncpy(models[model_count].id, search, len);
models[model_count].id[len] = '\0';
// Filter out modelperm-* and chatcmpl-* entries
if(strncmp(models[model_count].id, "modelperm-", 10) == 0 ||
strncmp(models[model_count].id, "chatcmpl-", 9) == 0) {
search = end + 1;
continue;
}
strncpy(models[model_count].endpoint, endpoint, MAX_URL_LEN-1);
models[model_count].max_tokens = 8192; // Default
printf(" - Discovered: %s\n", models[model_count].id);
model_count++;
}
}
search = end + 1;
}
}
// Discover models from an endpoint
void discover_models_from_endpoint(SoupSession *session, const char *endpoint) {
char url[MAX_URL_LEN];
snprintf(url, sizeof(url), "%s/models", endpoint);
printf("Discovering models from: %s\n", endpoint);
SoupMessage *msg = soup_message_new("GET", url);
if(!msg) return;
GError *error = NULL;
GInputStream *stream = soup_session_send(session, msg, NULL, &error);
if(error) {
g_error_free(error);
g_object_unref(msg);
return;
}
// Read response body
GString *body = g_string_new(NULL);
char buffer[4096];
gssize read;
while((read = g_input_stream_read(stream, buffer, sizeof(buffer)-1, NULL, NULL)) > 0) {
buffer[read] = '\0';
g_string_append(body, buffer);
}
extract_model_ids(body->str, endpoint);
g_string_free(body, TRUE);
g_object_unref(stream);
g_object_unref(msg);
}
// Discover all models from environment variables
void discover_all_models(SoupSession *session) {
printf("=== Model Discovery ===\n");
// Discover chat/code models
for(int i = 1; i < 10000; i++) {
char var_name[32];
snprintf(var_name, sizeof(var_name), "MODEL_ENDPOINT_%d", i);
char *endpoint = getenv(var_name);
if(!endpoint) break;
discover_models_from_endpoint(session, endpoint);
}
// Discover TTS endpoints
for(int i = 1; i < 10000; i++) {
char var_name[32];
snprintf(var_name, sizeof(var_name), "TTS_ENDPOINT_%d", i);
char *endpoint = getenv(var_name);
if(!endpoint) break;
printf("Discovering TTS from: %s\n", endpoint);
strncpy(tts_endpoints[tts_count++], endpoint, MAX_URL_LEN-1);
}
printf("\nTotal models discovered: %d\n", model_count);
printf("Total TTS endpoints: %d\n\n", tts_count);
}
// Make a non-streaming chat request
int chat_request(SoupSession *session, int model_idx, const char *prompt) {
char url[MAX_URL_LEN];
char json[2048];
snprintf(url, sizeof(url), "%s/chat/completions", models[model_idx].endpoint);
snprintf(json, sizeof(json),
"{\"model\":\"%s\","
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
"\"stream\":false,"
"\"temperature\":0.7,"
"\"max_tokens\":100}",
models[model_idx].id, prompt);
SoupMessage *msg = soup_message_new("POST", url);
if(!msg) return -1;
soup_message_set_request(msg, "application/json", SOUP_MEMORY_COPY, json, strlen(json));
GError *error = NULL;
GInputStream *stream = soup_session_send(session, msg, NULL, &error);
if(error) {
g_error_free(error);
g_object_unref(msg);
return -1;
}
// Read response
char buffer[4096];
gssize read = g_input_stream_read(stream, buffer, sizeof(buffer)-1, NULL, NULL);
if(read > 0) {
printf("Response received (%ld bytes)\n", read);
printf("(Full response requires JSON parsing library)\n");
}
g_object_unref(stream);
g_object_unref(msg);
return 0;
}
// Streaming context for SSE parsing
typedef struct {
char buffer[4096];
size_t buffer_pos;
} StreamContext;
// Extract content from SSE data line
void extract_sse_content(const char *json_data, char *content, size_t content_size) {
const char *content_marker = "\"content\":\"";
const char *found = strstr(json_data, content_marker);
if(found) {
found += strlen(content_marker);
const char *end = strchr(found, '"');
if(end) {
size_t len = end - found;
if(len < content_size) {
strncpy(content, found, len);
content[len] = '\0';
}
}
}
}
// Process streaming chunk
void process_stream_chunk(const char *data, size_t len, StreamContext *ctx) {
for(size_t i = 0; i < len; i++) {
if(data[i] == '\n') {
ctx->buffer[ctx->buffer_pos] = '\0';
// Process SSE line
if(strncmp(ctx->buffer, "data: ", 6) == 0) {
const char *json_data = ctx->buffer + 6;
if(strcmp(json_data, "[DONE]") == 0) {
return; // Stop streaming
}
char content[1024] = {0};
extract_sse_content(json_data, content, sizeof(content));
if(strlen(content) > 0) {
printf("%s", content);
fflush(stdout);
}
}
ctx->buffer_pos = 0;
} else {
if(ctx->buffer_pos < sizeof(ctx->buffer) - 1) {
ctx->buffer[ctx->buffer_pos++] = data[i];
}
}
}
}
// Streaming chat request
int chat_stream_request(SoupSession *session, int model_idx, const char *prompt) {
char url[MAX_URL_LEN];
char json[2048];
snprintf(url, sizeof(url), "%s/chat/completions", models[model_idx].endpoint);
snprintf(json, sizeof(json),
"{\"model\":\"%s\","
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
"\"stream\":true,"
"\"temperature\":0.7,"
"\"max_tokens\":500}",
models[model_idx].id, prompt);
SoupMessage *msg = soup_message_new("POST", url);
if(!msg) return -1;
soup_message_set_request(msg, "application/json", SOUP_MEMORY_COPY, json, strlen(json));
GError *error = NULL;
GInputStream *stream = soup_session_send(session, msg, NULL, &error);
if(error) {
g_error_free(error);
g_object_unref(msg);
return -1;
}
// Read and process streaming chunks
StreamContext ctx = {{0}, 0};
char buffer[1024];
gssize read;
while((read = g_input_stream_read(stream, buffer, sizeof(buffer), NULL, NULL)) > 0) {
process_stream_chunk(buffer, read, &ctx);
}
g_object_unref(stream);
g_object_unref(msg);
return 0;
}
int main(void) {
printf("=== UncloseAI C Client (libsoup/GNOME) ===\n\n");
// Initialize libsoup session
SoupSession *session = soup_session_new();
discover_all_models(session);
if(model_count == 0) {
printf("ERROR: No models discovered\n");
g_object_unref(session);
return 1;
}
// Non-streaming chat example
printf("=== Non-Streaming Chat ===\n");
printf("Model: %s\n", models[0].id);
if(chat_request(session, 0, "Explain quantum computing in one sentence") != 0) {
printf("Request failed\n");
}
printf("\n");
// Streaming chat example
int model_idx = (model_count >= 2) ? 1 : 0;
printf("=== Streaming Chat ===\n");
printf("Model: %s\n", models[model_idx].id);
printf("Response: ");
if(chat_stream_request(session, model_idx, "Write a hello world program in C") != 0) {
printf("\nStreaming request failed\n");
}
printf("\n\n");
// TTS example
if(tts_count > 0) {
printf("=== TTS Speech Generation ===\n");
printf("Model: tts-1\n");
char tts_url[MAX_URL_LEN];
snprintf(tts_url, sizeof(tts_url), "%s/audio/speech", tts_endpoints[0]);
const char *tts_json = "{"
"\"model\":\"tts-1\","
"\"voice\":\"alloy\","
"\"input\":\"Hello from UncloseAI libsoup client!\""
"}";
SoupMessage *tts_msg = soup_message_new("POST", tts_url);
if(tts_msg) {
soup_message_set_request(tts_msg, "application/json", SOUP_MEMORY_COPY,
tts_json, strlen(tts_json));
GError *error = NULL;
GInputStream *stream = soup_session_send(session, tts_msg, NULL, &error);
if(!error) {
FILE *fp = fopen("/tmp/speech.mp3", "wb");
if(fp) {
char buffer[4096];
gssize read;
while((read = g_input_stream_read(stream, buffer, sizeof(buffer), NULL, NULL)) > 0) {
fwrite(buffer, 1, read, fp);
}
fclose(fp);
printf("Audio saved to /tmp/speech.mp3\n");
} else {
printf("TTS failed: could not write file\n");
}
g_object_unref(stream);
} else {
printf("TTS failed: %s\n", error->message);
g_error_free(error);
}
g_object_unref(tts_msg);
}
}
printf("\n=== Examples Complete ===\n");
g_object_unref(session);
return 0;
}

View file

@ -0,0 +1,25 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
FROM alpine:latest
# Install C compiler and nghttp2 development libraries
RUN apk --no-cache add \
gcc \
musl-dev \
nghttp2-dev \
make \
ca-certificates
WORKDIR /app
COPY uncloseai.c .
COPY Makefile .
# Compile the application
RUN make
# Run the examples
CMD ["./uncloseai"]

View file

@ -0,0 +1,20 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
CC = gcc
CFLAGS = -Wall -Wextra -O2
LDFLAGS = -lnghttp2
TARGET = uncloseai
SRC = uncloseai.c
all: $(TARGET)
$(TARGET): $(SRC)
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
clean:
rm -f $(TARGET) speech.mp3
.PHONY: all clean

View file

@ -0,0 +1,630 @@
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com
/*
* UncloseAI C Client using nghttp2 (HTTP/2)
* OpenAI-compatible API client with HTTP/2 and streaming support
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <nghttp2/nghttp2.h>
#define MAX_ENDPOINTS 100
#define MAX_MODELS 100
#define MAX_URL_LEN 512
#define MAX_MODEL_LEN 256
#define BUFFER_SIZE 8192
// Structure to hold discovered model info
struct ModelInfo {
char id[MAX_MODEL_LEN];
char endpoint[MAX_URL_LEN];
char host[256];
int port;
int max_tokens;
};
struct ModelInfo models[MAX_MODELS];
int model_count = 0;
char tts_endpoints[MAX_ENDPOINTS][MAX_URL_LEN];
int tts_count = 0;
// HTTP/2 connection context
struct Connection {
nghttp2_session *session;
int sock;
char *response_data;
size_t response_size;
size_t response_capacity;
int done;
int streaming;
char stream_buffer[8192];
size_t stream_buffer_pos;
};
// Request data provider
struct RequestData {
const char *data;
size_t length;
size_t position;
};
// Parse URL into host, port, path
int parse_url(const char *url, char *host, int *port, char *path) {
const char *p = url;
// Skip protocol
if (strncmp(p, "http://", 7) == 0) {
p += 7;
*port = 80;
} else if (strncmp(p, "https://", 8) == 0) {
p += 8;
*port = 443;
} else {
return -1;
}
// Extract host and optional port
const char *slash = strchr(p, '/');
const char *colon = strchr(p, ':');
if (colon && (!slash || colon < slash)) {
// Port specified
size_t host_len = colon - p;
strncpy(host, p, host_len);
host[host_len] = '\0';
*port = atoi(colon + 1);
p = slash ? slash : (colon + strlen(colon));
} else if (slash) {
// No port, use default
size_t host_len = slash - p;
strncpy(host, p, host_len);
host[host_len] = '\0';
p = slash;
} else {
// No path
strcpy(host, p);
p = "";
}
strcpy(path, p[0] ? p : "/");
return 0;
}
// Connect to server
int connect_to_server(const char *host, int port) {
struct addrinfo hints, *res, *rp;
int sock = -1;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", port);
if (getaddrinfo(host, port_str, &hints, &res) != 0) {
return -1;
}
for (rp = res; rp; rp = rp->ai_next) {
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sock == -1) continue;
if (connect(sock, rp->ai_addr, rp->ai_addrlen) == 0) {
break;
}
close(sock);
sock = -1;
}
freeaddrinfo(res);
return sock;
}
// Send callback for nghttp2
ssize_t send_callback(nghttp2_session *session, const uint8_t *data,
size_t length, int flags, void *user_data) {
struct Connection *conn = (struct Connection *)user_data;
ssize_t rv = send(conn->sock, data, length, 0);
if (rv < 0) {
return NGHTTP2_ERR_CALLBACK_FAILURE;
}
return rv;
}
// Receive callback for nghttp2
ssize_t recv_callback(nghttp2_session *session, uint8_t *buf, size_t length,
int flags, void *user_data) {
struct Connection *conn = (struct Connection *)user_data;
ssize_t rv = recv(conn->sock, buf, length, 0);
if (rv < 0) {
return NGHTTP2_ERR_CALLBACK_FAILURE;
}
if (rv == 0) {
return NGHTTP2_ERR_EOF;
}
return rv;
}
// Extract content from SSE data chunk
void extract_sse_content(const char *data, char *content, size_t max_len) {
const char *content_marker = "\"content\":\"";
const char *start = strstr(data, content_marker);
if (!start) return;
start += strlen(content_marker);
const char *end = start;
// Find closing quote, handling escaped quotes
while (*end && *end != '"') {
if (*end == '\\' && *(end + 1)) {
end += 2;
} else {
end++;
}
}
size_t len = end - start;
if (len > max_len - 1) len = max_len - 1;
strncpy(content, start, len);
content[len] = '\0';
}
// Data chunk received callback
int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags,
int32_t stream_id, const uint8_t *data,
size_t len, void *user_data) {
struct Connection *conn = (struct Connection *)user_data;
if (conn->streaming) {
// Process streaming SSE data
for (size_t i = 0; i < len; i++) {
if (conn->stream_buffer_pos >= sizeof(conn->stream_buffer) - 1) {
conn->stream_buffer_pos = 0; // Reset if overflow
}
conn->stream_buffer[conn->stream_buffer_pos++] = data[i];
// Check for line ending
if (data[i] == '\n' && conn->stream_buffer_pos >= 2 &&
conn->stream_buffer[conn->stream_buffer_pos - 2] == '\n') {
conn->stream_buffer[conn->stream_buffer_pos] = '\0';
// Process SSE line
if (strncmp(conn->stream_buffer, "data: ", 6) == 0) {
const char *json_data = conn->stream_buffer + 6;
// Check for [DONE]
if (strncmp(json_data, "[DONE]", 6) == 0) {
conn->stream_buffer_pos = 0;
break;
}
// Extract and print content
char content[1024];
extract_sse_content(json_data, content, sizeof(content));
if (strlen(content) > 0) {
printf("%s", content);
fflush(stdout);
}
}
conn->stream_buffer_pos = 0;
}
}
} else {
// Non-streaming: accumulate response
if (conn->response_size + len > conn->response_capacity) {
size_t new_capacity = (conn->response_capacity + len) * 2;
char *new_data = realloc(conn->response_data, new_capacity);
if (!new_data) return 0;
conn->response_data = new_data;
conn->response_capacity = new_capacity;
}
memcpy(conn->response_data + conn->response_size, data, len);
conn->response_size += len;
}
return 0;
}
// Stream close callback
int on_stream_close_callback(nghttp2_session *session, int32_t stream_id,
uint32_t error_code, void *user_data) {
struct Connection *conn = (struct Connection *)user_data;
conn->done = 1;
return 0;
}
// Data provider read callback
ssize_t data_provider_read_callback(nghttp2_session *session, int32_t stream_id,
uint8_t *buf, size_t length,
uint32_t *data_flags,
nghttp2_data_source *source,
void *user_data) {
struct RequestData *req_data = (struct RequestData *)source->ptr;
size_t remaining = req_data->length - req_data->position;
size_t to_copy = (remaining < length) ? remaining : length;
memcpy(buf, req_data->data + req_data->position, to_copy);
req_data->position += to_copy;
if (req_data->position >= req_data->length) {
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
}
return to_copy;
}
// Setup HTTP/2 headers
void setup_http2_headers(nghttp2_nv *hdrs, const char *method, const char *path, const char *host) {
hdrs[0].name = (uint8_t *)":method";
hdrs[0].value = (uint8_t *)method;
hdrs[0].namelen = strlen(":method");
hdrs[0].valuelen = strlen(method);
hdrs[0].flags = NGHTTP2_NV_FLAG_NONE;
hdrs[1].name = (uint8_t *)":path";
hdrs[1].value = (uint8_t *)path;
hdrs[1].namelen = strlen(":path");
hdrs[1].valuelen = strlen(path);
hdrs[1].flags = NGHTTP2_NV_FLAG_NONE;
hdrs[2].name = (uint8_t *)":scheme";
hdrs[2].value = (uint8_t *)"http";
hdrs[2].namelen = strlen(":scheme");
hdrs[2].valuelen = 4;
hdrs[2].flags = NGHTTP2_NV_FLAG_NONE;
hdrs[3].name = (uint8_t *)":authority";
hdrs[3].value = (uint8_t *)host;
hdrs[3].namelen = strlen(":authority");
hdrs[3].valuelen = strlen(host);
hdrs[3].flags = NGHTTP2_NV_FLAG_NONE;
hdrs[4].name = (uint8_t *)"content-type";
hdrs[4].value = (uint8_t *)"application/json";
hdrs[4].namelen = strlen("content-type");
hdrs[4].valuelen = strlen("application/json");
hdrs[4].flags = NGHTTP2_NV_FLAG_NONE;
}
// Setup nghttp2 session with callbacks
void setup_nghttp2_session(struct Connection *conn, nghttp2_session_callbacks *callbacks) {
nghttp2_session_callbacks_new(&callbacks);
nghttp2_session_callbacks_set_send_callback(callbacks, send_callback);
nghttp2_session_callbacks_set_recv_callback(callbacks, recv_callback);
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, on_data_chunk_recv_callback);
nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, on_stream_close_callback);
nghttp2_session_client_new(&conn->session, callbacks, conn);
nghttp2_session_callbacks_del(callbacks);
nghttp2_submit_settings(conn->session, NGHTTP2_FLAG_NONE, NULL, 0);
}
// Make HTTP/2 request
int http2_request(const char *host, int port, const char *path,
const char *method, const char *body,
char **response, size_t *response_size) {
int sock = connect_to_server(host, port);
if (sock < 0) return -1;
// Initialize connection
struct Connection conn = {0};
conn.sock = sock;
conn.response_data = malloc(BUFFER_SIZE);
conn.response_capacity = BUFFER_SIZE;
conn.response_size = 0;
conn.done = 0;
conn.streaming = 0;
conn.stream_buffer_pos = 0;
// Setup nghttp2 session and callbacks
nghttp2_session_callbacks *callbacks;
setup_nghttp2_session(&conn, callbacks);
// Prepare headers
nghttp2_nv hdrs[5];
setup_http2_headers(hdrs, method, path, host);
// Submit request
if (body && strlen(body) > 0) {
struct RequestData req_data = {body, strlen(body), 0};
nghttp2_data_provider data_prd;
data_prd.source.ptr = &req_data;
data_prd.read_callback = data_provider_read_callback;
nghttp2_submit_request(conn.session, NULL, hdrs, 5, &data_prd, NULL);
} else {
nghttp2_submit_request(conn.session, NULL, hdrs, 4, NULL, NULL);
}
// Send and receive
while (!conn.done) {
if (nghttp2_session_send(conn.session) != 0) break;
if (nghttp2_session_recv(conn.session) != 0) break;
}
// Cleanup
*response = conn.response_data;
*response_size = conn.response_size;
nghttp2_session_del(conn.session);
close(sock);
return 0;
}
// Make streaming HTTP/2 request
int http2_stream_request(const char *host, int port, const char *path,
const char *method, const char *body) {
int sock = connect_to_server(host, port);
if (sock < 0) return -1;
// Initialize connection for streaming
struct Connection conn = {0};
conn.sock = sock;
conn.response_data = malloc(BUFFER_SIZE);
conn.response_capacity = BUFFER_SIZE;
conn.response_size = 0;
conn.done = 0;
conn.streaming = 1; // Enable streaming mode
conn.stream_buffer_pos = 0;
// Setup nghttp2 session and callbacks
nghttp2_session_callbacks *callbacks;
setup_nghttp2_session(&conn, callbacks);
// Prepare headers
nghttp2_nv hdrs[5];
setup_http2_headers(hdrs, method, path, host);
// Submit request
if (body && strlen(body) > 0) {
struct RequestData req_data = {body, strlen(body), 0};
nghttp2_data_provider data_prd;
data_prd.source.ptr = &req_data;
data_prd.read_callback = data_provider_read_callback;
nghttp2_submit_request(conn.session, NULL, hdrs, 5, &data_prd, NULL);
} else {
nghttp2_submit_request(conn.session, NULL, hdrs, 4, NULL, NULL);
}
// Send and receive (streaming prints directly in callback)
while (!conn.done) {
if (nghttp2_session_send(conn.session) != 0) break;
if (nghttp2_session_recv(conn.session) != 0) break;
}
// Cleanup
free(conn.response_data);
nghttp2_session_del(conn.session);
close(sock);
return 0;
}
// Simple JSON string extractor
void extract_model_ids(const char *json, const char *endpoint) {
const char *search = json;
const char *id_marker = "\"id\":\"";
while ((search = strstr(search, id_marker)) != NULL && model_count < MAX_MODELS) {
search += strlen(id_marker);
const char *end = strchr(search, '"');
if (end) {
size_t len = end - search;
if (len < MAX_MODEL_LEN) {
strncpy(models[model_count].id, search, len);
models[model_count].id[len] = '\0';
// Filter out modelperm-* entries
if (strncmp(models[model_count].id, "modelperm-", 10) == 0 ||
strncmp(models[model_count].id, "chatcmpl-", 9) == 0) {
search = end + 1;
continue;
}
strncpy(models[model_count].endpoint, endpoint, MAX_URL_LEN - 1);
char host[256];
int port;
char path[512];
if (parse_url(endpoint, host, &port, path) == 0) {
strncpy(models[model_count].host, host, sizeof(models[model_count].host) - 1);
models[model_count].port = port;
}
models[model_count].max_tokens = 8192;
model_count++;
}
}
search = end + 1;
}
}
// Discover models from endpoint
void discover_models_from_endpoint(const char *endpoint) {
printf("Discovering from: %s\n", endpoint);
char host[256];
int port;
char base_path[512];
if (parse_url(endpoint, host, &port, base_path) != 0) return;
char path[512];
snprintf(path, sizeof(path), "%s%s", base_path,
base_path[strlen(base_path)-1] == '/' ? "models" : "/models");
char *response = NULL;
size_t response_size = 0;
if (http2_request(host, port, path, "GET", NULL, &response, &response_size) == 0) {
if (response) {
extract_model_ids(response, endpoint);
free(response);
}
}
}
// Discover all models from environment
void discover_all_models() {
printf("=== Model Discovery ===\n");
for (int i = 1; i < 10000; i++) {
char var_name[32];
snprintf(var_name, sizeof(var_name), "MODEL_ENDPOINT_%d", i);
char *endpoint = getenv(var_name);
if (!endpoint) break;
discover_models_from_endpoint(endpoint);
}
for (int i = 1; i < 10000; i++) {
char var_name[32];
snprintf(var_name, sizeof(var_name), "TTS_ENDPOINT_%d", i);
char *endpoint = getenv(var_name);
if (!endpoint) break;
strncpy(tts_endpoints[tts_count++], endpoint, MAX_URL_LEN - 1);
}
printf("\nDiscovered %d model(s)\n\n", model_count);
}
int main(void) {
printf("=== UncloseAI C Client (nghttp2 HTTP/2) ===\n\n");
discover_all_models();
if (model_count == 0) {
printf("ERROR: No models discovered. Set environment variables:\n");
printf(" MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n");
return 1;
}
for (int i = 0; i < model_count; i++) {
printf(" - %s (max_tokens: %d)\n", models[i].id, models[i].max_tokens);
}
printf("\n");
// Non-streaming chat (Hermes)
printf("=== Non-Streaming Chat (Hermes) ===\n");
printf("Model: %s\n", models[0].id);
char json[2048];
snprintf(json, sizeof(json),
"{\"model\":\"%s\","
"\"messages\":[{\"role\":\"user\",\"content\":\"Give a Python Fizzbuzz solution in one line of code?\"}],"
"\"stream\":false,"
"\"max_tokens\":150,"
"\"temperature\":0.5}",
models[0].id);
char *response = NULL;
size_t response_size = 0;
char path[512];
snprintf(path, sizeof(path), "/v1/chat/completions");
if (http2_request(models[0].host, models[0].port, path, "POST", json,
&response, &response_size) == 0) {
printf("Response received (%zu bytes)\n", response_size);
printf("(Full response requires JSON parsing library)\n\n");
free(response);
} else {
printf("Request failed\n\n");
}
// Streaming chat (Hermes)
printf("=== Streaming Chat (Hermes) ===\n");
printf("Model: %s\n", models[0].id);
printf("Response: ");
char stream_json_hermes[2048];
snprintf(stream_json_hermes, sizeof(stream_json_hermes),
"{\"model\":\"%s\","
"\"messages\":[{\"role\":\"user\",\"content\":\"Give a Python Fizzbuzz solution in one line of code?\"}],"
"\"stream\":true,"
"\"max_tokens\":500,"
"\"temperature\":0.7}",
models[0].id);
if (http2_stream_request(models[0].host, models[0].port, path, "POST", stream_json_hermes) != 0) {
printf("\nStreaming request failed");
}
printf("\n\n");
// Streaming chat (Qwen Coder 3) - if second model available
if (model_count >= 2) {
printf("=== Streaming Chat (Qwen Coder 3) ===\n");
printf("Model: %s\n", models[1].id);
printf("Response: ");
char stream_json_qwen[2048];
snprintf(stream_json_qwen, sizeof(stream_json_qwen),
"{\"model\":\"%s\","
"\"messages\":[{\"role\":\"user\",\"content\":\"Write a hello world program in C\"}],"
"\"stream\":true,"
"\"max_tokens\":500,"
"\"temperature\":0.7}",
models[1].id);
if (http2_stream_request(models[1].host, models[1].port, path, "POST", stream_json_qwen) != 0) {
printf("\nStreaming request failed");
}
printf("\n\n");
}
// TTS
if (tts_count > 0) {
printf("=== TTS Speech Generation ===\n");
printf("Model: tts-1\n");
char host[256];
int port;
char base_path[512];
if (parse_url(tts_endpoints[0], host, &port, base_path) == 0) {
const char *tts_json = "{"
"\"model\":\"tts-1\","
"\"voice\":\"alloy\","
"\"input\":\"Hello from UncloseAI C client with nghttp2!\""
"}";
char *tts_response = NULL;
size_t tts_size = 0;
if (http2_request(host, port, "/audio/speech", "POST", tts_json,
&tts_response, &tts_size) == 0) {
FILE *fp = fopen("/tmp/speech.mp3", "wb");
if (fp) {
fwrite(tts_response, 1, tts_size, fp);
fclose(fp);
printf("Audio saved to /tmp/speech.mp3\n");
} else {
printf("TTS failed\n");
}
free(tts_response);
} else {
printf("TTS failed\n");
}
}
}
printf("\n=== Examples Complete ===\n");
return 0;
}

View file

@ -0,0 +1,17 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Clojure with OpenJDK 21 (checked 2025-10-13: clojure:temurin-21-tools-deps-alpine is latest stable)
FROM clojure:temurin-21-tools-deps-alpine
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY deps.edn .
COPY uncloseai.clj .
# Pre-download dependencies
RUN clojure -P
CMD ["clojure", "-M", "-m", "uncloseai"]

View file

@ -0,0 +1,18 @@
# Clojure Implementation
OpenAI-compatible API client with streaming support.
## Features
- Chat completions (streaming & non-streaming)
- Text-to-speech generation
- Dynamic model discovery via environment variables
## Source Code
View the full implementation in the git repository:
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/clojure
## Documentation
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.

View file

@ -0,0 +1,7 @@
;; PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
;; Copyright 2025 TimeHexOn & foxhop & russell@unturf
;; https://www.permacomputer.com
{:deps {clj-http/clj-http {:mvn/version "3.13.0"}
cheshire/cheshire {:mvn/version "5.13.0"}}
:paths ["."]}

View file

@ -0,0 +1,232 @@
;; PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
;; Copyright 2025 TimeHexOn & foxhop & russell@unturf
;; https://www.permacomputer.com
(ns uncloseai
"UncloseAI Clojure Library - OpenAI-compatible API client with streaming support
Compatible with vLLM, Ollama, and OpenAI-compatible endpoints"
(:require [clj-http.client :as client]
[cheshire.core :as json]
[clojure.string :as str]))
;; Client record for managing models and endpoints
(defrecord UncloseAIClient [models tts-endpoints])
(defn filter-modelperm
"Filter out modelperm entries from model list"
[models]
(remove #(str/starts-with? (:id %) "modelperm-") models))
(defn discover-models-from-endpoint
"Discover models from a single endpoint"
[endpoint]
(try
(let [response (client/get (str endpoint "/models") {:as :json})
body (:body response)
model-list (:data body)]
(->> model-list
(map (fn [model]
{:id (:id model)
:endpoint endpoint
:max-tokens (or (:max_model_len model) 8192)}))
(filter-modelperm)))
(catch Exception e
(println "Warning: Failed to discover models from" endpoint ":" (.getMessage e))
[])))
(defn discover-tts-endpoints
"Discover TTS endpoints from environment variables"
[]
(loop [i 1
endpoints []]
(if-let [endpoint (System/getenv (str "TTS_ENDPOINT_" i))]
(recur (inc i) (conj endpoints endpoint))
endpoints)))
(defn init-client
"Initialize UncloseAI client with model discovery from environment variables"
([]
(init-client nil nil))
([model-endpoints tts-endpoints]
(let [model-eps (or model-endpoints
(loop [i 1 eps []]
(if-let [ep (System/getenv (str "MODEL_ENDPOINT_" i))]
(recur (inc i) (conj eps ep))
eps)))
tts-eps (or tts-endpoints (discover-tts-endpoints))
discovered-models (mapcat discover-models-from-endpoint model-eps)]
(->UncloseAIClient discovered-models tts-eps))))
(defn list-models
"List all discovered models"
[client]
(:models client))
(defn get-model
"Get model by ID or return first model if ID is nil"
[client model-id]
(if model-id
(first (filter #(= (:id %) model-id) (:models client)))
(first (:models client))))
(defn chat
"Non-streaming chat completion
Args:
client: UncloseAI client instance
messages: Vector of message maps with :role and :content
options: Map with optional :model-id, :max-tokens, :temperature"
([client messages]
(chat client messages {}))
([client messages {:keys [model-id max-tokens temperature]
:or {max-tokens 100 temperature 0.7}}]
(let [model (get-model client model-id)]
(if-not model
(throw (ex-info "Model not found" {:model-id model-id}))
(let [url (str (:endpoint model) "/chat/completions")
payload {:model (:id model)
:messages messages
:max_tokens max-tokens
:temperature temperature
:stream false}
response (client/post url
{:content-type :json
:body (json/generate-string payload)
:as :json})
body (:body response)]
{:model (:id model)
:content (get-in body [:choices 0 :message :content])
:response body})))))
(defn parse-sse-line
"Parse a single SSE line and extract content"
[line]
(when (str/starts-with? line "data: ")
(let [data (subs line 6)]
(when-not (= data "[DONE]")
(try
(let [parsed (json/parse-string data true)]
(get-in parsed [:choices 0 :delta :content]))
(catch Exception e
nil))))))
(defn chat-stream
"Streaming chat completion using Server-Sent Events
Returns a lazy sequence of content chunks
Args:
client: UncloseAI client instance
messages: Vector of message maps with :role and :content
options: Map with optional :model-id, :max-tokens, :temperature"
([client messages]
(chat-stream client messages {}))
([client messages {:keys [model-id max-tokens temperature]
:or {max-tokens 500 temperature 0.7}}]
(let [model (get-model client model-id)]
(if-not model
(throw (ex-info "Model not found" {:model-id model-id}))
(let [url (str (:endpoint model) "/chat/completions")
payload {:model (:id model)
:messages messages
:max_tokens max-tokens
:temperature temperature
:stream true}
response (client/post url
{:content-type :json
:body (json/generate-string payload)
:as :stream})
stream (:body response)]
(->> (line-seq (clojure.java.io/reader stream))
(keep parse-sse-line)
(remove nil?)))))))
(defn tts
"Text-to-speech generation
Args:
client: UncloseAI client instance
text: Input text to convert to speech
options: Map with optional :voice, :model, :output-file"
([client text]
(tts client text {}))
([client text {:keys [voice model output-file]
:or {voice "alloy" model "tts-1" output-file "/tmp/speech.mp3"}}]
(if (empty? (:tts-endpoints client))
(throw (ex-info "No TTS endpoints available" {}))
(let [endpoint (first (:tts-endpoints client))
url (str endpoint "/audio/speech")
payload {:model model
:voice voice
:input text}
response (client/post url
{:content-type :json
:body (json/generate-string payload)
:as :byte-array})
audio-data (:body response)]
(with-open [out (clojure.java.io/output-stream output-file)]
(.write out audio-data))
{:file output-file
:size (count audio-data)}))))
;; Demo usage when run as script
(defn -main [& args]
(println "=== UncloseAI Clojure Client (with Streaming) ===")
(println)
;; Initialize client with auto-discovery
(let [client (init-client)]
(when (empty? (:models client))
(println "ERROR: No models discovered. Set environment variables:")
(println " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.")
(System/exit 1))
(println (str "Discovered " (count (:models client)) " model(s)"))
(doseq [model (:models client)]
(println (str " - " (:id model) " (max_tokens: " (:max-tokens model) ")")))
(println)
;; Non-streaming chat example
(println "=== Non-Streaming Chat ===")
(try
(let [result (chat client
[{:role "system" :content "You are a helpful AI assistant."}
{:role "user" :content "Explain quantum computing in one sentence."}])]
(println "Model:" (:model result))
(println "Response:" (:content result)))
(catch Exception e
(println "Error:" (.getMessage e))))
(println)
;; Streaming chat example
(println "=== Streaming Chat ===")
(let [model-id (if (>= (count (:models client)) 2)
(:id (nth (:models client) 1))
nil)]
(println "Model:" (or model-id (:id (first (:models client)))))
(print "Response: ")
(flush)
(try
(doseq [chunk (chat-stream client
[{:role "system" :content "You are a coding assistant."}
{:role "user" :content "Write a hello world function in Clojure."}]
{:model-id model-id :max-tokens 200})]
(print chunk)
(flush))
(println)
(catch Exception e
(println "\nError:" (.getMessage e)))))
(println)
;; TTS example
(when-not (empty? (:tts-endpoints client))
(println "=== TTS Speech Generation ===")
(try
(let [result (tts client "Hello from UncloseAI Clojure client! This demonstrates text to speech with streaming support.")]
(println (str "[OK] Speech file created: " (:file result) " (" (:size result) " bytes)")))
(catch Exception e
(println "Error:" (.getMessage e))))
(println))
(println "=== Examples Complete ===")))

View file

@ -0,0 +1,27 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# GnuCOBOL 3.x (checked 2025-10-13: hldtux/cobol-gnu is available)
FROM debian:bookworm-slim AS builder
RUN apt-get update && \
apt-get install -y gnucobol4 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY uncloseai.cob .
# Compile COBOL program
RUN cobc -x -free uncloseai.cob -o uncloseai
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y gnucobol4 libcob4 ca-certificates curl jq bash && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/uncloseai .
CMD ["./uncloseai"]

View file

@ -0,0 +1,18 @@
# Cobol Implementation
OpenAI-compatible API client with streaming support.
## Features
- Chat completions (streaming & non-streaming)
- Text-to-speech generation
- Dynamic model discovery via environment variables
## Source Code
View the full implementation in the git repository:
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/cobol
## Documentation
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.

View file

@ -0,0 +1,55 @@
#!/bin/sh
# This is free software for the public good of a permacomputer hosted at
# permacomputer.com, an always-on computer by the people, for the people.
# One which is durable, easy to repair, & distributed like tap water
# for machine learning intelligence.
#
# The permacomputer is community-owned infrastructure optimized around
# four values:
#
# TRUTH First principles, math & science, open source code freely distributed
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
# LOVE Be yourself without hurting others, cooperation through natural law
#
# This software contributes to that vision by making machine learning
# accessible to everyone through a free, open, embeddable chat interface.
# Code is seeds to sprout on any abandoned technology.
# Model discovery for COBOL - outputs discovered models to files
# Discover models from MODEL_ENDPOINT_N
echo "=== Model Discovery ===" > /tmp/models.txt
model_count=0
i=1
while [ $i -le 9999 ]; do
eval endpoint=\$MODEL_ENDPOINT_$i
[ -z "$endpoint" ] && break
i=$((i + 1))
echo "Discovering from: $endpoint" >> /tmp/models.txt
response=$(curl -s "${endpoint}/models")
# Extract first model ID
model_id=$(echo "$response" | grep -o '"id":"[^"]*"' | head -1 | sed 's/"id":"//g' | sed 's/"//g')
if [ -n "$model_id" ]; then
echo "$model_id" >> /tmp/model_$model_count.txt
echo "$endpoint" >> /tmp/endpoint_$model_count.txt
echo " - Discovered: $model_id" >> /tmp/models.txt
model_count=$((model_count + 1))
fi
done
# Discover TTS
i=1
while [ $i -le 9999 ]; do
eval endpoint=\$TTS_ENDPOINT_$i
[ -z "$endpoint" ] && break
echo "$endpoint" > /tmp/tts_endpoint.txt
echo "Discovering TTS from: $endpoint" >> /tmp/models.txt
i=$((i + 1))
done
echo "Total models: $model_count" >> /tmp/models.txt
cat /tmp/models.txt

View file

@ -0,0 +1,34 @@
#!/bin/sh
# This is free software for the public good of a permacomputer hosted at
# permacomputer.com, an always-on computer by the people, for the people.
# One which is durable, easy to repair, & distributed like tap water
# for machine learning intelligence.
#
# The permacomputer is community-owned infrastructure optimized around
# four values:
#
# TRUTH First principles, math & science, open source code freely distributed
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
# LOVE Be yourself without hurting others, cooperation through natural law
#
# This software contributes to that vision by making machine learning
# accessible to everyone through a free, open, embeddable chat interface.
# Code is seeds to sprout on any abandoned technology.
# Use first discovered model
if [ ! -f /tmp/model_0.txt ] || [ ! -f /tmp/endpoint_0.txt ]; then
echo "ERROR: No models discovered"
exit 1
fi
model=$(cat /tmp/model_0.txt)
endpoint=$(cat /tmp/endpoint_0.txt)
echo "Using model: $model"
echo "Endpoint: $endpoint"
curl -s "${endpoint}/chat/completions" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"$model\",\"messages\":[{\"role\":\"system\",\"content\":\"You are Hermes, a helpful AI assistant from Nous Research.\"},{\"role\":\"user\",\"content\":\"Explain quantum computing in one sentence.\"}],\"max_tokens\":100}" \
2>/dev/null | grep -o '"content":"[^"]*"' | head -1 | cut -d'"' -f4

View file

@ -0,0 +1,37 @@
#!/bin/sh
# This is free software for the public good of a permacomputer hosted at
# permacomputer.com, an always-on computer by the people, for the people.
# One which is durable, easy to repair, & distributed like tap water
# for machine learning intelligence.
#
# The permacomputer is community-owned infrastructure optimized around
# four values:
#
# TRUTH First principles, math & science, open source code freely distributed
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
# LOVE Be yourself without hurting others, cooperation through natural law
#
# This software contributes to that vision by making machine learning
# accessible to everyone through a free, open, embeddable chat interface.
# Code is seeds to sprout on any abandoned technology.
# Use second discovered model (or first if only one)
if [ -f /tmp/model_1.txt ] && [ -f /tmp/endpoint_1.txt ]; then
model=$(cat /tmp/model_1.txt)
endpoint=$(cat /tmp/endpoint_1.txt)
elif [ -f /tmp/model_0.txt ] && [ -f /tmp/endpoint_0.txt ]; then
model=$(cat /tmp/model_0.txt)
endpoint=$(cat /tmp/endpoint_0.txt)
else
echo "ERROR: No models discovered"
exit 1
fi
echo "Using model: $model"
echo "Endpoint: $endpoint"
curl -s "${endpoint}/chat/completions" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"$model\",\"messages\":[{\"role\":\"system\",\"content\":\"You are Qwen, a coding assistant specialized in software development.\"},{\"role\":\"user\",\"content\":\"Write a hello world function in COBOL.\"}],\"max_tokens\":200}" \
2>/dev/null | grep -o '"content":"[^"]*"' | head -1 | cut -d'"' -f4

View file

@ -0,0 +1,32 @@
#!/bin/sh
# This is free software for the public good of a permacomputer hosted at
# permacomputer.com, an always-on computer by the people, for the people.
# One which is durable, easy to repair, & distributed like tap water
# for machine learning intelligence.
#
# The permacomputer is community-owned infrastructure optimized around
# four values:
#
# TRUTH First principles, math & science, open source code freely distributed
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
# LOVE Be yourself without hurting others, cooperation through natural law
#
# This software contributes to that vision by making machine learning
# accessible to everyone through a free, open, embeddable chat interface.
# Code is seeds to sprout on any abandoned technology.
# Use first TTS endpoint
if [ ! -f /tmp/tts_endpoint.txt ]; then
echo "ERROR: No TTS endpoints discovered"
exit 1
fi
endpoint=$(cat /tmp/tts_endpoint.txt)
echo "Using TTS endpoint: $endpoint"
curl -s "${endpoint}/audio/speech" \
-H 'Content-Type: application/json' \
-d '{"model":"tts-1","voice":"alloy","input":"Hello from COBOL! This is a text to speech example."}' \
-o /app/output.mp3 2>/dev/null

View file

@ -0,0 +1,87 @@
*> PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
*> Copyright 2025 TimeHexOn & foxhop & russell@unturf
*> https://www.permacomputer.com
IDENTIFICATION DIVISION.
PROGRAM-ID. UNCLOSEAI.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 MESSAGE-TEXT PIC X(200).
01 MODEL-INDEX PIC 9.
01 TTS-TEXT PIC X(200).
01 TTS-VOICE PIC X(20).
01 TEMP-CMD PIC X(500).
PROCEDURE DIVISION.
UNCLOSEAI-INIT.
DISPLAY "Initializing UncloseAI client...".
CALL "SYSTEM" USING
"for i in {1..9999}; do ep=$MODEL_ENDPOINT_$i; "
"[ -z $ep ] && break; echo Endpoint $i: $ep; done".
UNCLOSEAI-CHAT.
DISPLAY "Model: first available".
STRING "ep=$MODEL_ENDPOINT_1; "
"curl -s $ep/chat/completions "
"-H Content-Type:application/json "
"-d {model:qwen,messages:[{role:user,content:"
MESSAGE-TEXT "}],stream:false}"
DELIMITED BY SIZE
INTO TEMP-CMD
END-STRING
CALL "SYSTEM" USING TEMP-CMD.
UNCLOSEAI-CHAT-STREAM.
DISPLAY "Model: streaming".
STRING "ep=$MODEL_ENDPOINT_1; "
"curl -s --no-buffer $ep/chat/completions "
"-H Content-Type:application/json "
"-d {model:qwen,messages:[{role:user,content:"
MESSAGE-TEXT "}],stream:true}"
DELIMITED BY SIZE
INTO TEMP-CMD
END-STRING
CALL "SYSTEM" USING TEMP-CMD.
UNCLOSEAI-TTS.
DISPLAY "Generating speech...".
STRING "curl -s $TTS_ENDPOINT_1/audio/speech "
"-H Content-Type:application/json "
"-d {model:tts-1,input:"
TTS-TEXT ",voice:" TTS-VOICE "} "
"-o /tmp/speech.mp3"
DELIMITED BY SIZE
INTO TEMP-CMD
END-STRING
CALL "SYSTEM" USING TEMP-CMD
DISPLAY "Audio saved to /tmp/speech.mp3".
DEMO-MAIN.
DISPLAY "=== UncloseAI COBOL Client ===".
DISPLAY " ".
PERFORM UNCLOSEAI-INIT.
DISPLAY " ".
DISPLAY "=== Non-Streaming Chat ===".
MOVE "Explain quantum computing" TO MESSAGE-TEXT.
MOVE 0 TO MODEL-INDEX.
PERFORM UNCLOSEAI-CHAT.
DISPLAY " ".
DISPLAY "=== Streaming Chat ===".
MOVE "Write hello world in COBOL" TO MESSAGE-TEXT.
MOVE 1 TO MODEL-INDEX.
PERFORM UNCLOSEAI-CHAT-STREAM.
DISPLAY " ".
DISPLAY "=== TTS Speech Generation ===".
MOVE "Hello from COBOL" TO TTS-TEXT.
MOVE "alloy" TO TTS-VOICE.
PERFORM UNCLOSEAI-TTS.
DISPLAY " ".
DISPLAY "=== Examples Complete ===".
STOP RUN.

View file

@ -0,0 +1,25 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Pin to specific SBCL version (checked 2025-10-15: clfoundation/sbcl:latest is stable)
FROM clfoundation/sbcl:latest
# Install Quicklisp (Common Lisp package manager)
RUN curl -O https://beta.quicklisp.org/quicklisp.lisp && \
sbcl --load quicklisp.lisp --eval '(quicklisp-quickstart:install)' --quit && \
echo '(load "~/quicklisp/setup.lisp")' >> ~/.sbclrc
# Install Dexador HTTP client and JSON parser
RUN sbcl --eval '(ql:quickload :dexador)' \
--eval '(ql:quickload :jonathan)' \
--quit
# Set working directory
WORKDIR /app
# Copy application files
COPY uncloseai.lisp .
# Default command runs the demo
CMD ["sbcl", "--script", "uncloseai.lisp"]

View file

@ -0,0 +1,18 @@
# Commonlisp Implementation
OpenAI-compatible API client with streaming support.
## Features
- Chat completions (streaming & non-streaming)
- Text-to-speech generation
- Dynamic model discovery via environment variables
## Source Code
View the full implementation in the git repository:
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/commonlisp
## Documentation
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.

View file

@ -0,0 +1,311 @@
#!/usr/bin/env sbcl --script
;; PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
;; Copyright 2025 TimeHexOn & foxhop & russell@unturf
;; https://www.permacomputer.com
;;; uncloseai - Common Lisp client library for OpenAI-compatible APIs
;;; Supports streaming and non-streaming chat, model discovery, and TTS
;;; Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
;;;
;;; Uses Dexador HTTP client and Jonathan JSON library
(load "~/quicklisp/setup.lisp")
(ql:quickload '(:dexador :jonathan) :silent t)
;;; Data structures
(defclass uncloseai ()
((models :initform nil :accessor models
:documentation "List of discovered models with metadata")
(tts-endpoints :initform nil :accessor tts-endpoints
:documentation "List of TTS endpoint URLs")
(api-key :initform nil :initarg :api-key :accessor api-key
:documentation "Optional API key for authentication")
(request-timeout :initform 30 :initarg :request-timeout :accessor request-timeout
:documentation "Request timeout in seconds")))
(defun make-model-info (id endpoint max-tokens)
"Create a model info plist"
(list :id id :endpoint endpoint :max-tokens max-tokens))
;;; Environment discovery
(defun discover-env-endpoints (prefix)
"Discover endpoints from environment variables like PREFIX_1, PREFIX_2, ..."
(loop for i from 1 to 9999
for var-name = (format nil "~A_~D" prefix i)
for endpoint = (uiop:getenv var-name)
while endpoint
collect endpoint))
;;; Model discovery
(defun discover-models-from-endpoint (client endpoint)
"Discover available models from an endpoint"
(handler-case
(let* ((url (concatenate 'string endpoint "/models"))
(headers (when (api-key client)
(list (cons "Authorization"
(format nil "Bearer ~A" (api-key client))))))
(response (dex:get url :headers headers))
(parsed (jonathan:parse response))
(data (getf parsed :|data|))
;; Coerce to list to handle both vector (vLLM) and list (Ollama) responses
(models-list (coerce data 'list)))
(dolist (model models-list)
(let ((model-id (getf model :|id|))
(max-tokens (or (getf model :|max_model_len|) 8192)))
(push (make-model-info model-id endpoint max-tokens)
(models client)))))
(error (e)
(format t "Warning: Failed to discover models from ~A: ~A~%" endpoint e))))
(defun initialize-client (client model-endpoints tts-endpoints)
"Initialize client with endpoint discovery and model detection"
;; Discover endpoints from environment if not provided
(let ((model-eps (or model-endpoints (discover-env-endpoints "MODEL_ENDPOINT")))
(tts-eps (or tts-endpoints (discover-env-endpoints "TTS_ENDPOINT"))))
;; Discover models from each endpoint
(dolist (endpoint model-eps)
(discover-models-from-endpoint client endpoint))
;; Reverse models list (they were pushed in reverse order)
(setf (models client) (nreverse (models client)))
;; Store TTS endpoints
(setf (tts-endpoints client) tts-eps))
client)
(defun make-uncloseai (&key model-endpoints tts-endpoints api-key (request-timeout 30))
"Create a new uncloseai client with auto-discovery from environment variables"
(let ((client (make-instance 'uncloseai
:api-key api-key
:request-timeout request-timeout)))
(initialize-client client model-endpoints tts-endpoints)))
;;; Helper functions
(defun get-model-info (client model-id)
"Get model info by ID or return first available model"
(when (null (models client))
(error "No models available. Check endpoint configuration."))
(if (null model-id)
(first (models client))
(or (find model-id (models client) :key (lambda (m) (getf m :id)) :test #'string=)
(error "Model '~A' not found in discovered models" model-id))))
(defun make-headers (client &optional (content-type t))
"Create HTTP headers with optional authorization"
(let ((headers nil))
(when content-type
(push (cons "Content-Type" "application/json") headers))
(when (api-key client)
(push (cons "Authorization"
(format nil "Bearer ~A" (api-key client)))
headers))
headers))
;;; Non-streaming chat completion
(defun chat (client messages &key model (max-tokens 100) (temperature 0.7))
"Non-streaming chat completion
Args:
messages - List of message plists with :role and :content
model - Model ID (defaults to first available model)
max-tokens - Maximum tokens in response
temperature - Sampling temperature
Returns:
Response plist with :choices containing the completion"
(let* ((model-info (get-model-info client model))
(endpoint (getf model-info :endpoint))
(model-id (getf model-info :id))
(url (concatenate 'string endpoint "/chat/completions"))
(payload (jonathan:to-json
(list :|model| model-id
:|messages| (coerce messages 'vector)
:|max_tokens| max-tokens
:|temperature| temperature
:|stream| :false)))
(headers (make-headers client))
(response (dex:post url
:headers headers
:content payload)))
(jonathan:parse response)))
;;; Streaming chat completion
(defun process-sse-line (line)
"Process a single Server-Sent Event line, return parsed content or nil"
(when (and line (> (length line) 6) (string= (subseq line 0 6) "data: "))
(let ((data (subseq line 6)))
(unless (string= data "[DONE]")
(handler-case
(let* ((parsed (jonathan:parse data))
;; Coerce choices to list to handle both vector and list responses
(choices (coerce (getf parsed :|choices|) 'list))
(delta (when choices
(getf (first choices) :|delta|)))
(content (when delta (getf delta :|content|))))
content)
(error (e) nil))))))
(defun chat-stream (client messages callback &key model (max-tokens 500) (temperature 0.7))
"Streaming chat completion using Server-Sent Events
Args:
messages - List of message plists with :role and :content
callback - Function to call for each content chunk (receives string)
model - Model ID (defaults to first available model)
max-tokens - Maximum tokens in response
temperature - Sampling temperature"
(let* ((model-info (get-model-info client model))
(endpoint (getf model-info :endpoint))
(model-id (getf model-info :id))
(url (concatenate 'string endpoint "/chat/completions"))
(payload (jonathan:to-json
(list :|model| model-id
:|messages| (coerce messages 'vector)
:|max_tokens| max-tokens
:|temperature| temperature
:|stream| t)))
(headers (make-headers client))
(stream (dex:request url
:method :post
:headers headers
:content payload
:want-stream t)))
(unwind-protect
(loop for line = (read-line stream nil nil)
while line
do (let ((content (process-sse-line line)))
(when content
(funcall callback content))))
(close stream))))
;;; Text-to-Speech
(defun tts (client text &key (voice "alloy") (model "tts-1") (response-format "mp3"))
"Generate speech from text
Args:
text - Input text to convert to speech
voice - Voice name (alloy, echo, fable, onyx, nova, shimmer)
model - TTS model (tts-1 or tts-1-hd)
response-format - Audio format (mp3, opus, aac, flac)
Returns:
Audio data as byte array"
(when (null (tts-endpoints client))
(error "No TTS endpoints available"))
(let* ((endpoint (first (tts-endpoints client)))
(url (concatenate 'string endpoint "/audio/speech"))
(payload (jonathan:to-json
(list :|model| model
:|voice| voice
:|input| text
:|response_format| response-format)))
(headers (make-headers client))
(response (dex:post url
:headers headers
:content payload
:force-binary t)))
response))
;;; Demo usage
(defun demo-nonstreaming (client)
"Demonstrate non-streaming chat"
(format t "=== Non-Streaming Chat ===~%")
(let* ((response (chat client
(list (list :|role| "system"
:|content| "You are a helpful AI assistant.")
(list :|role| "user"
:|content| "Explain quantum computing in one sentence."))
:max-tokens 100
:temperature 0.7))
(model-used (getf response :|model|))
;; Coerce choices to list to handle both vector and list responses
(choices (coerce (getf response :|choices|) 'list))
(choice (first choices))
(message (getf choice :|message|))
(content (getf message :|content|)))
(format t "Model: ~A~%" model-used)
(format t "Response: ~A~%~%" content)))
(defun demo-streaming (client)
"Demonstrate streaming chat"
(format t "=== Streaming Chat ===~%")
(let ((model-id (if (> (length (models client)) 1)
(getf (second (models client)) :id)
nil)))
(format t "Model: ~A~%" (or model-id (getf (first (models client)) :id)))
(format t "Response: ")
(force-output)
(chat-stream client
(list (list :|role| "system"
:|content| "You are a coding assistant.")
(list :|role| "user"
:|content| "Write a Common Lisp function to check if a number is prime"))
(lambda (content)
(format t "~A" content)
(force-output))
:model model-id
:max-tokens 200
:temperature 0.7)
(format t "~%~%")))
(defun demo-tts (client)
"Demonstrate text-to-speech"
(when (tts-endpoints client)
(format t "=== TTS Speech Generation ===~%")
(let ((audio-data (tts client
"Hello from uncloseai Common Lisp client! This demonstrates text to speech with streaming support."
:voice "alloy")))
(with-open-file (out "speech.mp3"
:direction :output
:if-exists :supersede
:if-does-not-exist :create
:element-type '(unsigned-byte 8))
(write-sequence audio-data out))
(format t "[OK] Speech file created: speech.mp3 (~D bytes)~%~%" (length audio-data)))))
(defun main ()
"Main demo function"
(format t "=== uncloseai Common Lisp Client (with Streaming) ===~%~%")
;; Initialize client (auto-discovers from environment)
(let ((client (make-uncloseai)))
(when (null (models client))
(format t "ERROR: No models discovered. Set environment variables:~%")
(format t " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.~%")
(uiop:quit 1))
(format t "Discovered ~D model(s)~%" (length (models client)))
(dolist (model (models client))
(format t " - ~A (max_tokens: ~D)~%"
(getf model :id)
(getf model :max-tokens)))
(format t "~%")
;; Run demos
(handler-case
(progn
(demo-nonstreaming client)
(demo-streaming client)
(demo-tts client)
(format t "=== Examples Complete ===~%"))
(error (e)
(format t "~%Error: ~A~%" e)))))
;; Main execution
(main)

View file

@ -0,0 +1,18 @@
# Cpp Implementation
OpenAI-compatible API client with streaming support.
## Features
- Chat completions (streaming & non-streaming)
- Text-to-speech generation
- Dynamic model discovery via environment variables
## Source Code
View the full implementation in the git repository:
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/cpp
## Documentation
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.

View file

@ -0,0 +1,25 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
FROM alpine:latest
# Install C++ compiler and Boost libraries
RUN apk --no-cache add \
g++ \
make \
boost1.84-dev \
openssl-dev \
ca-certificates
WORKDIR /app
COPY uncloseai.cpp .
COPY Makefile .
# Compile the application
RUN make
# Run the examples
CMD ["./uncloseai"]

View file

@ -0,0 +1,20 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
LDFLAGS = -lssl -lcrypto -lpthread
TARGET = uncloseai
SRC = uncloseai.cpp
all: $(TARGET)
$(TARGET): $(SRC)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
clean:
rm -f $(TARGET) speech.mp3
.PHONY: all clean

View file

@ -0,0 +1,271 @@
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com
/*
* UncloseAI C++ Library using Boost.Beast
* OpenAI-compatible API client with HTTP/HTTPS support
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
*/
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <fstream>
#include <cstdlib>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
namespace ssl = net::ssl;
using tcp = net::ip::tcp;
struct ModelInfo {
std::string id;
std::string host;
std::string port;
std::string base_path;
int max_tokens;
};
class UncloseAI {
private:
std::vector<ModelInfo> models;
std::vector<std::pair<std::string, std::string>> tts_endpoints; // host, port
int timeout;
void parse_url(const std::string& url, std::string& host, std::string& port, std::string& path) {
// Parse https://host:port/path
size_t proto_end = url.find("://");
if (proto_end == std::string::npos) return;
std::string rest = url.substr(proto_end + 3);
size_t slash = rest.find("/");
std::string host_port = (slash != std::string::npos) ? rest.substr(0, slash) : rest;
path = (slash != std::string::npos) ? rest.substr(slash) : "/v1";
size_t colon = host_port.find(":");
if (colon != std::string::npos) {
host = host_port.substr(0, colon);
port = host_port.substr(colon + 1);
} else {
host = host_port;
port = (url.find("https://") == 0) ? "443" : "80";
}
}
std::string http_get(const std::string& host, const std::string& port, const std::string& target) {
try {
net::io_context ioc;
tcp::resolver resolver(ioc);
beast::tcp_stream stream(ioc);
auto const results = resolver.resolve(host, port);
stream.connect(results);
http::request<http::string_body> req{http::verb::get, target, 11};
req.set(http::field::host, host);
req.set(http::field::user_agent, "UncloseAI-Beast");
http::write(stream, req);
beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return res.body();
} catch (...) {
return "";
}
}
std::string http_post(const std::string& host, const std::string& port,
const std::string& target, const std::string& body) {
try {
net::io_context ioc;
tcp::resolver resolver(ioc);
beast::tcp_stream stream(ioc);
auto const results = resolver.resolve(host, port);
stream.connect(results);
http::request<http::string_body> req{http::verb::post, target, 11};
req.set(http::field::host, host);
req.set(http::field::user_agent, "UncloseAI-Beast");
req.set(http::field::content_type, "application/json");
req.body() = body;
req.prepare_payload();
http::write(stream, req);
beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return res.body();
} catch (...) {
return "";
}
}
void discover_models_from_endpoint(const std::string& endpoint) {
std::string host, port, base_path;
parse_url(endpoint, host, port, base_path);
std::string target = base_path + (base_path.back() == '/' ? "models" : "/models");
std::string response = http_get(host, port, target);
if (!response.empty()) {
// Simple JSON parsing for model IDs
size_t pos = 0;
while ((pos = response.find("\"id\":\"", pos)) != std::string::npos) {
pos += 6;
size_t end = response.find("\"", pos);
if (end != std::string::npos) {
std::string model_id = response.substr(pos, end - pos);
// Filter out modelperm-* and chatcmpl-*
if (model_id.substr(0, 10) != "modelperm-" &&
model_id.substr(0, 9) != "chatcmpl-") {
ModelInfo info;
info.id = model_id;
info.host = host;
info.port = port;
info.base_path = base_path;
info.max_tokens = 8192;
models.push_back(info);
}
}
pos = end + 1;
}
}
}
public:
UncloseAI(int timeout_sec = 30) : timeout(timeout_sec) {
std::vector<std::string> endpoints, tts_eps;
// Discover endpoints from environment
for (int i = 1; i < 10000; i++) {
std::string var = "MODEL_ENDPOINT_" + std::to_string(i);
const char* ep = std::getenv(var.c_str());
if (!ep) break;
endpoints.push_back(ep);
}
for (int i = 1; i < 10000; i++) {
std::string var = "TTS_ENDPOINT_" + std::to_string(i);
const char* ep = std::getenv(var.c_str());
if (!ep) break;
tts_eps.push_back(ep);
}
for (const auto& ep : endpoints) {
std::cout << "Discovering from: " << ep << std::endl;
discover_models_from_endpoint(ep);
}
for (const auto& ep : tts_eps) {
std::string host, port, path;
parse_url(ep, host, port, path);
tts_endpoints.push_back({host, port});
}
std::cout << "\nDiscovered " << models.size() << " model(s)\n" << std::endl;
}
const std::vector<ModelInfo>& list_models() const { return models; }
int chat(const std::string& prompt, std::string& response, int model_idx = 0, int max_tokens = 100) {
if (model_idx >= static_cast<int>(models.size())) return -1;
const ModelInfo& model = models[model_idx];
std::string target = model.base_path + (model.base_path.back() == '/' ? "chat/completions" : "/chat/completions");
std::ostringstream json;
json << "{\"model\":\"" << model.id << "\","
<< "\"messages\":[{\"role\":\"user\",\"content\":\"" << prompt << "\"}],"
<< "\"stream\":false,"
<< "\"max_tokens\":" << max_tokens << ","
<< "\"temperature\":0.7}";
response = http_post(model.host, model.port, target, json.str());
return response.empty() ? -1 : 0;
}
int tts(const std::string& text, const std::string& voice, const std::string& output_file) {
if (tts_endpoints.empty()) return -1;
auto [host, port] = tts_endpoints[0];
std::ostringstream json;
json << "{\"model\":\"tts-1\","
<< "\"voice\":\"" << voice << "\","
<< "\"input\":\"" << text << "\"}";
std::string response = http_post(host, port, "/audio/speech", json.str());
if (!response.empty()) {
std::ofstream file(output_file, std::ios::binary);
if (file.is_open()) {
file.write(response.c_str(), response.size());
file.close();
return 0;
}
}
return -1;
}
};
int main() {
std::cout << "=== UncloseAI C++ Client (Boost.Beast) ===\n\n";
UncloseAI client(30);
if (client.list_models().empty()) {
std::cout << "ERROR: No models discovered. Set environment variables:\n";
std::cout << " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n";
return 1;
}
auto models = client.list_models();
for (const auto& m : models) {
std::cout << " - " << m.id << " (max_tokens: " << m.max_tokens << ")\n";
}
std::cout << "\n";
// Non-streaming chat
std::cout << "=== Non-Streaming Chat ===\n";
std::string response;
if (client.chat("Explain quantum computing in one sentence", response) == 0) {
std::cout << "Response received (" << response.size() << " bytes)\n\n";
} else {
std::cout << "Request failed\n\n";
}
// TTS
std::cout << "=== TTS Speech Generation ===\n";
if (client.tts("Hello from Boost.Beast!", "alloy", "/tmp/speech.mp3") == 0) {
std::cout << "Audio saved to /tmp/speech.mp3\n";
} else {
std::cout << "TTS failed\n";
}
std::cout << "\n=== Examples Complete ===\n";
return 0;
}

View file

@ -0,0 +1,28 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
FROM alpine:latest
# Install C++ compiler and build tools
RUN apk --no-cache add \
g++ \
make \
wget \
ca-certificates \
openssl-dev
WORKDIR /app
# Download cpp-httplib header-only library (v0.18.3 latest as of 2025-10-14)
RUN wget -O httplib.h https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.18.3/httplib.h
COPY uncloseai.cpp .
COPY Makefile .
# Compile the application
RUN make
# Run the examples
CMD ["./uncloseai"]

View file

@ -0,0 +1,20 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2 -DCPPHTTPLIB_OPENSSL_SUPPORT
LDFLAGS = -lssl -lcrypto -lpthread
TARGET = uncloseai
SRC = uncloseai.cpp
all: $(TARGET)
$(TARGET): $(SRC) httplib.h
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
clean:
rm -f $(TARGET) speech.mp3
.PHONY: all clean

View file

@ -0,0 +1,272 @@
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com
/*
* UncloseAI C++ Library using cpp-httplib (header-only)
* OpenAI-compatible API client with streaming support
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
*/
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <fstream>
#include <cstdlib>
#include <functional>
#include "httplib.h"
struct ModelInfo {
std::string id;
std::string endpoint;
std::string host;
int port;
int max_tokens;
};
class UncloseAI {
private:
std::vector<ModelInfo> models;
std::vector<std::pair<std::string, int>> tts_endpoints; // host, port
int timeout;
bool debug;
// Parse URL into host and port
bool parse_url(const std::string& url, std::string& host, int& port, std::string& base_path) {
// Simple URL parsing for https://host:port/path
size_t proto_end = url.find("://");
if (proto_end == std::string::npos) return false;
std::string rest = url.substr(proto_end + 3);
size_t slash_pos = rest.find("/");
std::string host_port;
if (slash_pos != std::string::npos) {
host_port = rest.substr(0, slash_pos);
base_path = rest.substr(slash_pos);
} else {
host_port = rest;
base_path = "/";
}
size_t colon_pos = host_port.find(":");
if (colon_pos != std::string::npos) {
host = host_port.substr(0, colon_pos);
port = std::stoi(host_port.substr(colon_pos + 1));
} else {
host = host_port;
port = (url.find("https://") == 0) ? 443 : 80;
}
return true;
}
void discover_endpoints_from_env(const std::string& prefix, std::vector<std::string>& endpoints) {
for(int i = 1; i < 10000; i++) {
std::string var_name = prefix + "_" + std::to_string(i);
const char* endpoint = std::getenv(var_name.c_str());
if(!endpoint) break;
endpoints.push_back(endpoint);
}
}
void discover_models(const std::vector<std::string>& endpoints) {
for(const auto& endpoint : endpoints) {
if(debug) {
std::cout << "[DEBUG] Discovering from: " << endpoint << std::endl;
}
std::string host;
int port;
std::string base_path;
if(!parse_url(endpoint, host, port, base_path)) continue;
httplib::Client cli(host, port);
cli.set_connection_timeout(0, 10000000); // 10 sec
cli.set_read_timeout(10, 0);
std::string models_path = base_path + (base_path.back() == '/' ? "models" : "/models");
auto res = cli.Get(models_path.c_str());
if(res && res->status == 200) {
// Simple JSON parsing for model IDs
std::string body = res->body;
size_t pos = 0;
while((pos = body.find("\"id\":\"", pos)) != std::string::npos) {
pos += 6;
size_t end = body.find("\"", pos);
if(end != std::string::npos) {
std::string model_id = body.substr(pos, end - pos);
// Filter out modelperm-* and chatcmpl-* entries
if(model_id.substr(0, 10) != "modelperm-" && model_id.substr(0, 9) != "chatcmpl-") {
ModelInfo info;
info.id = model_id;
info.endpoint = endpoint;
info.host = host;
info.port = port;
info.max_tokens = 8192;
models.push_back(info);
if(debug) {
std::cout << "[DEBUG] Discovered: " << model_id << std::endl;
}
}
}
pos = end + 1;
}
}
}
}
public:
UncloseAI(int timeout = 30, bool debug = false) : timeout(timeout), debug(debug) {
std::vector<std::string> endpoints;
std::vector<std::string> tts_eps;
discover_endpoints_from_env("MODEL_ENDPOINT", endpoints);
discover_endpoints_from_env("TTS_ENDPOINT", tts_eps);
if(debug) {
std::cout << "[DEBUG] Initialized with " << endpoints.size() << " endpoint(s)" << std::endl;
}
discover_models(endpoints);
// Parse TTS endpoints
for(const auto& ep : tts_eps) {
std::string host;
int port;
std::string base_path;
if(parse_url(ep, host, port, base_path)) {
tts_endpoints.push_back({host, port});
}
}
}
const std::vector<ModelInfo>& list_models() const {
return models;
}
int chat(const std::string& prompt, std::string& response, int model_idx = 0, int max_tokens = 100) {
if(model_idx >= static_cast<int>(models.size())) return -1;
const ModelInfo& model = models[model_idx];
httplib::Client cli(model.host, model.port);
cli.set_connection_timeout(0, timeout * 1000000);
cli.set_read_timeout(timeout, 0);
std::ostringstream json;
json << "{\"model\":\"" << model.id << "\","
<< "\"messages\":[{\"role\":\"user\",\"content\":\"" << prompt << "\"}],"
<< "\"stream\":false,"
<< "\"max_tokens\":" << max_tokens << ","
<< "\"temperature\":0.7}";
auto res = cli.Post("/chat/completions", json.str(), "application/json");
if(res && res->status == 200) {
response = res->body;
return 0;
}
return -1;
}
int chat_stream(const std::string& prompt, std::function<void(const std::string&)> callback, int model_idx = 0, int max_tokens = 500) {
// NOTE: cpp-httplib streaming API is complex, using simple buffered approach
// For production use, consider implementing proper SSE streaming with ContentReceiver
std::string response;
if(chat(prompt, response, model_idx, max_tokens) == 0) {
if(callback) {
callback(response);
}
return 0;
}
return -1;
}
int tts(const std::string& text, const std::string& voice, const std::string& output_file) {
if(tts_endpoints.empty()) return -1;
auto [host, port] = tts_endpoints[0];
httplib::Client cli(host, port);
cli.set_connection_timeout(0, timeout * 1000000);
cli.set_read_timeout(timeout, 0);
std::ostringstream json;
json << "{\"model\":\"tts-1\","
<< "\"voice\":\"" << voice << "\","
<< "\"input\":\"" << text << "\"}";
auto res = cli.Post("/audio/speech", json.str(), "application/json");
if(res && res->status == 200) {
std::ofstream file(output_file, std::ios::binary);
if(file.is_open()) {
file.write(res->body.c_str(), res->body.size());
file.close();
return 0;
}
}
return -1;
}
};
// Demo program showing library usage
int main() {
std::cout << "=== UncloseAI C++ Client (cpp-httplib with Streaming) ===\n\n";
UncloseAI client(30, true);
if(client.list_models().empty()) {
std::cout << "ERROR: No models discovered. Set environment variables:\n";
std::cout << " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n";
return 1;
}
auto models = client.list_models();
std::cout << "\nDiscovered " << models.size() << " model(s):\n";
for(const auto& m : models) {
std::cout << " - " << m.id << " (max_tokens: " << m.max_tokens << ")\n";
}
std::cout << "\n";
// Non-streaming chat
std::cout << "=== Non-Streaming Chat ===\n";
std::string response;
if(client.chat("Explain quantum computing in one sentence", response) == 0) {
std::cout << "Response received (" << response.size() << " bytes)\n";
std::cout << "(Full response requires JSON parsing library)\n\n";
} else {
std::cout << "Request failed\n\n";
}
// Streaming chat
std::cout << "=== Streaming Chat ===\n";
int model_idx = (models.size() > 1) ? 1 : 0;
std::cout << "Model: " << models[model_idx].id << "\n";
std::cout << "Response: ";
client.chat_stream("Write a hello world program in C++",
[](const std::string& content) {
std::cout << content << std::flush;
}, model_idx, 500);
std::cout << "\n\n";
// TTS
std::cout << "=== TTS Speech Generation ===\n";
std::cout << "Model: tts-1\n";
if(client.tts("Hello from UncloseAI C++ client with cpp-httplib! This demonstrates streaming support.",
"alloy", "/tmp/speech.mp3") == 0) {
std::cout << "Audio saved to /tmp/speech.mp3\n";
} else {
std::cout << "TTS failed\n";
}
std::cout << "\n=== Examples Complete ===\n";
return 0;
}

View file

@ -0,0 +1,25 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Pin to specific Alpine version (checked 2025-10-12: alpine:3.21 is latest stable)
FROM alpine:latest
# Install C++ compiler and libcurl development libraries
RUN apk --no-cache add \
g++ \
musl-dev \
curl-dev \
make \
ca-certificates
WORKDIR /app
COPY uncloseai.cpp .
COPY Makefile .
# Compile the application
RUN make
# Run the examples
CMD ["./uncloseai"]

View file

@ -0,0 +1,20 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
CXX = g++
CXXFLAGS = -std=c++11 -Wall -Wextra -O2
LDFLAGS = -lcurl
TARGET = uncloseai
SRC = uncloseai.cpp
all: $(TARGET)
$(TARGET): $(SRC)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
clean:
rm -f $(TARGET) speech.mp3
.PHONY: all clean

View file

@ -0,0 +1,340 @@
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com
/*
* UncloseAI C++ Library using libcurl
* OpenAI-compatible API client with streaming support
*/
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <cstring>
#include <cstdlib>
#include <fstream>
#include <curl/curl.h>
#define MAX_CONTENT_LEN 1024
struct ModelInfo {
std::string id;
std::string endpoint;
int max_tokens;
};
struct MemoryStruct {
std::string data;
};
// Callback for non-streaming responses
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
MemoryStruct *mem = static_cast<MemoryStruct*>(userp);
mem->data.append(static_cast<char*>(contents), realsize);
return realsize;
}
// Streaming context
struct StreamContext {
std::function<void(const std::string&)> callback;
std::string buffer;
};
// Extract content from SSE JSON
static void extract_sse_content(const std::string& data, std::string& content) {
const char *content_marker = "\"content\":\"";
size_t start = data.find(content_marker);
if(start == std::string::npos) return;
start += strlen(content_marker);
size_t end = start;
while(end < data.size() && data[end] != '"') {
if(data[end] == '\\' && end + 1 < data.size()) {
end += 2;
} else {
end++;
}
}
content = data.substr(start, end - start);
}
// Streaming callback
static size_t StreamCallback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
StreamContext *ctx = static_cast<StreamContext*>(userp);
ctx->buffer.append(static_cast<char*>(contents), realsize);
size_t pos = 0;
while((pos = ctx->buffer.find("\n\n")) != std::string::npos) {
std::string line = ctx->buffer.substr(0, pos);
ctx->buffer.erase(0, pos + 2);
if(line.substr(0, 6) == "data: ") {
std::string data = line.substr(6);
if(data == "[DONE]") break;
std::string content;
extract_sse_content(data, content);
if(!content.empty() && ctx->callback) {
ctx->callback(content);
}
}
}
return realsize;
}
class UncloseAI {
private:
std::vector<ModelInfo> models;
std::vector<std::string> tts_endpoints;
int timeout;
bool debug;
void discover_endpoints_from_env(const std::string& prefix, std::vector<std::string>& endpoints) {
for(int i = 1; i < 10000; i++) {
std::string var_name = prefix + "_" + std::to_string(i);
const char* endpoint = std::getenv(var_name.c_str());
if(!endpoint) break;
endpoints.push_back(endpoint);
}
}
void discover_models(const std::vector<std::string>& endpoints) {
for(const auto& endpoint : endpoints) {
if(debug) {
std::cout << "[DEBUG] Discovering from: " << endpoint << std::endl;
}
CURL *curl = curl_easy_init();
if(curl) {
MemoryStruct response;
std::string url = endpoint + "/models";
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if(res == CURLE_OK) {
// Simple JSON parsing for model IDs
size_t pos = 0;
while((pos = response.data.find("\"id\":\"", pos)) != std::string::npos) {
pos += 6;
size_t end = response.data.find("\"", pos);
if(end != std::string::npos) {
std::string model_id = response.data.substr(pos, end - pos);
if(model_id.substr(0, 10) != "modelperm-") {
ModelInfo info;
info.id = model_id;
info.endpoint = endpoint;
info.max_tokens = 8192;
models.push_back(info);
if(debug) {
std::cout << "[DEBUG] Discovered: " << model_id << std::endl;
}
}
}
pos = end + 1;
}
}
}
}
}
public:
UncloseAI(int timeout = 30, bool debug = false) : timeout(timeout), debug(debug) {
std::vector<std::string> endpoints;
std::vector<std::string> tts_eps;
discover_endpoints_from_env("MODEL_ENDPOINT", endpoints);
discover_endpoints_from_env("TTS_ENDPOINT", tts_eps);
if(debug) {
std::cout << "[DEBUG] Initialized with " << endpoints.size() << " endpoint(s)" << std::endl;
}
discover_models(endpoints);
tts_endpoints = tts_eps;
}
const std::vector<ModelInfo>& list_models() const {
return models;
}
int chat(const std::string& prompt, std::string& response, int model_idx = 0) {
if(model_idx >= static_cast<int>(models.size())) return -1;
const ModelInfo& model = models[model_idx];
std::string url = model.endpoint + "/chat/completions";
std::string json = "{\"model\":\"" + model.id + "\","
"\"messages\":[{\"role\":\"user\",\"content\":\"" + prompt + "\"}],"
"\"max_tokens\":100}";
CURL *curl = curl_easy_init();
if(!curl) return -1;
MemoryStruct mem;
struct curl_slist *headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
response = mem.data;
return (res == CURLE_OK) ? 0 : -1;
}
int chat_stream(const std::string& prompt, std::function<void(const std::string&)> callback, int model_idx = 0) {
if(model_idx >= static_cast<int>(models.size())) return -1;
const ModelInfo& model = models[model_idx];
std::string url = model.endpoint + "/chat/completions";
std::string json = "{\"model\":\"" + model.id + "\","
"\"messages\":[{\"role\":\"user\",\"content\":\"" + prompt + "\"}],"
"\"stream\":true,"
"\"max_tokens\":500}";
CURL *curl = curl_easy_init();
if(!curl) return -1;
StreamContext ctx;
ctx.callback = callback;
struct curl_slist *headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, StreamCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return (res == CURLE_OK) ? 0 : -1;
}
int tts(const std::string& text, const std::string& voice, const std::string& output_file) {
if(tts_endpoints.empty()) return -1;
std::string url = tts_endpoints[0] + "/audio/speech";
std::string json = "{\"model\":\"tts-1\","
"\"voice\":\"" + voice + "\","
"\"input\":\"" + text + "\"}";
CURL *curl = curl_easy_init();
if(!curl) return -1;
MemoryStruct mem;
struct curl_slist *headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if(res == CURLE_OK) {
std::ofstream file(output_file, std::ios::binary);
if(file.is_open()) {
file.write(mem.data.c_str(), mem.data.size());
file.close();
return 0;
}
}
return -1;
}
};
// Demo
int main() {
std::cout << "=== UncloseAI C++ Client (with Streaming) ===\n\n";
curl_global_init(CURL_GLOBAL_ALL);
UncloseAI client(30, true);
if(client.list_models().empty()) {
std::cout << "ERROR: No models discovered. Set environment variables:\n";
std::cout << " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n";
curl_global_cleanup();
return 1;
}
auto models = client.list_models();
std::cout << "\nDiscovered " << models.size() << " model(s):\n";
for(const auto& m : models) {
std::cout << " - " << m.id << " (max_tokens: " << m.max_tokens << ")\n";
}
std::cout << "\n";
// Non-streaming chat
std::cout << "=== Non-Streaming Chat ===\n";
std::string response;
if(client.chat("Explain quantum computing in one sentence", response) == 0) {
std::cout << "Response: (" << response.size() << " bytes received)\n\n";
}
// Streaming chat
std::cout << "=== Streaming Chat ===\n";
int model_idx = (models.size() > 1) ? 1 : 0;
std::cout << "Model: " << models[model_idx].id << "\n";
std::cout << "Response: ";
client.chat_stream("Write a C++ function to check if a number is prime",
[](const std::string& content) {
std::cout << content << std::flush;
}, model_idx);
std::cout << "\n\n";
// TTS
std::cout << "=== TTS Speech Generation ===\n";
if(client.tts("Hello from UncloseAI C++ client! This demonstrates streaming support.",
"alloy", "speech.mp3") == 0) {
std::cout << "[OK] Speech file created: speech.mp3\n\n";
} else {
std::cout << "[ERROR] TTS Error\n\n";
}
std::cout << "=== Examples Complete ===\n";
curl_global_cleanup();
return 0;
}

View file

@ -0,0 +1,21 @@
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
# Crystal latest-alpine (checked 2025-10-13: crystallang/crystal:latest-alpine is latest stable)
FROM crystallang/crystal:latest-alpine AS builder
WORKDIR /app
COPY uncloseai.cr .
# Build the application
RUN crystal build --release uncloseai.cr
FROM alpine:latest
RUN apk add --no-cache ca-certificates gc-dev pcre2-dev libevent-dev
WORKDIR /app
COPY --from=builder /app/uncloseai .
CMD ["./uncloseai"]

Some files were not shown because too many files have changed in this diff Show more