PWA对话界面重构 + 记忆库桥接

- 纯对话界面:消息气泡、深灰输入框
- 桥接服务接入 DeepSeek API
- 记忆库文件纳入 mao1 仓库(含沈晏身份/对话规则)
- 移除 ModelSelector、光晕效果等无关元素

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fyah
2026-05-09 13:33:12 +08:00
parent 91a8b6b7e4
commit c08424a4dc
12 changed files with 325 additions and 68 deletions

View File

@@ -1,18 +1,58 @@
import { useState } from 'react'
import ModelSelector from './components/ModelSelector'
import ChatArea from './components/ChatArea'
import InputArea from './components/InputArea'
export type Model = 'Omni' | 'Qwen3.5' | 'Deepseek'
export type Message = {
id: number
role: 'user' | 'shenyan'
text: string
}
const API = 'http://localhost:3001/api/chat'
let nextId = 0
export default function App() {
const [activeModel, setActiveModel] = useState<Model>('Deepseek')
const [messages, setMessages] = useState<Message[]>([])
const [loading, setLoading] = useState(false)
const handleSend = async (text: string) => {
const userMsg: Message = { id: nextId++, role: 'user', text }
setMessages((prev) => [...prev, userMsg])
setLoading(true)
try {
const apiMessages = [
...messages.map((m) => ({
role: m.role === 'user' ? 'user' : 'assistant',
content: m.text,
})),
{ role: 'user', content: userMsg.text },
]
const res = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'deepseek-chat', messages: apiMessages }),
})
const data = await res.json()
const reply = data.choices?.[0]?.message?.content || '(no response)'
setMessages((prev) => [...prev, { id: nextId++, role: 'shenyan', text: reply }])
} catch {
setMessages((prev) => [...prev, { id: nextId++, role: 'shenyan', text: '连接失败' }])
} finally {
setLoading(false)
}
}
return (
<div className="flex flex-col h-full bg-black">
<ModelSelector active={activeModel} onSelect={setActiveModel} />
<ChatArea />
<InputArea />
<header className="text-center pt-3 pb-2 text-sm tracking-wide flex-shrink-0" style={{ color: '#999' }}>
</header>
<ChatArea messages={messages} />
<InputArea onSend={handleSend} disabled={loading} />
</div>
)
}

View File

@@ -1,15 +1,33 @@
export default function ChatArea() {
import { useRef, useEffect } from 'react'
import type { Message } from '../App'
export default function ChatArea({ messages }: { messages: Message[] }) {
const bottom = useRef<HTMLDivElement>(null)
useEffect(() => {
bottom.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
return (
<div className="flex-1 overflow-y-auto no-scrollbar relative z-10">
<div className="glow-area px-6 py-8 min-h-full flex items-center justify-center">
<div className="w-full max-w-sm space-y-4">
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.55)' }}>
1.
</p>
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.35)' }}>
2.
</p>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar px-4">
<div className="w-full py-4 space-y-3">
{messages.map((m) => (
<div
key={m.id}
className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className="max-w-[80%] px-4 py-2.5 rounded-2xl text-sm leading-relaxed"
style={{
backgroundColor: m.role === 'user' ? '#fff' : 'rgba(255,255,255,0.3)',
color: '#888',
}}
>
{m.text}
</div>
</div>
))}
<div ref={bottom} />
</div>
</div>
)

View File

@@ -1,13 +1,39 @@
export default function InputArea() {
import { useState } from 'react'
export default function InputArea({ onSend, disabled }: { onSend: (text: string) => void; disabled?: boolean }) {
const [value, setValue] = useState('')
const handleSubmit = () => {
const trimmed = value.trim()
if (!trimmed) return
onSend(trimmed)
setValue('')
}
return (
<div className="px-6 pb-8 pt-4 relative z-10">
<div className="glow-area">
<input
type="text"
<div className="flex-shrink-0 px-4 pb-6 pt-3" style={{ height: '25vw' }}>
<div className="w-full h-full relative">
<textarea
placeholder="输入"
className="w-full bg-transparent text-sm outline-none placeholder:text-[rgba(255,255,255,0.25)]"
style={{ color: 'rgba(255,255,255,0.5)' }}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}}
disabled={disabled}
className="w-full h-full px-4 pt-2.5 pb-10 rounded-xl text-sm outline-none resize-none"
style={{ backgroundColor: '#2a2a2a', color: disabled ? '#555' : '#888' }}
/>
<button
onClick={handleSubmit}
className="absolute rounded-lg text-xs px-3 py-1"
style={{ right: 12, bottom: 12, backgroundColor: '#3a3a3a', color: '#999' }}
>
</button>
</div>
</div>
)

View File

@@ -1,31 +0,0 @@
import type { Model } from '../App'
const models: Model[] = ['Omni', 'Qwen3.5', 'Deepseek']
export default function ModelSelector({
active,
onSelect
}: {
active: Model
onSelect: (m: Model) => void
}) {
return (
<div
className="flex justify-center gap-8 pt-6 pb-4 z-10 relative"
>
{models.map((m) => (
<button
key={m}
onClick={() => onSelect(m)}
className="text-sm tracking-wide transition-colors duration-300"
style={{
color: m === active ? '#ffffff' : 'rgba(255,255,255,0.35)',
fontWeight: m === active ? 500 : 400,
}}
>
{m}
</button>
))}
</div>
)
}

View File

@@ -9,25 +9,12 @@
html, body, #root {
height: 100%;
background: #000;
color: #e5e5e5;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", sans-serif;
-webkit-font-smoothing: antialiased;
overflow: hidden;
user-select: none;
}
.glow-area {
position: relative;
}
.glow-area::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse 65% 80% at 50% 50%, rgba(255,255,255,0.035) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}