// 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 ? (
) : (
)}
{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 });