From a46f072602ad096c50fde1303e28224e38970a1b Mon Sep 17 00:00:00 2001 From: reodesureodesu Date: Tue, 3 Mar 2026 19:59:31 +0900 Subject: [PATCH 1/3] Add Gemini Developer API key mode for AI button --- icons/app-icon.svg | 12 + index.html | 2250 ++++++++++++++++++++++++++++++++++++++++---- manifest.json | 18 + sw.js | 30 + 4 files changed, 2115 insertions(+), 195 deletions(-) create mode 100644 icons/app-icon.svg create mode 100644 manifest.json create mode 100644 sw.js diff --git a/icons/app-icon.svg b/icons/app-icon.svg new file mode 100644 index 0000000..6aa37ea --- /dev/null +++ b/icons/app-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/index.html b/index.html index fa8bab5..c0c4c0a 100644 --- a/index.html +++ b/index.html @@ -3,11 +3,14 @@ + + + + LINE Ultimate Stable
-
LINE Ultimate
- -
-

ログインしてチャットを始めましょう

-

Google またはメールアドレスでサインインできます。

- - - - - -
+
-
-
-
- -
- - + + +
+ + LINE Ultimate +
+ + + + avatar + +
+
+ +
通知ボックス

送信済み申請
+ + + +
+ 着信中... +
+ + +
+
+ +
+
+ ビデオ通話 + +
+
+
+ 相手 + +
+
+ 自分 + +
+
+
+ +
+

ログインしてチャットを始めましょう

+

Google またはメールアドレスでサインインできます。

+
+ + ※ Googleログインが失敗した場合はメールログインをお使いください。 + + + + + +
+ +
+
+
+ + + + + 全体 +
+
+
+
+ +
+ +
+ + + +
+ +
+
-
- -
diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..42b266d --- /dev/null +++ b/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "LINE Ultimate", + "short_name": "LINE Ult", + "description": "チャットとミニゲームを楽しめるLINE風アプリ", + "start_url": "./index.html", + "scope": "./", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#06c755", + "icons": [ + { + "src": "./icons/app-icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..4ac604b --- /dev/null +++ b/sw.js @@ -0,0 +1,30 @@ +const CACHE_NAME = "line-ultimate-pwa-v1"; +const ASSETS = [ + "./", + "./index.html", + "./manifest.json", + "./icons/app-icon.svg" +]; + +self.addEventListener("install", (event) => { + event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))) + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + if (event.request.method !== "GET") return; + event.respondWith( + caches.match(event.request).then((cached) => cached || fetch(event.request).then((res) => { + const cloned = res.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, cloned)); + return res; + }).catch(() => caches.match("./index.html"))) + ); +}); From df88e582e07b05bbd57cde4bea500219e8aec666 Mon Sep 17 00:00:00 2001 From: reodesureodesu Date: Tue, 3 Mar 2026 20:31:10 +0900 Subject: [PATCH 2/3] Add automatic AI cooldown on 429 rate-limit errors --- index.html | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index c0c4c0a..a431f30 100644 --- a/index.html +++ b/index.html @@ -669,6 +669,7 @@

ログインしてチャットを始めましょう

passwordInput: document.getElementById("password"), displayNameInput: document.getElementById("displayName"), msgInput: document.getElementById("msg"), + aiBtn: document.getElementById("aiBtn"), imageInput: document.getElementById("imageInput"), profileImageInput: document.getElementById("profileImageInput"), backgroundImageInput: document.getElementById("backgroundImageInput"), @@ -764,6 +765,8 @@

ログインしてチャットを始めましょう

const AI_MODEL_CANDIDATES = ["gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-flash"]; const GEMINI_API_MODEL_CANDIDATES = ["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-2.0-flash"]; let geminiApiKey = localStorage.getItem("line_gemini_api_key") || ""; +let aiCooldownUntil = Number(localStorage.getItem("line_ai_cooldown_until") || 0); +let aiCooldownTimer = null; const authErrorMessages = { "auth/email-already-in-use": "このメールアドレスはすでに登録されています。ログインをお試しください。", @@ -1681,6 +1684,10 @@

ログインしてチャットを始めましょう

syncScreenControls(); updateRetryButton(); updateGeminiKeyHint(); +updateAiCooldownUi(); +if (aiCooldownUntil > Date.now()) { + aiCooldownTimer = setInterval(updateAiCooldownUi, 1000); +} renderStampBar(); ui.friendSearchInput.oninput = renderFriends; ui.retryQueueBtn.onclick = retryFailedQueue; @@ -2151,6 +2158,37 @@

ログインしてチャットを始めましょう

throw lastError || new Error("GEMINI_DEVELOPER_API_UNAVAILABLE"); } +const isRateLimitError = (error) => { + const text = `${error?.message || ""} ${error?.code || ""}`.toLowerCase(); + return text.includes("429") || text.includes("resource_exhausted") || text.includes("quota") || text.includes("rate"); +}; + +const updateAiCooldownUi = () => { + const remainingMs = aiCooldownUntil - Date.now(); + if (remainingMs <= 0) { + aiCooldownUntil = 0; + localStorage.removeItem("line_ai_cooldown_until"); + ui.aiBtn.disabled = false; + ui.aiBtn.textContent = "AI"; + if (aiCooldownTimer) { + clearInterval(aiCooldownTimer); + aiCooldownTimer = null; + } + return; + } + const sec = Math.ceil(remainingMs / 1000); + ui.aiBtn.disabled = true; + ui.aiBtn.textContent = `AI(${sec})`; +}; + +const startAiCooldown = (seconds = 60) => { + aiCooldownUntil = Date.now() + (seconds * 1000); + localStorage.setItem("line_ai_cooldown_until", String(aiCooldownUntil)); + updateAiCooldownUi(); + if (aiCooldownTimer) clearInterval(aiCooldownTimer); + aiCooldownTimer = setInterval(updateAiCooldownUi, 1000); +}; + const buildLocalAiFallback = (text) => { const clean = safeText(text).slice(0, 120); @@ -2181,7 +2219,13 @@

ログインしてチャットを始めましょう

throw lastError || new Error("AI model unavailable"); }; -document.getElementById("aiBtn").onclick = async () => { +ui.aiBtn.onclick = async () => { + updateAiCooldownUi(); + if (aiCooldownUntil > Date.now()) { + setStatus(`AIはクールダウン中です。${Math.ceil((aiCooldownUntil - Date.now())/1000)}秒後に再試行してください。`, "error"); + return; + } + const text = ui.msgInput.value.trim(); if (!text) { setStatus("AIに質問するメッセージを入力してください。", "error"); @@ -2207,6 +2251,10 @@

ログインしてチャットを始めましょう

setStatus("AI応答を送信しました。", "success"); } catch (error) { console.error("firebase ai logic error", error); + if (isRateLimitError(error)) { + startAiCooldown(60); + setStatus("429制限のためAIを60秒クールダウンします。しばらく待って再試行してください。", "error"); + } const fallbackText = buildLocalAiFallback(text); if (auth.currentUser) { await postMessage({ text: `🤖 ${fallbackText}`, image: null, audio: null }, true); @@ -2214,7 +2262,9 @@

ログインしてチャットを始めましょう

} else { ui.msgInput.value = fallbackText; } - setStatus("AI接続に失敗したため簡易AIモードで返信しました。", "info"); + if (!isRateLimitError(error)) { + setStatus("AI接続に失敗したため簡易AIモードで返信しました。", "info"); + } } }; From ffafa13ad905f769d79424c037d0a8ff628f4539 Mon Sep 17 00:00:00 2001 From: reodesureodesu Date: Wed, 4 Mar 2026 06:04:27 +0900 Subject: [PATCH 3/3] Update index.html --- index.html | 54 ++---------------------------------------------------- 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/index.html b/index.html index a431f30..c0c4c0a 100644 --- a/index.html +++ b/index.html @@ -669,7 +669,6 @@

ログインしてチャットを始めましょう

passwordInput: document.getElementById("password"), displayNameInput: document.getElementById("displayName"), msgInput: document.getElementById("msg"), - aiBtn: document.getElementById("aiBtn"), imageInput: document.getElementById("imageInput"), profileImageInput: document.getElementById("profileImageInput"), backgroundImageInput: document.getElementById("backgroundImageInput"), @@ -765,8 +764,6 @@

ログインしてチャットを始めましょう

const AI_MODEL_CANDIDATES = ["gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-flash"]; const GEMINI_API_MODEL_CANDIDATES = ["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-2.0-flash"]; let geminiApiKey = localStorage.getItem("line_gemini_api_key") || ""; -let aiCooldownUntil = Number(localStorage.getItem("line_ai_cooldown_until") || 0); -let aiCooldownTimer = null; const authErrorMessages = { "auth/email-already-in-use": "このメールアドレスはすでに登録されています。ログインをお試しください。", @@ -1684,10 +1681,6 @@

ログインしてチャットを始めましょう

syncScreenControls(); updateRetryButton(); updateGeminiKeyHint(); -updateAiCooldownUi(); -if (aiCooldownUntil > Date.now()) { - aiCooldownTimer = setInterval(updateAiCooldownUi, 1000); -} renderStampBar(); ui.friendSearchInput.oninput = renderFriends; ui.retryQueueBtn.onclick = retryFailedQueue; @@ -2158,37 +2151,6 @@

ログインしてチャットを始めましょう

throw lastError || new Error("GEMINI_DEVELOPER_API_UNAVAILABLE"); } -const isRateLimitError = (error) => { - const text = `${error?.message || ""} ${error?.code || ""}`.toLowerCase(); - return text.includes("429") || text.includes("resource_exhausted") || text.includes("quota") || text.includes("rate"); -}; - -const updateAiCooldownUi = () => { - const remainingMs = aiCooldownUntil - Date.now(); - if (remainingMs <= 0) { - aiCooldownUntil = 0; - localStorage.removeItem("line_ai_cooldown_until"); - ui.aiBtn.disabled = false; - ui.aiBtn.textContent = "AI"; - if (aiCooldownTimer) { - clearInterval(aiCooldownTimer); - aiCooldownTimer = null; - } - return; - } - const sec = Math.ceil(remainingMs / 1000); - ui.aiBtn.disabled = true; - ui.aiBtn.textContent = `AI(${sec})`; -}; - -const startAiCooldown = (seconds = 60) => { - aiCooldownUntil = Date.now() + (seconds * 1000); - localStorage.setItem("line_ai_cooldown_until", String(aiCooldownUntil)); - updateAiCooldownUi(); - if (aiCooldownTimer) clearInterval(aiCooldownTimer); - aiCooldownTimer = setInterval(updateAiCooldownUi, 1000); -}; - const buildLocalAiFallback = (text) => { const clean = safeText(text).slice(0, 120); @@ -2219,13 +2181,7 @@

ログインしてチャットを始めましょう

throw lastError || new Error("AI model unavailable"); }; -ui.aiBtn.onclick = async () => { - updateAiCooldownUi(); - if (aiCooldownUntil > Date.now()) { - setStatus(`AIはクールダウン中です。${Math.ceil((aiCooldownUntil - Date.now())/1000)}秒後に再試行してください。`, "error"); - return; - } - +document.getElementById("aiBtn").onclick = async () => { const text = ui.msgInput.value.trim(); if (!text) { setStatus("AIに質問するメッセージを入力してください。", "error"); @@ -2251,10 +2207,6 @@

ログインしてチャットを始めましょう

setStatus("AI応答を送信しました。", "success"); } catch (error) { console.error("firebase ai logic error", error); - if (isRateLimitError(error)) { - startAiCooldown(60); - setStatus("429制限のためAIを60秒クールダウンします。しばらく待って再試行してください。", "error"); - } const fallbackText = buildLocalAiFallback(text); if (auth.currentUser) { await postMessage({ text: `🤖 ${fallbackText}`, image: null, audio: null }, true); @@ -2262,9 +2214,7 @@

ログインしてチャットを始めましょう

} else { ui.msgInput.value = fallbackText; } - if (!isRateLimitError(error)) { - setStatus("AI接続に失敗したため簡易AIモードで返信しました。", "info"); - } + setStatus("AI接続に失敗したため簡易AIモードで返信しました。", "info"); } };