PWA对话界面重构 + 记忆库桥接
- 纯对话界面:消息气泡、深灰输入框 - 桥接服务接入 DeepSeek API - 记忆库文件纳入 mao1 仓库(含沈晏身份/对话规则) - 移除 ModelSelector、光晕效果等无关元素 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user