Why: Keys must be added without writing masks back, and a turn needs copy, exclude, and regenerate without deleting history. Evidence: npx tsc -b. Session notes and candidate switches call the new conversation and message routes. Impact: Settings gains key, lore, and slash-template panels. Excluded messages stay visible and are dimmed. Refs: docs/records/2026-10-04-sillytavern-review.md
105 lines
5.3 KiB
TypeScript
105 lines
5.3 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import { Btn, SettingRow, SettingsGroup } from '@ui'
|
||
import { api } from '../api'
|
||
|
||
const services = [
|
||
['models.chat', '主模型'],
|
||
['models.lightweight', '轻量模型'],
|
||
['models.embedding', 'Embedding'],
|
||
['models.rerank', 'Reranker'],
|
||
['tools.tavily', 'Tavily'],
|
||
] as const
|
||
|
||
type Secret = { id: string; service: string; label: string; active: boolean; mask: string }
|
||
type Entry = { id: string; keys: string[]; content: string; constant: boolean; logic: string }
|
||
type Cmd = { id: string; name: string; title: string; template: string }
|
||
|
||
export function BorrowPanels() {
|
||
const [secrets, setSecrets] = useState<Secret[]>([])
|
||
const [entries, setEntries] = useState<Entry[]>([])
|
||
const [cmds, setCmds] = useState<Cmd[]>([])
|
||
const [service, setService] = useState<string>(services[0][0])
|
||
const [label, setLabel] = useState('')
|
||
const [value, setValue] = useState('')
|
||
const [keys, setKeys] = useState('')
|
||
const [content, setContent] = useState('')
|
||
const [constant, setConstant] = useState(false)
|
||
const [cmdName, setCmdName] = useState('/summary')
|
||
const [cmdTitle, setCmdTitle] = useState('总结')
|
||
const [cmdBody, setCmdBody] = useState('请总结以上内容。\n{{input}}')
|
||
|
||
const reload = () => {
|
||
api.listSecrets().then(setSecrets).catch(() => undefined)
|
||
api.listContextEntries().then(setEntries).catch(() => undefined)
|
||
api.listPromptCommands().then(setCmds).catch(() => undefined)
|
||
}
|
||
useEffect(() => { reload() }, [])
|
||
|
||
return (
|
||
<>
|
||
<SettingsGroup title="密钥">
|
||
<SettingRow label="新增密钥" desc="每个服务同时只有一把生效。列表只显示掩码。">
|
||
<select value={service} onChange={e => setService(e.target.value)}>
|
||
{services.map(([id, name]) => <option key={id} value={id}>{name}</option>)}
|
||
</select>
|
||
</SettingRow>
|
||
<SettingRow label="标签与密钥" desc="保存后立即生效,并停用同服务的其他密钥。">
|
||
<input className="search-input" value={label} placeholder="标签" onChange={e => setLabel(e.target.value)} />
|
||
<input className="search-input" type="password" autoComplete="new-password" value={value} placeholder="密钥" onChange={e => setValue(e.target.value)} />
|
||
<Btn variant="primary" onClick={async () => {
|
||
if (!value.trim()) return
|
||
await api.createSecret(service, label || '未命名', value.trim())
|
||
setValue(''); setLabel(''); reload()
|
||
}}>添加</Btn>
|
||
</SettingRow>
|
||
{secrets.map(s => (
|
||
<SettingRow key={s.id} label={s.label} desc={`${s.service} · ${s.mask}${s.active ? ' · 生效' : ''}`}>
|
||
{!s.active && <Btn onClick={async () => { await api.activateSecret(s.id); reload() }}>启用</Btn>}
|
||
<Btn onClick={async () => { await api.deleteSecret(s.id); reload() }}>删除</Btn>
|
||
</SettingRow>
|
||
))}
|
||
</SettingsGroup>
|
||
<SettingsGroup title="条件注入">
|
||
<SettingRow label="关键词" desc="逗号分隔。用 /正则/ 表示正则。勾选后变为常驻。">
|
||
<input className="search-input" value={keys} placeholder="关键词" onChange={e => setKeys(e.target.value)} />
|
||
<label><input type="checkbox" checked={constant} onChange={e => setConstant(e.target.checked)} /> 常驻</label>
|
||
</SettingRow>
|
||
<SettingRow label="注入正文" desc="命中后附在上下文末尾,不改系统前缀。">
|
||
<textarea rows={3} value={content} onChange={e => setContent(e.target.value)} />
|
||
<Btn variant="primary" onClick={async () => {
|
||
if (!content.trim()) return
|
||
await api.saveContextEntry({
|
||
keys: keys.split(/[,,]/).map(s => s.trim()).filter(Boolean),
|
||
content: content.trim(), constant, logic: 'and', enabled: true, scanDepth: 4, budgetChars: 2000,
|
||
})
|
||
setContent(''); setKeys(''); reload()
|
||
}}>添加</Btn>
|
||
</SettingRow>
|
||
{entries.map(e => (
|
||
<SettingRow key={e.id} label={e.constant ? '常驻' : (e.keys.join(' / ') || '未命名')} desc={e.content}>
|
||
<Btn onClick={async () => { await api.deleteContextEntry(e.id); reload() }}>删除</Btn>
|
||
</SettingRow>
|
||
))}
|
||
</SettingsGroup>
|
||
<SettingsGroup title="用户命令">
|
||
<SettingRow label="命令" desc="发送 /名称 时展开成提示词。不会自动触发。{{input}} 换成命令后面的文字。">
|
||
<input className="search-input" value={cmdName} onChange={e => setCmdName(e.target.value)} />
|
||
<input className="search-input" value={cmdTitle} onChange={e => setCmdTitle(e.target.value)} />
|
||
</SettingRow>
|
||
<SettingRow label="模板" desc="名称需为小写字母、数字或连字符,例如 /summary。">
|
||
<textarea rows={3} value={cmdBody} onChange={e => setCmdBody(e.target.value)} />
|
||
<Btn variant="primary" onClick={async () => {
|
||
await api.savePromptCommand({ name: cmdName, title: cmdTitle, template: cmdBody })
|
||
reload()
|
||
}}>添加</Btn>
|
||
</SettingRow>
|
||
{cmds.map(c => (
|
||
<SettingRow key={c.id} label={c.name} desc={c.title}>
|
||
<Btn onClick={async () => { await api.deletePromptCommand(c.id); reload() }}>删除</Btn>
|
||
</SettingRow>
|
||
))}
|
||
</SettingsGroup>
|
||
</>
|
||
)
|
||
}
|