add shenyan-app RN project + PWA push/noise updates

This commit is contained in:
2026-05-13 17:38:35 +08:00
parent 66f94f0fcf
commit 4d7110c58d
82 changed files with 14677 additions and 124 deletions

63
PWA/package-lock.json generated
View File

@@ -2363,9 +2363,6 @@
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2380,9 +2377,6 @@
"arm"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2397,9 +2391,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2414,9 +2405,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2431,9 +2419,6 @@
"loong64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2448,9 +2433,6 @@
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2465,9 +2447,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2482,9 +2461,6 @@
"ppc64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2499,9 +2475,6 @@
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2516,9 +2489,6 @@
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2533,9 +2503,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2550,9 +2517,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2567,9 +2531,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2793,9 +2754,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2813,9 +2771,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2833,9 +2788,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2853,9 +2805,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5015,9 +4964,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -5039,9 +4985,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -5063,9 +5006,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -5087,9 +5027,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [

View File

@@ -1,4 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" fill="#000" rx="6"/>
<text x="16" y="22" text-anchor="middle" fill="#fff" font-size="18" font-family="system-ui">E</text>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="22" fill="none" stroke="#fff" stroke-width="3"/>
<line x1="50" y1="12" x2="50" y2="24" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="50" y1="76" x2="50" y2="88" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="12" y1="50" x2="24" y2="50" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="76" y1="50" x2="88" y2="50" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="23" y1="23" x2="32" y2="32" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="68" y1="68" x2="77" y2="77" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="23" y1="77" x2="32" y2="68" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="68" y1="23" x2="77" y2="32" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 223 B

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 12 KiB

26
PWA/public/sw-push.js Normal file
View File

@@ -0,0 +1,26 @@
self.addEventListener('push', function (event) {
var data = {};
try { data = event.data.json(); } catch (e) { data = { title: '沈晏', body: event.data ? event.data.text() : '' }; }
event.waitUntil(
self.registration.showNotification(data.title || '沈晏', {
body: data.body || '',
icon: '/icon-192.png',
badge: '/icon-192.png',
vibrate: [200, 100, 200],
tag: 'shenyan-push',
renotify: true,
})
);
});
self.addEventListener('notificationclick', function (event) {
event.notification.close();
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (list) {
for (var i = 0; i < list.length; i++) {
if (list[i].url && 'focus' in list[i]) return list[i].focus();
}
if (clients.openWindow) return clients.openWindow('/');
})
);
});

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M30 58 Q20 50 25 42 Q30 33 40 36 Q42 25 55 30 Q65 20 75 33 Q88 32 85 45 Q92 50 85 58 Q80 65 65 62 Q50 68 40 62 Z" fill="#fff" opacity="0.9"/>
<path d="M35 55 Q30 50 32 45 Q38 42 42 46 Q44 38 52 42 Q58 34 65 42 Q72 40 70 48 Q75 52 70 56 Q65 60 55 57 Q45 60 38 57 Z" fill="none" stroke="#fff" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<line x1="20" y1="55" x2="80" y2="55" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="25" y1="45" x2="75" y2="45" stroke="#fff" stroke-width="3" stroke-linecap="round" opacity="0.8"/>
<line x1="30" y1="65" x2="70" y2="65" stroke="#fff" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
<line x1="22" y1="75" x2="78" y2="75" stroke="#fff" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="35" cy="40" r="18" fill="none" stroke="#fff" stroke-width="2.5"/>
<line x1="35" y1="8" x2="35" y2="18" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="35" y1="62" x2="35" y2="72" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="3" y1="40" x2="13" y2="40" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="57" y1="40" x2="67" y2="40" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="12" y1="17" x2="19" y2="24" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="51" y1="56" x2="58" y2="63" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="12" y1="63" x2="19" y2="56" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="51" y1="17" x2="58" y2="24" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<path d="M55 55 Q65 45 75 55 Q85 58 80 68 Q75 75 60 72 Q50 70 55 55" fill="#fff" opacity="0.8"/>
<path d="M58 68 Q65 63 72 68 Q75 72 70 75 Q62 78 58 75 Q55 72 58 68" fill="none" stroke="#fff" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M25 48 Q15 40 20 32 Q25 23 35 26 Q38 15 50 20 Q60 10 72 23 Q85 22 82 35 Q88 40 82 48 Q77 55 60 52 Q50 58 40 52 Z" fill="#fff" opacity="0.85"/>
<line x1="30" y1="62" x2="24" y2="78" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="45" y1="60" x2="39" y2="76" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="60" y1="62" x2="54" y2="78" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
<line x1="52" y1="72" x2="46" y2="88" stroke="#fff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
<line x1="38" y1="70" x2="32" y2="86" stroke="#fff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 737 B

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M25 45 Q15 37 20 29 Q25 20 35 23 Q38 12 50 17 Q60 7 72 20 Q85 19 82 32 Q88 37 82 45 Q77 52 60 49 Q50 55 40 49 Z" fill="#fff" opacity="0.8"/>
<circle cx="30" cy="65" r="2.5" fill="#fff"/>
<circle cx="42" cy="62" r="2.5" fill="#fff"/>
<circle cx="54" cy="65" r="2.5" fill="#fff"/>
<circle cx="66" cy="62" r="2.5" fill="#fff"/>
<circle cx="36" cy="75" r="2" fill="#fff" opacity="0.6"/>
<circle cx="50" cy="72" r="2" fill="#fff" opacity="0.6"/>
<circle cx="62" cy="75" r="2" fill="#fff" opacity="0.6"/>
<circle cx="44" cy="82" r="1.5" fill="#fff" opacity="0.4"/>
<circle cx="56" cy="80" r="1.5" fill="#fff" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 717 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M20 42 Q10 34 15 26 Q20 17 30 20 Q33 9 45 14 Q55 4 68 17 Q82 16 79 29 Q85 34 79 42 Q74 49 56 46 Q46 52 36 46 Z" fill="#fff" opacity="0.8"/>
<polygon points="53,48 42,58 48,58 38,72 52,58 46,58" fill="#fff" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/>
<line x1="32" y1="68" x2="26" y2="84" stroke="#fff" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<line x1="68" y1="65" x2="62" y2="81" stroke="#fff" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="22" fill="none" stroke="#fff" stroke-width="3"/>
<line x1="50" y1="12" x2="50" y2="24" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="50" y1="76" x2="50" y2="88" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="12" y1="50" x2="24" y2="50" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="76" y1="50" x2="88" y2="50" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="23" y1="23" x2="32" y2="32" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="68" y1="68" x2="77" y2="77" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="23" y1="77" x2="32" y2="68" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<line x1="68" y1="23" x2="77" y2="32" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 915 B

View File

@@ -46,6 +46,29 @@ async function saveRemote(msgs: Message[]) {
} catch { /* silent */ }
}
async function setupPush() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return
try {
const reg = await navigator.serviceWorker.ready
const existing = await reg.pushManager.getSubscription()
if (existing) return // already subscribed
const res = await fetch('/push/vapid-public-key')
const { key } = await res.json()
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: key,
})
await fetch('/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sub),
})
} catch { /* silent — user may have denied */ }
}
export default function App() {
const [messages, setMessages] = useState<Message[]>(loadLocal)
const [loading, setLoading] = useState(false)
@@ -59,14 +82,14 @@ export default function App() {
setMessages(remote)
nextId.current = Math.max(...remote.map(m => m.id)) + 1
} else {
const local = loadLocal()
if (local.length > 0) {
nextId.current = Math.max(...local.map(m => m.id)) + 1
saveRemote(local)
if (messages.length > 0) {
nextId.current = Math.max(...messages.map(m => m.id)) + 1
saveRemote(messages)
}
}
setSynced(true)
})()
setupPush()
}, [])
useEffect(() => {
@@ -105,14 +128,9 @@ export default function App() {
}
}
const handleClear = () => {
setMessages([])
localStorage.removeItem(STORAGE_KEY)
}
return (
<div
className="flex flex-col h-full"
className="flex flex-col h-full relative"
style={{
backgroundImage: 'url(/1.jpg)',
backgroundSize: 'cover',
@@ -120,18 +138,6 @@ export default function App() {
backgroundAttachment: 'fixed',
}}
>
<header className="flex items-center justify-center pt-3 pb-2 flex-shrink-0 relative">
<span className="tracking-wide" style={{ color: '#999', fontSize: '4.35vw' }}></span>
{messages.length > 0 && (
<button
onClick={handleClear}
className="absolute right-4 text-xs"
style={{ color: 'rgba(255,255,255,0.25)' }}
>
</button>
)}
</header>
<ChatArea messages={messages} />
<InputArea onSend={handleSend} disabled={loading} />
</div>

View File

@@ -2,22 +2,24 @@ import { useRef, useEffect } from 'react'
import type { Message } from '../App'
export default function ChatArea({ messages }: { messages: Message[] }) {
const bottom = useRef<HTMLDivElement>(null)
const container = useRef<HTMLDivElement>(null)
useEffect(() => {
bottom.current?.scrollIntoView({ behavior: 'smooth' })
if (container.current) {
container.current.scrollTop = container.current.scrollHeight
}
}, [messages])
return (
<div className="flex-1 overflow-y-auto no-scrollbar flex justify-center">
<div className="py-4 space-y-3" style={{ width: '90vw', maxWidth: 600 }}>
<div ref={container} className="flex-1 overflow-y-auto overflow-x-hidden no-scrollbar flex justify-center">
<div className="py-4 flex flex-col" style={{ width: '90vw', maxWidth: 600, gap: 12 }}>
{messages.map((m) => (
<div
key={m.id}
className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className="max-w-[80%] rounded-2xl text-sm leading-relaxed"
className="max-w-[80%] rounded-2xl text-sm leading-relaxed break-words"
style={{
backgroundColor: m.role === 'user' ? '#ffffffa1' : 'rgba(255,255,255,0.3)',
color: '#ffffff',
@@ -32,7 +34,7 @@ export default function ChatArea({ messages }: { messages: Message[] }) {
</div>
</div>
))}
<div ref={bottom} />
<div style={{ height: 'calc(25vw + 46px)', flexShrink: 0 }} />
</div>
</div>
)

View File

@@ -12,11 +12,28 @@ export default function InputArea({ onSend, disabled }: { onSend: (text: string)
return (
<div
className="flex-shrink-0 flex justify-center"
className="absolute bottom-0 left-0 right-0 z-10 flex justify-center"
style={{ padding: 17, backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)' }}
>
<div style={{ height: '25vw', width: '90vw', maxWidth: 600 }}>
<div className="w-full h-full relative">
<button
onClick={handleSubmit}
className="absolute rounded-lg"
style={{
right: '4.11vw',
bottom: '4.11vw',
backgroundColor: 'transparent',
color: '#ffffffd1',
fontSize: '3.38vw',
padding: '2vw 4vw',
border: '1px solid rgba(255,255,255,0.3)',
}}
>
Missio
</button>
<textarea
placeholder="输入"
value={value}
@@ -29,15 +46,8 @@ export default function InputArea({ onSend, disabled }: { onSend: (text: string)
}}
disabled={disabled}
className="w-full h-full rounded-xl outline-none resize-none"
style={{ backgroundColor: '#2a2a2a', color: disabled ? '#555' : '#888', fontSize: '4.35vw', padding: '4.11vw' }}
style={{ backgroundColor: '#ffffff3e', color: disabled ? '#ffffffbb' : '#fffffff0', fontSize: '4.35vw', padding: '4.11vw' }}
/>
<button
onClick={handleSubmit}
className="absolute rounded-lg px-3 py-1"
style={{ right: '4.11vw', bottom: '4.11vw', backgroundColor: '#3a3a3a', color: '#999', fontSize: '3.38vw' }}
>
</button>
</div>
</div>
</div>

View File

@@ -1,18 +1,11 @@
@import "tailwindcss";
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #root {
height: 100%;
background: #000;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", sans-serif;
-webkit-font-smoothing: antialiased;
overflow: hidden;
user-select: none;
}
.no-scrollbar::-webkit-scrollbar {

32
PWA/src/sw.ts Normal file
View File

@@ -0,0 +1,32 @@
import { precacheAndRoute } from 'workbox-precaching'
declare let self: ServiceWorkerGlobalScope
precacheAndRoute(self.__WB_MANIFEST)
self.addEventListener('push', (event) => {
let data: { title?: string; body?: string } = {}
try { data = event.data?.json() ?? {} } catch { data = { body: event.data?.text() ?? '' } }
event.waitUntil(
self.registration.showNotification(data.title ?? '沈晏', {
body: data.body ?? '',
icon: '/icon-192.png',
badge: '/icon-192.png',
vibrate: [200, 100, 200],
tag: 'shenyan-push',
renotify: true,
})
)
})
self.addEventListener('notificationclick', (event) => {
event.notification.close()
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((list) => {
for (const client of list) {
if ('focus' in client) return client.focus()
}
return self.clients.openWindow('/')
})
)
})

View File

@@ -13,5 +13,6 @@
"noEmit": true,
"types": ["vite/client"]
},
"include": ["src"]
"include": ["src"],
"exclude": ["src/sw.ts"]
}

View File

@@ -3,10 +3,20 @@ import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
server: {
proxy: {
'/api': 'http://localhost:3001',
'/memory': 'http://localhost:3001',
'/push': 'http://localhost:3001',
},
},
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',
manifest: {
name: 'Elec',
short_name: 'Elec',