/**
* 记忆文件管理页面
*/
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showModal } from '../components/modal.js'
import { t } from '../lib/i18n.js'
function CATEGORIES() {
return [
{ key: 'memory', label: t('memory.catMemory'), desc: t('memory.catMemoryDesc') },
{ key: 'archive', label: t('memory.catArchive'), desc: t('memory.catArchiveDesc') },
{ key: 'core', label: t('memory.catCore'), desc: t('memory.catCoreDesc') },
]
}
export async function render() {
const page = document.createElement('div')
page.className = 'page'
page.innerHTML = `
${CATEGORIES().map((c, i) => `
${c.label}
`).join('')}
${CATEGORIES()[0].desc}
`
const state = { category: 'memory', currentPath: null, agentId: 'main' }
// 先用默认选项填充下拉框,立即显示页面
const agentSelect = page.querySelector('#agent-select')
agentSelect.innerHTML = ''
// 异步加载 agent 列表并更新下拉框
api.listAgents().then(agents => {
if (!agentSelect) return
const options = agents.map(a => {
const label = a.identityName ? a.identityName.split(',')[0].trim() : a.id
return ``
}).join('')
agentSelect.innerHTML = options
}).catch(() => {})
// Agent 切换
page.querySelector('#agent-select').onchange = (e) => {
state.agentId = e.target.value
state.currentPath = null
resetEditor(page)
// 显示加载动画
const tree = page.querySelector('#file-tree')
tree.innerHTML = ''
loadFiles(page, state)
}
// Tab 切换
page.querySelectorAll('.tab').forEach(tab => {
tab.onclick = () => {
page.querySelectorAll('.tab').forEach(t => t.classList.remove('active'))
tab.classList.add('active')
state.category = tab.dataset.tab
state.currentPath = null
const cat = CATEGORIES().find(c => c.key === state.category)
page.querySelector('#category-desc').textContent = cat?.desc || ''
resetEditor(page)
// 显示加载动画
const tree = page.querySelector('#file-tree')
tree.innerHTML = ''
loadFiles(page, state)
}
})
// 保存
page.querySelector('#btn-save-file').onclick = () => saveFile(page, state)
// 预览(简易 Markdown 渲染)
page.querySelector('#btn-preview').onclick = () => togglePreview(page, state)
// 新建文件
page.querySelector('#btn-new-file').onclick = () => {
showModal({
title: t('memory.newFileTitle'),
fields: [{ name: 'filename', label: t('memory.newFileLabel'), placeholder: t('memory.newFilePlaceholder'), hint: t('memory.newFileHint') }],
onConfirm: async ({ filename }) => {
if (!filename) return
try {
await api.writeMemoryFile(filename, `# ${filename}\n\n`, state.category, state.agentId)
toast(t('memory.created', { name: filename }), 'success')
loadFiles(page, state)
} catch (e) {
toast(t('memory.createFailed') + ': ' + e, 'error')
}
},
})
}
// 删除文件
page.querySelector('#btn-del-file').onclick = async () => {
if (!state.currentPath) return
const name = state.currentPath.split('/').pop()
const { showConfirm } = await import('../components/modal.js')
const yes = await showConfirm(t('memory.confirmDelete', { name }))
if (!yes) return
try {
await api.deleteMemoryFile(state.currentPath, state.agentId)
toast(t('memory.deleted', { name }), 'success')
state.currentPath = null
resetEditor(page)
loadFiles(page, state)
} catch (e) {
toast(t('memory.deleteFailed') + ': ' + e, 'error')
}
}
// 单个下载
page.querySelector('#btn-download').onclick = () => downloadCurrentFile(page, state)
// 打包下载
page.querySelector('#btn-export-zip').onclick = () => exportZip(state)
loadFiles(page, state)
return page
}
async function loadFiles(page, state) {
const tree = page.querySelector('#file-tree')
try {
const files = await api.listMemoryFiles(state.category, state.agentId)
if (!files || !files.length) {
tree.innerHTML = `${t('memory.noFiles')}
`
return
}
renderFileTree(page, state, files)
} catch (e) {
tree.innerHTML = `${t('memory.loadFailed')}: ${e}
`
toast(t('memory.loadListFailed') + ': ' + e, 'error')
}
}
function renderFileTree(page, state, files) {
const tree = page.querySelector('#file-tree')
tree.innerHTML = files.map(f => {
const name = f.split('/').pop()
const active = state.currentPath === f ? ' active' : ''
return `${name}
`
}).join('')
tree.querySelectorAll('.file-item').forEach(item => {
item.onclick = () => {
state.currentPath = item.dataset.path
tree.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'))
item.classList.add('active')
loadFileContent(page, state)
}
})
}
async function loadFileContent(page, state) {
const editor = page.querySelector('#file-editor')
const label = page.querySelector('#current-file')
const btnSave = page.querySelector('#btn-save-file')
const btnPreview = page.querySelector('#btn-preview')
const btnDel = page.querySelector('#btn-del-file')
const btnDl = page.querySelector('#btn-download')
editor.disabled = true
editor.value = t('memory.loading')
label.textContent = state.currentPath
// 退出预览模式
editor.style.display = ''
const previewEl = page.querySelector('#md-preview')
if (previewEl) previewEl.remove()
btnPreview.textContent = t('memory.preview')
try {
const content = await api.readMemoryFile(state.currentPath, state.agentId)
editor.value = content || ''
editor.disabled = false
btnSave.disabled = false
btnPreview.disabled = false
btnDel.disabled = false
btnDl.disabled = false
} catch (e) {
editor.value = t('memory.readFailed') + ': ' + e
toast(t('memory.readFileFailed') + ': ' + e, 'error')
}
}
function resetEditor(page) {
const editor = page.querySelector('#file-editor')
editor.value = ''
editor.disabled = true
editor.style.display = ''
const previewEl = page.querySelector('#md-preview')
if (previewEl) previewEl.remove()
page.querySelector('#current-file').textContent = t('memory.selectFile')
page.querySelector('#btn-save-file').disabled = true
page.querySelector('#btn-preview').disabled = true
page.querySelector('#btn-preview').textContent = t('memory.preview')
page.querySelector('#btn-del-file').disabled = true
page.querySelector('#btn-download').disabled = true
}
async function saveFile(page, state) {
if (!state.currentPath) return
const content = page.querySelector('#file-editor').value
try {
await api.writeMemoryFile(state.currentPath, content, state.category, state.agentId)
toast(t('memory.fileSaved'), 'success')
} catch (e) {
toast(t('memory.saveFailed') + ': ' + e, 'error')
}
}
function togglePreview(page) {
const editor = page.querySelector('#file-editor')
const btn = page.querySelector('#btn-preview')
let previewEl = page.querySelector('#md-preview')
if (previewEl) {
// 退出预览
previewEl.remove()
editor.style.display = ''
btn.textContent = t('memory.preview')
} else {
// 进入预览
const md = editor.value
previewEl = document.createElement('div')
previewEl.id = 'md-preview'
previewEl.style.cssText = 'flex:1;padding:var(--space-lg);overflow-y:auto;line-height:1.8;color:var(--text-primary)'
previewEl.innerHTML = renderMarkdown(md)
editor.style.display = 'none'
editor.parentElement.appendChild(previewEl)
btn.textContent = t('memory.edit')
}
}
// 简易 Markdown 渲染
function renderMarkdown(md) {
return md
.replace(/&/g, '&').replace(//g, '>')
.replace(/^### (.+)$/gm, '$1
')
.replace(/^## (.+)$/gm, '$1
')
.replace(/^# (.+)$/gm, '$1
')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/`(.+?)`/g, '$1')
.replace(/^- (.+)$/gm, '$1')
.replace(/\n\n/g, '
')
.replace(/\n/g, '
')
}
// ===== 下载功能 =====
function triggerDownload(filename, content) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
async function downloadCurrentFile(page, state) {
if (!state.currentPath) return
try {
const content = page.querySelector('#file-editor').value
const filename = state.currentPath.split('/').pop()
triggerDownload(filename, content)
toast(t('memory.downloaded', { name: filename }), 'success')
} catch (e) {
toast(t('memory.downloadFailed') + ': ' + e, 'error')
}
}
async function exportZip(state) {
try {
const zipPath = await api.exportMemoryZip(state.category, state.agentId)
const label = CATEGORIES().find(c => c.key === state.category)?.label || state.category
// 尝试用 Tauri shell open 打开文件所在目录
try {
const { open } = await import('@tauri-apps/plugin-shell')
const dir = zipPath.substring(0, zipPath.lastIndexOf('/')) || zipPath
await open(dir)
toast(t('memory.exported', { label, path: zipPath }), 'success')
} catch {
// fallback:仅显示路径
toast(t('memory.exported', { label, path: zipPath }), 'success')
}
} catch (e) {
toast(t('memory.exportFailed') + ': ' + e, 'error')
}
}