From e7bf848fa551f1836ed4ec3f85bdd6676a52af8b Mon Sep 17 00:00:00 2001 From: XTRA_xty <3212559507@qq.com> Date: Thu, 4 Jun 2026 10:44:15 +0800 Subject: [PATCH] Add GitHub chat panel --- README.md | 8 +- github-chat.css | 215 +++++++++++++++++++++++++++++++ github-chat.js | 331 ++++++++++++++++++++++++++++++++++++++++++++++++ manifest.json | 16 ++- 4 files changed, 567 insertions(+), 3 deletions(-) create mode 100644 github-chat.css create mode 100644 github-chat.js diff --git a/README.md b/README.md index 0af0dd3..d2d065c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ # GitHub Stories -View stories on your GitHub dashboard. +View stories on your GitHub dashboard and keep local chat notes on GitHub pages. No uploads required. Just [install](#how-to-install) and visit [github.com](https://github.com). ![GitHub Stories Demo](./github-stories.gif) +## GitHub Chat + +The extension adds a `Chat` button in the lower-right corner of every `github.com` page. Open it to write page-specific notes, review previous messages, or clear the current page history. + +Messages are stored locally with `chrome.storage.local`. Repository pages share one chat thread per `owner/repo`, while GitHub system pages use their page path. The panel also updates when GitHub changes pages without a full reload. + ## How to Install 1. [Download ZIP](https://github.com/inquid/github-stories/archive/master.zip) and unzip on your computer. diff --git a/github-chat.css b/github-chat.css new file mode 100644 index 0000000..b16b9fe --- /dev/null +++ b/github-chat.css @@ -0,0 +1,215 @@ +#github-stories-chat-root { + color-scheme: light; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + position: fixed; + z-index: 2147483647; +} + +#github-stories-chat-root * { + box-sizing: border-box; +} + +.ghs-chat-toggle { + align-items: center; + background: #24292f; + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 999px; + bottom: 24px; + box-shadow: 0 10px 28px rgba(31, 35, 40, 0.25); + color: #ffffff; + cursor: pointer; + display: inline-flex; + font-size: 14px; + font-weight: 600; + gap: 8px; + line-height: 1; + min-height: 44px; + padding: 0 15px; + position: fixed; + right: 24px; +} + +.ghs-chat-toggle:focus-visible, +.ghs-chat-panel button:focus-visible, +.ghs-chat-input:focus { + outline: 2px solid #0969da; + outline-offset: 2px; +} + +.ghs-chat-count { + align-items: center; + background: #ffffff; + border-radius: 999px; + color: #24292f; + display: inline-flex; + font-size: 12px; + justify-content: center; + min-width: 22px; + padding: 4px 6px; +} + +.ghs-chat-panel { + background: #ffffff; + border: 1px solid #d0d7de; + border-radius: 8px; + bottom: 78px; + box-shadow: 0 16px 48px rgba(31, 35, 40, 0.22); + display: flex; + flex-direction: column; + max-height: min(560px, calc(100vh - 110px)); + overflow: hidden; + position: fixed; + right: 24px; + width: min(380px, calc(100vw - 32px)); +} + +.ghs-chat-hidden { + display: none; +} + +.ghs-chat-header { + align-items: flex-start; + background: #f6f8fa; + border-bottom: 1px solid #d8dee4; + display: flex; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; +} + +.ghs-chat-header strong { + color: #24292f; + display: block; + font-size: 14px; + line-height: 20px; +} + +.ghs-chat-context { + color: #57606a; + display: block; + font-size: 12px; + line-height: 18px; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ghs-chat-actions { + display: flex; + gap: 6px; +} + +.ghs-chat-actions button, +.ghs-chat-send { + background: #f6f8fa; + border: 1px solid #d0d7de; + border-radius: 6px; + color: #24292f; + cursor: pointer; + font-size: 12px; + font-weight: 600; + line-height: 18px; + padding: 4px 8px; +} + +.ghs-chat-actions button:disabled { + color: #8c959f; + cursor: default; +} + +.ghs-chat-close { + min-width: 28px; +} + +.ghs-chat-messages { + background: #ffffff; + display: flex; + flex: 1; + flex-direction: column; + gap: 10px; + min-height: 170px; + overflow-y: auto; + padding: 14px; +} + +.ghs-chat-empty { + color: #57606a; + font-size: 13px; + margin: auto; + text-align: center; +} + +.ghs-chat-message { + background: #f6f8fa; + border: 1px solid #d8dee4; + border-radius: 8px; + color: #24292f; + padding: 9px 10px; +} + +.ghs-chat-message-meta { + color: #57606a; + font-size: 11px; + line-height: 16px; + margin-bottom: 4px; +} + +.ghs-chat-message-body { + font-size: 13px; + line-height: 19px; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.ghs-chat-form { + border-top: 1px solid #d8dee4; + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; +} + +.ghs-chat-input { + border: 1px solid #d0d7de; + border-radius: 6px; + color: #24292f; + font: inherit; + font-size: 13px; + line-height: 19px; + min-height: 74px; + padding: 8px; + resize: vertical; +} + +.ghs-chat-footer { + align-items: center; + display: flex; + gap: 12px; + justify-content: space-between; +} + +.ghs-chat-status { + color: #57606a; + font-size: 12px; + line-height: 18px; +} + +.ghs-chat-send { + background: #1f883d; + border-color: #1f883d; + color: #ffffff; + min-width: 64px; +} + +@media (max-width: 480px) { + .ghs-chat-panel { + bottom: 72px; + right: 16px; + } + + .ghs-chat-toggle { + bottom: 18px; + right: 16px; + } +} diff --git a/github-chat.js b/github-chat.js new file mode 100644 index 0000000..8532221 --- /dev/null +++ b/github-chat.js @@ -0,0 +1,331 @@ +(() => { + const ROOT_ID = 'github-stories-chat-root'; + const STORAGE_PREFIX = 'githubStoriesChat'; + const MAX_MESSAGES = 100; + const MAX_MESSAGE_LENGTH = 2000; + + if (window.location.hostname !== 'github.com') { + return; + } + + if (document.getElementById(ROOT_ID)) { + return; + } + + const state = { + context: getChatContext(), + messages: [], + isOpen: false, + lastLocation: window.location.href, + }; + + const root = document.createElement('div'); + root.id = ROOT_ID; + root.innerHTML = ` + +
+
+
+ GitHub Chat + +
+
+ + +
+
+
+
+ + +
+
+ `; + + document.body.appendChild(root); + + const elements = { + toggle: root.querySelector('.ghs-chat-toggle'), + panel: root.querySelector('.ghs-chat-panel'), + count: root.querySelector('.ghs-chat-count'), + context: root.querySelector('.ghs-chat-context'), + messages: root.querySelector('.ghs-chat-messages'), + form: root.querySelector('.ghs-chat-form'), + input: root.querySelector('.ghs-chat-input'), + status: root.querySelector('.ghs-chat-status'), + send: root.querySelector('.ghs-chat-send'), + clear: root.querySelector('.ghs-chat-clear'), + close: root.querySelector('.ghs-chat-close'), + }; + + elements.toggle.addEventListener('click', () => setPanelOpen(!state.isOpen)); + elements.close.addEventListener('click', () => setPanelOpen(false)); + elements.clear.addEventListener('click', clearMessages); + elements.form.addEventListener('submit', handleSubmit); + elements.input.addEventListener('keydown', handleInputKeydown); + + document.addEventListener('turbo:load', syncLocationContext); + document.addEventListener('pjax:end', syncLocationContext); + window.addEventListener('popstate', syncLocationContext); + window.addEventListener('hashchange', syncLocationContext); + window.setInterval(syncLocationContext, 800); + + updateContextLabel(); + loadMessages(); + + async function handleSubmit(event) { + event.preventDefault(); + + const text = elements.input.value.trim(); + if (!text) { + setStatus('Write a message before sending.'); + return; + } + + const message = { + id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, + text: text.slice(0, MAX_MESSAGE_LENGTH), + createdAt: new Date().toISOString(), + }; + + state.messages = [...state.messages, message].slice(-MAX_MESSAGES); + await saveMessages(); + elements.input.value = ''; + renderMessages(); + setStatus('Message saved locally.'); + } + + function handleInputKeydown(event) { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + elements.form.requestSubmit(); + } + } + + async function clearMessages() { + if (!state.messages.length) { + setStatus('No messages to clear.'); + return; + } + + if (!window.confirm('Clear this page chat history?')) { + return; + } + + state.messages = []; + await removeMessages(); + renderMessages(); + setStatus('Chat history cleared.'); + } + + function setPanelOpen(isOpen) { + state.isOpen = isOpen; + elements.panel.classList.toggle('ghs-chat-hidden', !isOpen); + elements.toggle.setAttribute('aria-expanded', String(isOpen)); + + if (isOpen) { + loadMessages().then(() => { + elements.input.focus(); + }); + } + } + + async function loadMessages() { + const value = await storageGet(getStorageKey()); + state.messages = Array.isArray(value) ? value.slice(-MAX_MESSAGES) : []; + renderMessages(); + } + + async function saveMessages() { + await storageSet(getStorageKey(), state.messages); + } + + async function removeMessages() { + await storageRemove(getStorageKey()); + } + + function renderMessages() { + elements.count.textContent = String(state.messages.length); + elements.clear.disabled = state.messages.length === 0; + elements.messages.replaceChildren(); + + if (!state.messages.length) { + const empty = document.createElement('p'); + empty.className = 'ghs-chat-empty'; + empty.textContent = 'No messages yet for this page.'; + elements.messages.appendChild(empty); + return; + } + + state.messages.forEach((message) => { + const article = document.createElement('article'); + article.className = 'ghs-chat-message'; + + const meta = document.createElement('div'); + meta.className = 'ghs-chat-message-meta'; + meta.textContent = `You - ${formatTimestamp(message.createdAt)}`; + + const body = document.createElement('div'); + body.className = 'ghs-chat-message-body'; + body.textContent = message.text; + + article.appendChild(meta); + article.appendChild(body); + elements.messages.appendChild(article); + }); + + elements.messages.scrollTop = elements.messages.scrollHeight; + } + + function syncLocationContext() { + if (state.lastLocation === window.location.href) { + return; + } + + state.lastLocation = window.location.href; + state.context = getChatContext(); + updateContextLabel(); + loadMessages(); + } + + function updateContextLabel() { + elements.context.textContent = state.context.label; + } + + function getStorageKey() { + return `${STORAGE_PREFIX}:${state.context.key}`; + } + + function setStatus(message) { + elements.status.textContent = message; + } + + function getChatContext() { + const parts = window.location.pathname.split('/').filter(Boolean); + + if (parts.length >= 2 && !isGitHubSystemPath(parts[0])) { + const repoKey = `${parts[0]}/${parts[1]}`; + return { + key: `repo:${repoKey}`, + label: repoKey, + }; + } + + const page = window.location.pathname === '/' ? 'home' : window.location.pathname; + return { + key: `page:${page}`, + label: page === 'home' ? 'github.com home' : page, + }; + } + + function isGitHubSystemPath(segment) { + return [ + 'dashboard', + 'explore', + 'marketplace', + 'notifications', + 'organizations', + 'pulls', + 'settings', + 'topics', + 'trending', + ].includes(segment); + } + + function formatTimestamp(value) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return 'just now'; + } + + return date.toLocaleString([], { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } + + function hasChromeStorage() { + return ( + typeof chrome !== 'undefined' && + chrome.storage && + chrome.storage.local + ); + } + + function storageGet(key) { + if (!hasChromeStorage()) { + return Promise.resolve(readLocalStorage(key)); + } + + return new Promise((resolve) => { + chrome.storage.local.get([key], (result) => { + if (chrome.runtime.lastError) { + resolve(readLocalStorage(key)); + return; + } + + resolve(result[key]); + }); + }); + } + + function storageSet(key, value) { + if (!hasChromeStorage()) { + writeLocalStorage(key, value); + return Promise.resolve(); + } + + return new Promise((resolve) => { + chrome.storage.local.set({ [key]: value }, () => { + if (chrome.runtime.lastError) { + writeLocalStorage(key, value); + } + + resolve(); + }); + }); + } + + function storageRemove(key) { + if (!hasChromeStorage()) { + window.localStorage.removeItem(key); + return Promise.resolve(); + } + + return new Promise((resolve) => { + chrome.storage.local.remove(key, () => { + if (chrome.runtime.lastError) { + window.localStorage.removeItem(key); + } + + resolve(); + }); + }); + } + + function readLocalStorage(key) { + try { + return JSON.parse(window.localStorage.getItem(key) || '[]'); + } catch (error) { + return []; + } + } + + function writeLocalStorage(key, value) { + window.localStorage.setItem(key, JSON.stringify(value)); + } +})(); diff --git a/manifest.json b/manifest.json index db0a183..5230b35 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "GitHub Stories", - "version": "0.1", - "description": "Stories on GitHub", + "version": "0.2", + "description": "Stories and local chat notes on GitHub", "permissions": [ "activeTab", "declarativeContent", @@ -27,6 +27,18 @@ "story-list.css", "story-view.css" ] + }, + { + "matches": [ + "https://github.com/*" + ], + "js": [ + "github-chat.js" + ], + "css": [ + "github-chat.css" + ], + "run_at": "document_idle" } ], "icons": {