Initial upload for secondary development
This commit is contained in:
271
chatlab-web/frontend/src/pages/SettingsPage.jsx
Normal file
271
chatlab-web/frontend/src/pages/SettingsPage.jsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Copy, Check, Save } from 'lucide-react'
|
||||
|
||||
const CONFIG_ITEMS = [
|
||||
{
|
||||
group: 'chatlog 底层服务',
|
||||
items: [
|
||||
{ label: 'chatlog 地址', value: 'http://127.0.0.1:5030', desc: 'Go 后端,负责读取微信数据库' },
|
||||
{ label: 'API 前缀', value: '/api/v1', desc: '所有 chatlog 接口均在此前缀下' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'chatlog_fastAPI 业务层',
|
||||
items: [
|
||||
{ label: 'FastAPI 地址', value: 'http://127.0.0.1:8000', desc: 'Python 后端,负责 AI 分析和知识库' },
|
||||
{ label: '搜索接口', value: '/api/search', desc: '聊天记录搜索' },
|
||||
{ label: '话题接口', value: '/api/topics', desc: 'AI 话题分析管理' },
|
||||
{ label: '报告库接口', value: '/api/knowledge', desc: '售后报告管理' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: '桌面应用服务',
|
||||
items: [
|
||||
{ label: '本地应用入口', value: '/', desc: '桌面应用内置界面,由本地业务服务托管' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const AI_FIELDS = [
|
||||
{ key: 'ai_base_url', label: 'AI 接口地址', placeholder: 'https://dashscope.aliyuncs.com/compatible-mode/v1', desc: '兼容 OpenAI 格式的 API 地址' },
|
||||
{ key: 'ai_api_key', label: 'AI API Key', placeholder: 'sk-...', desc: '留空则 AI 功能不可用', type: 'password' },
|
||||
{ key: 'ai_model', label: '话题分析模型', placeholder: 'qwen-plus', desc: '用于消息分类的模型' },
|
||||
{ key: 'summary_model', label: '报告生成模型', placeholder: 'qwen-max', desc: '用于生成售后报告的模型' },
|
||||
{ key: 'vision_model', label: '视觉模型', placeholder: 'qwen-vl-plus', desc: '用于图片/视频描述' },
|
||||
{ key: 'voice_model', label: '语音模型', placeholder: 'paraformer-v2', desc: '用于语音转文字' },
|
||||
]
|
||||
|
||||
const TOPIC_PROMPT_PLACEHOLDER = '例如:本群主要是某类设备售后群,请优先按设备部件、故障现象、处理进度来拆分话题;不要按客户名或日期拆分。'
|
||||
|
||||
function CopyButton({ text }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: '2px 4px' }}
|
||||
title="复制"
|
||||
>
|
||||
{copied ? <Check size={13} color="var(--success, #10b981)" /> : <Copy size={13} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function AISettingsForm() {
|
||||
const [form, setForm] = useState({})
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [msg, setMsg] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/settings')
|
||||
.then(r => r.json())
|
||||
.then(data => setForm(data))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
setForm(prev => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setMsg('')
|
||||
try {
|
||||
const res = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(form),
|
||||
})
|
||||
if (res.ok) {
|
||||
setMsg('已保存')
|
||||
setTimeout(() => setMsg(''), 2000)
|
||||
const updated = await fetch('/api/settings').then(r => r.json())
|
||||
setForm(updated)
|
||||
} else {
|
||||
setMsg('保存失败')
|
||||
}
|
||||
} catch {
|
||||
setMsg('保存失败')
|
||||
}
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 10 }}>
|
||||
AI 模型配置
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 12 }}>
|
||||
首次使用请填入你的 API Key 和接口地址,保存后立即生效,无需重启服务。
|
||||
</div>
|
||||
|
||||
{/* 未配置 API Key 时显示橙色警告横条 */}
|
||||
{!form.ai_api_key && (
|
||||
<div style={{
|
||||
marginBottom: 12, padding: '8px 12px',
|
||||
background: 'rgba(245,158,11,0.1)', border: '1px solid rgba(245,158,11,0.3)',
|
||||
borderRadius: 8, fontSize: 12, color: '#d97706',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
}}>
|
||||
⚠️ 未配置 AI API Key,所有 AI 功能不可用。请填入您自己的 API Key 并保存。
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
|
||||
{AI_FIELDS.map((field, i) => (
|
||||
<div
|
||||
key={field.key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
borderBottom: i < AI_FIELDS.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: '0 0 130px' }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500 }}>{field.label}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{field.desc}</div>
|
||||
</div>
|
||||
<input
|
||||
type={field.type || 'text'}
|
||||
value={form[field.key] || ''}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(e) => handleChange(field.key, e.target.value)}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
padding: '7px 12px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 6,
|
||||
background: 'var(--surface-2)',
|
||||
color: 'var(--text)',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
{/* 配置状态指示点:绿色=已配置,红色=未配置 */}
|
||||
<div
|
||||
title={form[field.key] ? '已配置' : '未配置'}
|
||||
style={{
|
||||
width: 7, height: 7, borderRadius: '50%', flexShrink: 0,
|
||||
background: form[field.key] ? 'var(--success, #10b981)' : '#ef4444',
|
||||
boxShadow: form[field.key] ? '0 0 4px rgba(16,185,129,0.5)' : '0 0 4px rgba(239,68,68,0.5)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 4 }}>AI 话题分析提示词</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 8 }}>
|
||||
作为全局默认分析口径;单个群聊可在 AI 话题分析里单独覆盖。
|
||||
</div>
|
||||
<textarea
|
||||
value={form.topic_analysis_prompt || ''}
|
||||
placeholder={TOPIC_PROMPT_PLACEHOLDER}
|
||||
onChange={(e) => handleChange('topic_analysis_prompt', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: 120,
|
||||
boxSizing: 'border-box',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
padding: '10px 12px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 8,
|
||||
background: 'var(--surface-2)',
|
||||
color: 'var(--text)',
|
||||
outline: 'none',
|
||||
resize: 'vertical',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 14 }}>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 20px',
|
||||
background: 'var(--accent, #6366f1)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
cursor: saving ? 'not-allowed' : 'pointer',
|
||||
opacity: saving ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<Save size={14} />
|
||||
{saving ? '保存中...' : '保存配置'}
|
||||
</button>
|
||||
{msg && <span style={{ fontSize: 12, color: msg === '已保存' ? 'var(--success, #10b981)' : '#ef4444' }}>{msg}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '28px 36px' }}>
|
||||
<div style={{ maxWidth: 720 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 18, marginBottom: 6 }}>设置</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--text-muted)', marginBottom: 28 }}>
|
||||
系统各服务地址及 AI 配置管理。
|
||||
</div>
|
||||
|
||||
{/* AI 配置表单 */}
|
||||
<AISettingsForm />
|
||||
|
||||
{CONFIG_ITEMS.map((group) => (
|
||||
<div key={group.group} style={{ marginBottom: 28 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 10 }}>
|
||||
{group.group}
|
||||
</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
|
||||
{group.items.map((item, i) => (
|
||||
<div
|
||||
key={item.label}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
borderBottom: i < group.items.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500 }}>{item.label}</div>
|
||||
{item.desc && (
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{item.desc}</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<code style={{
|
||||
fontSize: 12,
|
||||
padding: '3px 10px',
|
||||
background: 'var(--surface-2)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 6,
|
||||
color: 'var(--accent-light, #a5b4fc)',
|
||||
}}>
|
||||
{item.value}
|
||||
</code>
|
||||
<CopyButton text={item.value} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user