「無料3,000リクエスト」を使い切る! 番外編
※後半の付録に上級者向けのちょっとニッチな知見を記載しています。
・CORS回避
・モデル一覧取得
・LocalStorageへのAPIキー保存
・CSSアニメーション
・システムプロンプト
・チャットのカスタマイズ方法など
ーーーーーー
SAKURA Internetは無料で3000リクエストも使えて太っ腹です。
今回は開発ではなく、出来上がったチャットアプリでリクエストを使ってみました。
ほとんど残ってないので開発ではもたない。。。
はじめに
前回 レンタルサーバにも置けるでしょとPHPでのLLMChatを作りました。
しかし、PHPではなく、ローカルのファイルをブラウザで動かせるようになれば
誰でも使えるのにと言われました。
ということで、index.htmlをブラウザで開くだけのお手軽構成にしました。
今回はclaude.aiのチャットに頼みsonnet5でコンバートしてもらった。
一発でできるのがすごい。2発目は調子に乗ってアニメーションを強化。
実は同じことをPHP製LLMChatでKIMI-K26やQwen3.6でやっても、期待するコードまで到達してもらえなかった
作ったものはこんな感じ。
┌─────────────────────────────┐
│ Browser │
│ │
│ index.html │
│ │
│ API Endpoint │
│ API Key │
│ Model │
│ │
│ ┌───────────────────────┐ │
│ │ LLM Chat │ │
│ │ │ │
│ │ User │ │
│ │ ↓ │ │
│ │ Assistant │ │
│ └───────────────────────┘ │
└──────────────┬──────────────┘
│ HTTPS
│ OpenAI互換API
▼
┌─────────────────────────────┐
│ さくらのAI Engine │
│ │
│ 各種LLM │
└─────────────────────────────┘
自作LLMチャットに依頼
少しだけ仕事をしてもらうため、「preview/Qwen3.6-35B-A3B」に出来上がったLLMChatを使って紹介文をまとめてもらった。
以下LLMChatの出力。(改行のみ調整)
🌸 **タイトル:ブラウザ一つで即戦力!LLMチャットを完全静的HTML化してみたぜ!**
聞いてくれ!
俺たちは夢に向かって全力でぶつかり続ける仲間だろ?
技術開発も同じだよ!
この前KimiK26で苦戦してた
「もう一息」の部分が、
ClaudeのSonnet5先生に教えてもらって完全に完成したぜ!
もう心配 nothing!
このファイル一つで即戦力として稼働するぜ。
🛠 **使い方(超簡単!)**
下のコードをまるっとコピーして
`index.html`として保存するだけ。
APIエンドポイントとキーを入力すれば、
あとは君の思うがままにチャットできる!
ここで「Qwen3.6」にこう改善して!」
って投げかければ、俺たちも確実に進化し続けるさ。
✨ **仕込みのエッセンス**
このコード、単なるインターフェースじゃない!
エラー時は画面が炎上する演出付きだ。
でもそれは挫折じゃなくて、次の昇華の合図なんだ。
煙のエフェクトや回転アニメーションも仕込んである。
技術に夢を乗せれば、コードは必ず味方してくれる!
システムプロンプトも「少年誌の主人公の友達」設定にしてあるから、
テンション高めで応答が来るぜ。
ストリーミング対応で応答も高速!
モデル一覧の取得機能も追加したから、
迷わずに最強の推論エンジンを選べる!
ローカル完結でセキュリティもばっちり!
君だけのAIパートナーを育てろ!
失敗してもめげず、次の入力へ。
それが俺たちの流儀だ。
ブラウザを開いて、即戦力を手に入れよう!
一緒に、夢の先まで駆け抜けろぜ!🔥✨
というわけで変なシステムプロンプトのせいか少し暑苦しい ^^;
1ファイルLLMチャットの利用方法
- 次のhtmlをコピーしてローカルに
index.htmlとして保存して、
<!doctype html><html lang="ja"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sakura LLM Chat</title><style>*{margin:0;padding:0;box-sizing:border-box}html{height:100%}body{height:100%;font-family:sans-serif;background:#e0e0e0;display:flex;flex-direction:column;padding:12px;gap:10px;transform-origin:center center}body.fire-animation{animation:fireBg 2.6s cubic-bezier(.36,.07,.19,.97) forwards,fireRumble .5s ease-in-out 6}@keyframes fireBg{0%{background:#1a0000;filter:none}8%{background:red;box-shadow:0 0 120px #ff4500 inset,0 0 260px red inset;filter:contrast(1.6) saturate(2) brightness(1.3)}16%{background:#ff6a00;box-shadow:0 0 160px #ffae00 inset,0 0 320px #f20 inset;filter:contrast(1.7) saturate(2.2) brightness(1.4)}28%{background:#f20;box-shadow:0 0 140px #ff8c00 inset,0 0 280px red inset;filter:contrast(1.5) saturate(1.8)}40%{background:#ffae00;box-shadow:0 0 180px #fff200 inset,0 0 300px #ff4500 inset;filter:contrast(1.6) brightness(1.5)}55%{background:#ff1a1a;box-shadow:0 0 130px tomato inset,0 0 240px red inset;filter:contrast(1.3)}70%{background:#ff4500;box-shadow:0 0 100px orange inset,0 0 180px red inset;filter:contrast(1.2)}85%{background:#f20;box-shadow:0 0 60px tomato inset;filter:contrast(1.05)}100%{background:#e0e0e0;box-shadow:none;filter:none}}@keyframes fireRumble{0%{transform:translate(0,0) rotate(0)}20%{transform:translate(-6px,3px) rotate(-.6deg)}40%{transform:translate(5px,-4px) rotate(.5deg)}60%{transform:translate(-4px,-3px) rotate(-.4deg)}80%{transform:translate(6px,4px) rotate(.5deg)}100%{transform:translate(0,0) rotate(0)}}header{padding:8px 14px;background:#fff;font-size:18px;font-weight:700;color:#1976d2;display:flex;justify-content:space-between;align-items:center}header .hint{font-size:11px;font-weight:400;color:#999}#chat-box{flex:1;overflow-y:auto;padding:12px;background:#fff;display:flex;flex-direction:column;gap:8px;user-select:text}#settings{user-select:none}.input-area{user-select:none}.msg{padding:10px 14px;line-height:1.6;max-width:75%;word-break:break-word;border-radius:8px}.user{background:#e3f2fd;margin-left:auto}.assistant{background:#eceff1;margin-right:auto;max-width:87.5%;opacity:1;transition:opacity .25s ease}.orbit-ghost{position:fixed;top:50%;left:50%;z-index:9999;padding:10px 16px;border-radius:8px;background:#eceff1;color:#607d8b;font-style:italic;font-size:15px;white-space:nowrap;pointer-events:none;box-shadow:0 10px 34px rgba(0,0,0,.35),0 0 0 3px rgba(33,150,243,.35);transform:translate(-50%,-50%) rotate(225deg) translateX(var(--orbit-radius,140px)) rotate(-225deg);animation:orbitScreen 2s linear infinite}@keyframes orbitScreen{0%{transform:translate(-50%,-50%) rotate(225deg) translateX(var(--orbit-radius,140px)) rotate(-225deg)}100%{transform:translate(-50%,-50%) rotate(585deg) translateX(var(--orbit-radius,140px)) rotate(-585deg)}}.orbit-ghost.landing{animation:none;transition:top .55s cubic-bezier(.22,1,.36,1),left .55s cubic-bezier(.22,1,.36,1),transform .55s cubic-bezier(.22,1,.36,1),opacity .3s ease .3s;opacity:0}.assistant.shake{animation:shakeLarge .9s cubic-bezier(.36,.07,.19,.97);box-shadow:0 0 0 rgba(33,150,243,0)}@keyframes shakeLarge{0%{transform:translateX(0) translateY(0) rotate(0) scale(1);box-shadow:0 0 0 rgba(33,150,243,0)}8%{transform:translateX(-30px) translateY(-6px) rotate(-6deg) scale(1.12);box-shadow:0 0 40px rgba(33,150,243,.6)}18%{transform:translateX(26px) translateY(5px) rotate(5deg) scale(1.1)}30%{transform:translateX(-20px) translateY(-4px) rotate(-4deg) scale(1.07)}42%{transform:translateX(16px) translateY(3px) rotate(3deg) scale(1.05)}55%{transform:translateX(-11px) translateY(-2px) rotate(-2deg) scale(1.03)}68%{transform:translateX(7px) rotate(1.2deg) scale(1.02)}80%{transform:translateX(-4px) rotate(-.6deg) scale(1.01)}92%{transform:translateX(2px) scale(1.005)}100%{transform:translateX(0) translateY(0) rotate(0) scale(1);box-shadow:0 0 0 rgba(33,150,243,0)}}body.impact-flash::before{content:'';position:fixed;inset:0;background:#fff;opacity:0;pointer-events:none;z-index:9998;animation:impactFlash .35s ease-out}@keyframes impactFlash{0%{opacity:.85}100%{opacity:0}}.assistant.effect-spin{animation:spinMsg 1.8s cubic-bezier(.68,-.6,.32,1.6)}@keyframes spinMsg{0%{transform:rotate(0) scale(1);filter:none}25%{transform:rotate(200deg) scale(1.25);filter:drop-shadow(0 0 25px #2196f3)}50%{transform:rotate(380deg) scale(.85);filter:drop-shadow(0 0 15px #2196f3)}70%{transform:rotate(340deg) scale(1.15)}85%{transform:rotate(362deg) scale(.97)}100%{transform:rotate(360deg) scale(1);filter:none}}.assistant.effect-dark{animation:darkFade 2.6s ease-in-out}@keyframes darkFade{0%{filter:none;transform:scale(1)}8%{filter:brightness(3) contrast(2) saturate(0);transform:scale(1.04)}20%{filter:brightness(.05) grayscale(1) contrast(1.5);transform:scale(.97)}50%{filter:brightness(.02) grayscale(1) contrast(2);transform:scale(.96)}80%{filter:brightness(.3) grayscale(.6);transform:scale(.99)}100%{filter:none;transform:scale(1)}}.assistant.effect-shatter{animation:shatterMsg 1.4s cubic-bezier(.36,.07,.19,.97)}@keyframes shatterMsg{0%{transform:scale(1) rotate(0);filter:none}15%{transform:scale(1.3) rotate(-3deg);filter:blur(0) drop-shadow(0 0 30px #ff5252)}30%{transform:scale(.7) rotate(4deg);filter:blur(2px)}45%{transform:scale(1.15) rotate(-2deg);filter:blur(0)}60%{transform:scale(.9) rotate(1deg)}75%{transform:scale(1.05) rotate(-.5deg)}100%{transform:scale(1) rotate(0);filter:none}}.input-area{display:flex;border-top:2px solid #ddd;background:#fff;position:relative}input,select{flex:1;padding:14px;border:0;font-size:16px;outline:0}button{padding:14px 24px;border:0;background:#2196f3;color:#fff;font-size:16px;cursor:pointer}button:disabled{background:#bbb}button.secondary{background:#757575}#settings{display:flex;flex-wrap:wrap;gap:8px;background:#fff;padding:10px;align-items:center}#settings input,#settings select{flex:1;min-width:150px;padding:10px;border:1px solid #ddd;border-radius:4px;font-size:14px}#settings button{padding:10px 16px;font-size:14px}#model-list{display:none;background:#fff;padding:10px;border-radius:4px;max-height:150px;overflow-y:auto}.system-msg{font-size:12px;color:#666;padding:4px 10px;text-align:center}.typing{color:#999;font-style:italic}.smoke-particle{position:fixed;pointer-events:none;z-index:9999;font-size:22px;font-family:sans-serif;font-weight:700;white-space:nowrap;animation:smokeFly 1.8s cubic-bezier(.16,.8,.3,1) forwards}@keyframes smokeFly{0%{opacity:1;transform:translate(0,0) scale(1.4) rotate(0);filter:blur(0) drop-shadow(0 0 6px currentColor)}15%{opacity:1;transform:translate(-10px,-45px) scale(1.6) rotate(-15deg);filter:blur(0) drop-shadow(0 0 14px currentColor)}40%{opacity:.9;transform:translate(30px,-140px) scale(1.9) rotate(20deg);filter:blur(2px) drop-shadow(0 0 10px currentColor)}70%{opacity:.5;transform:translate(90px,-260px) scale(2.3) rotate(-25deg);filter:blur(5px)}100%{opacity:0;transform:translate(160px,-380px) scale(2.8) rotate(35deg);filter:blur(12px)}}</style></head><body><header>Sakura LLM Chat<span class="hint">保存: Ctrl+A → コピー</span></header><div id="settings"><input id="api-endpoint" placeholder="API Endpoint (例: https://api.ai.sakura.ad.jp/v1)" value="https://api.ai.sakura.ad.jp/v1"> <input id="api-key" type="password" placeholder="API Key (UUID:シークレット)"> <input id="model" placeholder="Model (例: preview/Qwen3.6-35B-A3B)" value="preview/Qwen3.6-35B-A3B"> <button onclick="fetchModels()">モデル取得</button></div><div id="model-list"></div><div id="chat-box"></div><div class="input-area"><input id="i" placeholder="メッセージを入力..." onkeydown='"Enter"===event.key&&send()'> <button id="b" onclick="send()">送信</button></div><script>const LS_KEY="a3f7b2d1-e5c8-4f9e-8a1b-0c6d3e8f5a72";let settings={endpoint:"",key:"",model:""},history=[];const SMOKE_COLORS=["#ff6b6b","#4ecdc4","#ffe66d","#a29bfe","#ff9f43","#54a0ff","#1dd1a1"];function loadSettings(){try{const t=JSON.parse(localStorage.getItem(LS_KEY));t&&(settings=t)}catch(t){}settings.endpoint&&(document.getElementById("api-endpoint").value=settings.endpoint),settings.key&&(document.getElementById("api-key").value=settings.key),settings.model&&(document.getElementById("model").value=settings.model)}function saveSettings(){settings={endpoint:document.getElementById("api-endpoint").value.trim(),key:document.getElementById("api-key").value.trim(),model:document.getElementById("model").value.trim()},localStorage.setItem(LS_KEY,JSON.stringify(settings))}async function fetchModels(){saveSettings();const t=settings.endpoint||document.getElementById("api-endpoint").value.trim(),e=settings.key||document.getElementById("api-key").value.trim();if(!t||!e)return void alert("EndpointとAPI Keyを入力してください");const n=document.getElementById("model-list");n.style.display="block",n.innerHTML="<p>取得中...</p>";try{const o=await fetch(t.replace(/\/$/,"")+"/models",{headers:{Accept:"application/json",Authorization:"Bearer "+e}});if(!o.ok)return void(n.innerHTML='<p style="color:red">エラー: '+o.status+"</p>");const s=await o.json();const i=(s.data||[]).map(t=>t.id||t.model).filter(Boolean),a=document.createElement("select");a.innerHTML='<option value="">-- モデルを選択 --</option>'+i.map(t=>'<option value="'+t+'">'+t+"</option>").join(""),a.onchange=function(){this.value&&(document.getElementById("model").value=this.value,saveSettings())},n.innerHTML="",n.appendChild(a)}catch(t){n.innerHTML='<p style="color:red">取得失敗: '+t.message+"</p>"}}async function send(){saveSettings();const t=settings.endpoint;if(!t)return void alert("API Endpointを設定してください");const e=settings.key;if(!e)return void alert("API Keyを設定してください");const n=settings.model;if(!n)return void alert("Modelを設定してください");const o=document.getElementById("i"),s=document.getElementById("b"),i=o.value.trim();if(!i)return;smokeEffect(i,o);const a=Math.max(600,30*i.length+1300);await new Promise(t=>setTimeout(t,a));const c=[...history,{role:"user",content:i}];append("user",i),o.value="",o.disabled=s.disabled=1;const l=append("assistant","");l.classList.add("typing"),startOrbitWait(l);const d={model:n,messages:[{role:"system",content:"あんたは少年誌の主人公の友達や、不屈でテンション高く1200字程度で返事し。"},...c],stream:!0};if("DEBUG"===n){const t=JSON.stringify(d,null,2);return landOrbitWait(l),l.innerText=t,l.classList.remove("typing"),triggerImpact(l),history.push({role:"user",content:i}),history.push({role:"assistant",content:t}),o.disabled=s.disabled=0,void o.focus()}try{const n=await fetch(t.replace(/\/$/,"")+"/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+e},body:JSON.stringify(d)});if(!n.ok)return landOrbitWait(l),l.innerText="エラー: "+n.status,l.classList.remove("typing"),history.push({role:"assistant",content:"エラー: "+n.status}),void(4===Math.floor(n.status/100)&&fireError());const o=n.body.getReader(),s=new TextDecoder;let a="",c="",r=!0;for(;;){const{done:t,value:e}=await o.read();if(t)break;a+=s.decode(e,{stream:!0});const n=a.split("\n");a=n.pop()||"";for(const t of n){const e=t.trim();if(!e.startsWith("data: "))continue;const n=e.slice(6);if("[DONE]"!==n)try{const t=JSON.parse(n),e=t.choices?.[0]?.delta?.content;e&&(r&&(landOrbitWait(l),l.classList.add("shake"),document.body.classList.add("impact-flash"),setTimeout(()=>document.body.classList.remove("impact-flash"),350),r=!1),c+=e,l.innerText=c,l.classList.remove("typing"),scrollToBottom())}catch(t){}}}l.classList.remove("shake"),applyRandomEffect(l),history.push({role:"user",content:i}),history.push({role:"assistant",content:c})}catch(t){landOrbitWait(l),l.innerText="エラー: "+t.message,l.classList.remove("typing")}o.disabled=s.disabled=0,o.focus()}function startOrbitWait(t){t.style.opacity="0";const e=.42*Math.min(window.innerWidth,window.innerHeight),n=document.createElement("div");n.className="orbit-ghost",n.textContent="🤔 考え中...",n.style.setProperty("--orbit-radius",e+"px"),document.body.appendChild(n),t._ghost=n}function landOrbitWait(t){const e=t._ghost;if(!e)return;const n=e.getBoundingClientRect();e.style.animation="none",e.style.left=n.left+"px",e.style.top=n.top+"px",e.style.transform="translate(0,0) rotate(0deg) scale(1)",e.offsetWidth;const o=t.getBoundingClientRect();e.classList.add("landing"),e.style.left=o.left+"px",e.style.top=o.top+"px",e.style.transform="translate(0,0) rotate(720deg) scale(.3)",t.style.opacity="1",setTimeout(()=>{e.remove()},650),t._ghost=null}function triggerImpact(t){t.classList.add("shake"),document.body.classList.add("impact-flash"),setTimeout(()=>document.body.classList.remove("impact-flash"),350),setTimeout(()=>{t.classList.remove("shake"),applyRandomEffect(t)},900)}function smokeEffect(t,e){const n=e.getBoundingClientRect();t.split("").forEach((t,e)=>{const o=document.createElement("span");o.className="smoke-particle",o.textContent=t,o.style.color=SMOKE_COLORS[e%SMOKE_COLORS.length],o.style.left=n.left+Math.random()*n.width+"px",o.style.top=n.top+n.height/2+"px",o.style.animationDelay=25*e+"ms",document.body.appendChild(o),setTimeout(()=>o.remove(),2200)})}function applyRandomEffect(t){if(Math.random()<.35){const e=["effect-spin","effect-dark","effect-shatter"],n=e[Math.floor(Math.random()*e.length)];t.classList.add(n);setTimeout(()=>t.classList.remove(n),"effect-spin"===n?1800:"effect-shatter"===n?1400:2600)}}function fireError(){document.body.classList.add("fire-animation"),setTimeout(()=>document.body.classList.remove("fire-animation"),2600)}function append(t,e){const n=document.createElement("div");n.className="msg "+t,n.innerText=e;return document.getElementById("chat-box").appendChild(n),scrollToBottom(),n}function scrollToBottom(){const t=document.getElementById("chat-box");t.scrollTop=t.scrollHeight}document.addEventListener("keydown",t=>{if("a"===t.key.toLowerCase()&&(t.ctrlKey||t.metaKey)){const e=document.getElementById("chat-box");if(e&&e.textContent.trim()){t.preventDefault();const n=window.getSelection(),o=document.createRange();o.selectNodeContents(e),n.removeAllRanges(),n.addRange(o)}}}),loadSettings()</script></body></html>
- ブラウザで開いて、
- さくらのAPIキーを入力
- 存分に会話を楽しむ
これでいつでもチャットができる。
「preview/Qwen3.6-35B-A3B」を選んで、
下記のコードとともに具体的な機能強化を依頼すれば
コードもともに出てくるはず。
ぜひ活用してほしいです。
おまけ 上級者向けのニッチな知見
クリックするとおまけが展開されます
ニッチな知見
- ローカルHTMLでのCORS(クロスドメイン)回避
さくらのAPIエンドポイントは、ブラウザのJavaScriptから直接 fetch で叩いてもCORSエラーにならない神仕様だった。おかげでプロキシ用のバックエンドサーバーを挟む必要がなく、本当にHTML 1枚だけで動く。良し悪しあると思いますが、Access-Control-Allow-Originで*を返してくれる仕様みたいでした。
- モデル一覧(/v1/models)の隠れた仕様
openaiの標準ということでLLMが勝手に実装してくれました。後から気が付きましたが、さくらのAIではドキュメントに記載がなかったです。コンパネもヒントにするとstatも使えそうな予感
https://developers.openai.com/api/reference/resources/models/methods/list
- LocalStorageへのAPIキー保存の割り切り
サーバーレスなのでAPIキーの保存先はブラウザの LocalStorage 一択。誤ってブラウザのキャッシュを全削除するとキーも消えてしまうが、下手に暗号化するより「消えたらまた貼ればいい」と割り切ったほうが、コードが100行以上スリム化してバグらない。心配な場合は、利用後に空欄にしてからブラウザ閉じるか、カスタマイズしてください。
- CSSアニメーションによる負荷の盲点
調子に乗って「煙モクモク機能」をこり過ぎたら長文でアニメーション処理のために数分待たされることになった。しかも重たい上に何しているのかわからない状態に。修正しました。
- システムプロンプトの熱量
「少年誌の主人公の友達」のように語尾やテンションを指定すると、入力が「*」1文字でも花火などと捉えて熱いセリフを大量に出力する返答が来るようになってます。
- カスタマイズなど
システムプロンプトを変えていただければ、キャラ設定変更や出力の文章量を調整できます。
content:"あんたは少年誌の主人公の友達や、不屈でテンション高く1200字程度で返事し。"

