// MainView.jsx — 单行顶栏(Logo + 工具行)+ 多引擎墨吞 + 真实导入 const { useState: useStateMV, useRef: useRefMV, useEffect: useEffectMV } = React; // —— 点击外部关闭 —— // function useOutside(ref, onClose) { useEffectMV(() => { function h(e) { if (ref.current && !ref.current.contains(e.target)) onClose(); } document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, [onClose]); } // ============ 下拉菜单包装 ============ function ToolMenu({ icon, label, children, align }) { const [open, setOpen] = useStateMV(false); const ref = useRefMV(null); useOutside(ref, () => setOpen(false)); return (
{open && (
setOpen(false)}> {children}
)}
); } function DDItem({ icon, children, k, onClick }) { return (
{icon && } {children} {k && {k}}
); } // ============ 墨吞 · 接入 AI 引擎(多引擎 · 可扩展)============ function MotunSettings() { const [open, setOpen] = useStateMV(false); const ref = useRefMV(null); useOutside(ref, () => setOpen(false)); const [providers, setProviders] = useStateMV(() => getProviders()); const [active, setActive] = useStateMV(() => getActiveId()); const cur = providers.find(p => p.id === active) || providers[0]; const [apiKey, setApiKey] = useStateMV(() => getKey(active)); const [model, setModelS] = useStateMV(() => getModel(active)); const [base, setBaseS] = useStateMV(() => getBase(active)); const [proxy, setProxyS] = useStateMV(() => getProxy()); const [show, setShow] = useStateMV(false); const [status, setStatus] = useStateMV('idle'); // idle|saved|testing|ok|err const [msg, setMsg] = useStateMV(''); const [adding, setAdding] = useStateMV(false); const [nf, setNf] = useStateMV({ name: '', base: '', model: '' }); function pickProvider(id) { setActiveId(id); setActive(id); setApiKey(getKey(id)); setModelS(getModel(id)); setBaseS(getBase(id)); setStatus('idle'); setMsg(''); } function persist() { setKey(active, apiKey.trim()); setBase(active, base.trim() === cur.base ? '' : base.trim()); setModel(active, model); setProxy(proxy.trim()); setProviders(getProviders()); } function save() { persist(); setStatus('saved'); setMsg('已保存在本机'); } async function test() { persist(); setStatus('testing'); setMsg('正在连接 ' + cur.name + '…'); try { const r = await aiPing(); setStatus('ok'); setMsg('连接成功 · 「' + r + '」'); } catch (e) { setStatus('err'); setMsg(e.message || '连接失败'); } } function createCustom() { if (!nf.name.trim() || !nf.base.trim()) return; const id = addCustomProvider({ name: nf.name.trim(), base: nf.base.trim(), models: nf.model.trim() ? [nf.model.trim()] : ['default'] }); setProviders(getProviders()); setAdding(false); setNf({ name: '', base: '', model: '' }); pickProvider(id); } function deleteCustom(id, e) { e.stopPropagation(); removeCustomProvider(id); const left = getProviders(); setProviders(left); if (active === id) pickProvider('deepseek'); } const connected = hasActiveKey(); return (
{open && (
e.stopPropagation()}>
墨吞 · 接入 AI 引擎
选一个引擎、填入你自己的密钥。可同时保存多家、随时切换。密钥仅存本机。
引擎
{providers.map(p => ( ))} {adding ? (
setNf({ ...nf, name: e.target.value })} /> setNf({ ...nf, base: e.target.value })} /> setNf({ ...nf, model: e.target.value })} />
) : ( )}
{cur.name} · API Key
{ setApiKey(e.target.value); setStatus('idle'); }} />
进阶 · 接口地址与模型
接口地址
setBaseS(e.target.value)} />
模型
{cur.models.map(m => ( ))}
代理地址 · 可选
setProxyS(e.target.value)} />
浏览器直连 Claude / Gemini 常被跨域拦截。部署一个代理后填这里,全部引擎即可直连。
{msg &&
{status === 'ok' && } {status === 'err' && } {status === 'testing' && } {msg}
}
)}
); } // ============ 顶栏(单行:Logo + 工具行)============ function TopBar({ notebooks, onImport, onOpenContacts, onOpenSkills, onOpenShare, onOpenInbox, inboxCount, onOpenPlans }) { const fileRef = useRefMV(null); function pick(kind) { const inp = fileRef.current; if (!inp) return; inp.accept = (kind === 'image' || kind === 'camera') ? 'image/*' : kind === 'word' ? '.doc,.docx' : kind === 'pdf' ? '.pdf,application/pdf' : '.txt,.md,.markdown,text/plain'; if (kind === 'camera') inp.setAttribute('capture', 'environment'); else inp.removeAttribute('capture'); inp.click(); } function onFile(e) { const f = e.target.files && e.target.files[0]; e.target.value = ''; if (f) onImport(f); } return (
按本子
{notebooks.map(nb => {nb.name})}
自定义筛选
从文件导入到书写台
pick('text')}>纯文本 / Markdown pick('word')}>Word 文档 pick('pdf')}>PDF 文档
pick('image')}>照片导入 pick('camera')}>拍照 · 识别
{typeof InboxButton === 'function' && }
); } Object.assign(window, { TopBar, MotunSettings, useOutside });