Compare commits

...

2 Commits

Author SHA1 Message Date
fyah
66f94f0fcf 合并: 清空按钮 + 背景图 + 模糊效果
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 21:12:26 +08:00
fyah
9260f4dc1e PWA: 页面背景图 + 对话气泡与输入区模糊效果
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 21:09:09 +08:00
4 changed files with 96 additions and 33 deletions

BIN
PWA/public/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

View File

@@ -8,41 +8,86 @@ export type Message = {
text: string text: string
} }
const API = 'http://localhost:3001/api/chat' const API = '/api/chat'
const STORAGE_KEY = 'shenyan-messages' const HISTORY = '/api/chat-history'
const STORAGE_KEY = 'shenyan-chat-messages'
const MAX_MSGS = 200
function loadMessages(): Message[] { function loadLocal(): Message[] {
try { try {
const raw = localStorage.getItem(STORAGE_KEY) const raw = localStorage.getItem(STORAGE_KEY)
return raw ? JSON.parse(raw) : [] return raw ? JSON.parse(raw) : []
} catch { } catch { return [] }
return []
}
} }
let nextId = (loadMessages().reduce((max, m) => Math.max(max, m.id), 0) || 0) + 1 function saveLocal(msgs: Message[]) {
const trimmed = msgs.length > MAX_MSGS ? msgs.slice(-MAX_MSGS) : msgs
localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed))
}
async function loadRemote(): Promise<Message[] | null> {
try {
const res = await fetch(HISTORY)
if (!res.ok) return null
const data = await res.json()
if (Array.isArray(data) && data.length > 0) return data
return null
} catch { return null }
}
async function saveRemote(msgs: Message[]) {
try {
const trimmed = msgs.length > MAX_MSGS ? msgs.slice(-MAX_MSGS) : msgs
await fetch(HISTORY, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(trimmed),
})
} catch { /* silent */ }
}
export default function App() { export default function App() {
const [messages, setMessages] = useState<Message[]>(loadMessages) const [messages, setMessages] = useState<Message[]>(loadLocal)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const didLoad = useRef(false) const [synced, setSynced] = useState(false)
const nextId = useRef(0)
useEffect(() => { useEffect(() => {
if (!didLoad.current) { didLoad.current = true; return } (async () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(messages)) const remote = await loadRemote()
}, [messages]) if (remote && remote.length > 0) {
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)
}
}
setSynced(true)
})()
}, [])
useEffect(() => {
if (!synced || messages.length === 0) return
saveLocal(messages)
saveRemote(messages)
}, [messages, synced])
const handleSend = async (text: string) => { const handleSend = async (text: string) => {
const userMsg: Message = { id: nextId++, role: 'user', text } const userMsg: Message = { id: nextId.current++, role: 'user', text }
const updated = [...messages, userMsg] setMessages((prev) => [...prev, userMsg])
setMessages(updated)
setLoading(true) setLoading(true)
try { try {
const apiMessages = updated.map((m) => ({ const apiMessages = [
...messages.map((m) => ({
role: m.role === 'user' ? 'user' : 'assistant', role: m.role === 'user' ? 'user' : 'assistant',
content: m.text, content: m.text,
})) })),
{ role: 'user', content: userMsg.text },
]
const res = await fetch(API, { const res = await fetch(API, {
method: 'POST', method: 'POST',
@@ -52,9 +97,9 @@ export default function App() {
const data = await res.json() const data = await res.json()
const reply = data.choices?.[0]?.message?.content || '(no response)' const reply = data.choices?.[0]?.message?.content || '(no response)'
setMessages((prev) => [...prev, { id: nextId++, role: 'shenyan', text: reply }]) setMessages((prev) => [...prev, { id: nextId.current++, role: 'shenyan', text: reply }])
} catch { } catch {
setMessages((prev) => [...prev, { id: nextId++, role: 'shenyan', text: '连接失败' }]) setMessages((prev) => [...prev, { id: nextId.current++, role: 'shenyan', text: '连接失败' }])
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -66,9 +111,17 @@ export default function App() {
} }
return ( return (
<div className="flex flex-col h-full bg-black"> <div
className="flex flex-col h-full"
style={{
backgroundImage: 'url(/1.jpg)',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundAttachment: 'fixed',
}}
>
<header className="flex items-center justify-center pt-3 pb-2 flex-shrink-0 relative"> <header className="flex items-center justify-center pt-3 pb-2 flex-shrink-0 relative">
<span className="text-sm tracking-wide" style={{ color: '#999' }}></span> <span className="tracking-wide" style={{ color: '#999', fontSize: '4.35vw' }}></span>
{messages.length > 0 && ( {messages.length > 0 && (
<button <button
onClick={handleClear} onClick={handleClear}

View File

@@ -9,18 +9,23 @@ export default function ChatArea({ messages }: { messages: Message[] }) {
}, [messages]) }, [messages])
return ( return (
<div className="flex-1 overflow-y-auto no-scrollbar px-4"> <div className="flex-1 overflow-y-auto no-scrollbar flex justify-center">
<div className="w-full py-4 space-y-3"> <div className="py-4 space-y-3" style={{ width: '90vw', maxWidth: 600 }}>
{messages.map((m) => ( {messages.map((m) => (
<div <div
key={m.id} key={m.id}
className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}
> >
<div <div
className="max-w-[80%] px-4 py-2.5 rounded-2xl text-sm leading-relaxed" className="max-w-[80%] rounded-2xl text-sm leading-relaxed"
style={{ style={{
backgroundColor: m.role === 'user' ? '#fff' : 'rgba(255,255,255,0.3)', backgroundColor: m.role === 'user' ? '#ffffffa1' : 'rgba(255,255,255,0.3)',
color: '#888', color: '#ffffff',
border: m.role === 'shenyan' ? '1px solid rgba(255,255,255,0.4)' : 'none',
padding: 12,
fontSize: '4.5vw',
backdropFilter: 'blur(8px)',
WebkitBackdropFilter: 'blur(8px)',
}} }}
> >
{m.text} {m.text}

View File

@@ -11,7 +11,11 @@ export default function InputArea({ onSend, disabled }: { onSend: (text: string)
} }
return ( return (
<div className="flex-shrink-0 px-4 pb-6 pt-3" style={{ height: '25vw' }}> <div
className="flex-shrink-0 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"> <div className="w-full h-full relative">
<textarea <textarea
placeholder="输入" placeholder="输入"
@@ -24,17 +28,18 @@ export default function InputArea({ onSend, disabled }: { onSend: (text: string)
} }
}} }}
disabled={disabled} disabled={disabled}
className="w-full h-full px-4 pt-2.5 pb-10 rounded-xl text-sm outline-none resize-none" className="w-full h-full rounded-xl outline-none resize-none"
style={{ backgroundColor: '#2a2a2a', color: disabled ? '#555' : '#888' }} style={{ backgroundColor: '#2a2a2a', color: disabled ? '#555' : '#888', fontSize: '4.35vw', padding: '4.11vw' }}
/> />
<button <button
onClick={handleSubmit} onClick={handleSubmit}
className="absolute rounded-lg text-xs px-3 py-1" className="absolute rounded-lg px-3 py-1"
style={{ right: 12, bottom: 12, backgroundColor: '#3a3a3a', color: '#999' }} style={{ right: '4.11vw', bottom: '4.11vw', backgroundColor: '#3a3a3a', color: '#999', fontSize: '3.38vw' }}
> >
</button> </button>
</div> </div>
</div> </div>
</div>
) )
} }