diff --git a/packages/web/package.json b/packages/web/package.json index de56ae81..d2b6a50b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -27,6 +27,7 @@ "markstream-vue": "0.0.7-beta.2", "mermaid": "^11.12.2", "modern-css-reset": "^1.4.0", + "monaco-editor": "^0.52.2", "pinia": "^3.0.4", "pinia-plugin-persistedstate": "^4.7.1", "shiki": "^3.21.0", diff --git a/packages/web/src/components/file-manager/file-list.vue b/packages/web/src/components/file-manager/file-list.vue new file mode 100644 index 00000000..01c6c37f --- /dev/null +++ b/packages/web/src/components/file-manager/file-list.vue @@ -0,0 +1,146 @@ + + + + + + + {{ t('common.loading') }} + + + + + {{ t('bots.files.empty') }} + + + + + + + {{ t('bots.files.name') }} + + + {{ t('bots.files.size') }} + + + {{ t('bots.files.modified') }} + + + + + + + + + + {{ entry.name }} + + + {{ entry.isDir ? '' : formatFileSize(entry.size) }} + + + {{ formatRelativeTime(entry.modTime) }} + + + + + + + {{ t('bots.files.download') }} + + + + {{ t('bots.files.rename') }} + + + + + {{ t('bots.files.delete') }} + + + + + + diff --git a/packages/web/src/components/file-manager/file-viewer.vue b/packages/web/src/components/file-manager/file-viewer.vue new file mode 100644 index 00000000..0d7d4dfe --- /dev/null +++ b/packages/web/src/components/file-manager/file-viewer.vue @@ -0,0 +1,233 @@ + + + + + + + + + {{ filename }} + {{ formatFileSize(file.size) }} + {{ t('bots.files.unsaved') }} + + + + + {{ t('bots.files.save') }} + + + + {{ t('bots.files.download') }} + + + + + + + + + + + + {{ t('common.loading') }} + + + + + + + + + + + + {{ t('bots.files.previewNotAvailable') }} + + + + {{ t('bots.files.download') }} + + + + + diff --git a/packages/web/src/components/file-manager/index.vue b/packages/web/src/components/file-manager/index.vue new file mode 100644 index 00000000..23417040 --- /dev/null +++ b/packages/web/src/components/file-manager/index.vue @@ -0,0 +1,431 @@ + + + + + + + + + + + + + {{ seg.name }} + + + + + + + + + + {{ t('bots.files.upload') }} + + + + {{ t('bots.files.newFolder') }} + + loadDirectory(currentPath)" + > + + + + + + + + + + + + + + + + + + + + + + + {{ t('bots.files.newFolder') }} + + + + + {{ t('common.cancel') }} + + + + {{ t('common.confirm') }} + + + + + + + + + + {{ t('bots.files.rename') }} + + + + + {{ t('common.cancel') }} + + + + {{ t('common.confirm') }} + + + + + + + + + + {{ t('bots.files.confirmDelete') }} + + + {{ t('bots.files.confirmDeleteMessage', { name: deleteTarget?.name ?? '' }) }} + + + + {{ t('common.cancel') }} + + + + {{ t('bots.files.delete') }} + + + + + + diff --git a/packages/web/src/components/file-manager/utils.ts b/packages/web/src/components/file-manager/utils.ts new file mode 100644 index 00000000..b96f3881 --- /dev/null +++ b/packages/web/src/components/file-manager/utils.ts @@ -0,0 +1,171 @@ +const EXTENSION_LANGUAGE_MAP: Record = { + js: 'javascript', + mjs: 'javascript', + cjs: 'javascript', + jsx: 'javascript', + ts: 'typescript', + tsx: 'typescript', + mts: 'typescript', + cts: 'typescript', + vue: 'html', + html: 'html', + htm: 'html', + css: 'css', + scss: 'scss', + less: 'less', + json: 'json', + jsonc: 'json', + md: 'markdown', + markdown: 'markdown', + xml: 'xml', + svg: 'xml', + yaml: 'yaml', + yml: 'yaml', + toml: 'ini', + ini: 'ini', + conf: 'ini', + sh: 'shell', + bash: 'shell', + zsh: 'shell', + fish: 'shell', + py: 'python', + rb: 'ruby', + go: 'go', + rs: 'rust', + java: 'java', + kt: 'kotlin', + kts: 'kotlin', + swift: 'swift', + c: 'c', + cpp: 'cpp', + cc: 'cpp', + cxx: 'cpp', + h: 'c', + hpp: 'cpp', + cs: 'csharp', + php: 'php', + r: 'r', + R: 'r', + sql: 'sql', + graphql: 'graphql', + gql: 'graphql', + dockerfile: 'dockerfile', + makefile: 'makefile', + lua: 'lua', + perl: 'perl', + pl: 'perl', + scala: 'scala', + groovy: 'groovy', + dart: 'dart', + elixir: 'elixir', + ex: 'elixir', + exs: 'elixir', + clj: 'clojure', + bat: 'bat', + cmd: 'bat', + ps1: 'powershell', + psm1: 'powershell', + tf: 'hcl', + hcl: 'hcl', +} + +const TEXT_EXTENSIONS = new Set([ + ...Object.keys(EXTENSION_LANGUAGE_MAP), + 'txt', 'log', 'env', 'gitignore', 'gitattributes', + 'editorconfig', 'prettierrc', 'eslintrc', 'babelrc', + 'npmrc', 'nvmrc', 'dockerignore', 'lock', 'csv', 'tsv', + 'properties', 'cfg', 'cmake', +]) + +const TEXT_FILENAMES = new Set([ + 'Makefile', 'Dockerfile', 'Vagrantfile', 'Gemfile', + 'Rakefile', 'Procfile', 'LICENSE', 'CHANGELOG', + 'README', 'AUTHORS', 'CONTRIBUTORS', '.gitignore', + '.gitattributes', '.editorconfig', '.env', '.env.local', + '.env.development', '.env.production', '.prettierrc', + '.eslintrc', '.babelrc', '.npmrc', '.nvmrc', + '.dockerignore', +]) + +const IMAGE_EXTENSIONS = new Set([ + 'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'bmp', 'avif', +]) + +export function getExtension(filename: string): string { + const dotIndex = filename.lastIndexOf('.') + if (dotIndex === -1 || dotIndex === filename.length - 1) return '' + return filename.slice(dotIndex + 1).toLowerCase() +} + +export function getLanguageByFilename(filename: string): string { + const lower = filename.toLowerCase() + if (lower === 'dockerfile' || lower.startsWith('dockerfile.')) return 'dockerfile' + if (lower === 'makefile') return 'makefile' + const ext = getExtension(filename) + return EXTENSION_LANGUAGE_MAP[ext] ?? 'plaintext' +} + +export function isTextFile(filename: string): boolean { + if (TEXT_FILENAMES.has(filename)) return true + const ext = getExtension(filename) + if (!ext) return false + return TEXT_EXTENSIONS.has(ext) +} + +export function isImageFile(filename: string): boolean { + const ext = getExtension(filename) + return IMAGE_EXTENSIONS.has(ext) +} + +export function formatFileSize(bytes: number | undefined): string { + if (bytes === undefined || bytes === null) return '' + if (bytes === 0) return '0 B' + const units = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(1024)) + const size = bytes / Math.pow(1024, i) + return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}` +} + +export function formatRelativeTime(dateStr: string | undefined): string { + if (!dateStr) return '' + const date = new Date(dateStr) + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffSec = Math.floor(diffMs / 1000) + const diffMin = Math.floor(diffSec / 60) + const diffHour = Math.floor(diffMin / 60) + const diffDay = Math.floor(diffHour / 24) + + if (diffDay > 30) { + return date.toLocaleDateString() + } + if (diffDay > 0) return `${diffDay}d ago` + if (diffHour > 0) return `${diffHour}h ago` + if (diffMin > 0) return `${diffMin}m ago` + return 'just now' +} + +export function joinPath(...parts: string[]): string { + return parts + .join('/') + .replace(/\/+/g, '/') + .replace(/\/$/, '') || '/' +} + +export function parentPath(path: string): string { + if (path === '/' || path === '') return '/' + const parts = path.replace(/\/$/, '').split('/') + parts.pop() + return parts.join('/') || '/' +} + +export function pathSegments(path: string): { name: string; path: string }[] { + const parts = path.split('/').filter(Boolean) + const segments: { name: string; path: string }[] = [{ name: '/', path: '/' }] + let current = '' + for (const part of parts) { + current += '/' + part + segments.push({ name: part, path: current }) + } + return segments +} diff --git a/packages/web/src/components/monaco-editor/index.vue b/packages/web/src/components/monaco-editor/index.vue new file mode 100644 index 00000000..61bc8a75 --- /dev/null +++ b/packages/web/src/components/monaco-editor/index.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/packages/web/src/i18n/locales/en.json b/packages/web/src/i18n/locales/en.json index 03050bd7..2853a41e 100644 --- a/packages/web/src/i18n/locales/en.json +++ b/packages/web/src/i18n/locales/en.json @@ -351,8 +351,39 @@ "history": "History", "skills": "Skills", "email": "Email", + "files": "Files", "settings": "Settings" }, + "files": { + "name": "Name", + "size": "Size", + "modified": "Modified", + "empty": "This directory is empty", + "upload": "Upload", + "newFolder": "New Folder", + "download": "Download", + "rename": "Rename", + "delete": "Delete", + "save": "Save", + "unsaved": "(unsaved)", + "previewNotAvailable": "Preview not available for this file type", + "folderNamePlaceholder": "Folder name", + "newNamePlaceholder": "New name", + "confirmDelete": "Confirm Delete", + "confirmDeleteMessage": "Are you sure you want to delete \"{name}\"? This action cannot be undone.", + "loadFailed": "Failed to load directory", + "readFailed": "Failed to read file", + "saveSuccess": "File saved", + "saveFailed": "Failed to save file", + "uploadSuccess": "File uploaded", + "uploadFailed": "Failed to upload file", + "mkdirSuccess": "Folder created", + "mkdirFailed": "Failed to create folder", + "renameSuccess": "Renamed successfully", + "renameFailed": "Failed to rename", + "deleteSuccess": "Deleted successfully", + "deleteFailed": "Failed to delete" + }, "email": { "title": "Email", "subtitle": "Manage email provider bindings and view inbox / outbox.", diff --git a/packages/web/src/i18n/locales/zh.json b/packages/web/src/i18n/locales/zh.json index 53585040..9c0052c0 100644 --- a/packages/web/src/i18n/locales/zh.json +++ b/packages/web/src/i18n/locales/zh.json @@ -347,8 +347,39 @@ "history": "对话历史", "skills": "技能", "email": "邮件", + "files": "文件", "settings": "设置" }, + "files": { + "name": "名称", + "size": "大小", + "modified": "修改时间", + "empty": "此目录为空", + "upload": "上传", + "newFolder": "新建文件夹", + "download": "下载", + "rename": "重命名", + "delete": "删除", + "save": "保存", + "unsaved": "(未保存)", + "previewNotAvailable": "无法预览此文件类型", + "folderNamePlaceholder": "文件夹名称", + "newNamePlaceholder": "新名称", + "confirmDelete": "确认删除", + "confirmDeleteMessage": "确定要删除 \"{name}\" 吗?此操作不可撤销。", + "loadFailed": "加载目录失败", + "readFailed": "读取文件失败", + "saveSuccess": "文件已保存", + "saveFailed": "保存文件失败", + "uploadSuccess": "文件已上传", + "uploadFailed": "上传文件失败", + "mkdirSuccess": "文件夹已创建", + "mkdirFailed": "创建文件夹失败", + "renameSuccess": "重命名成功", + "renameFailed": "重命名失败", + "deleteSuccess": "删除成功", + "deleteFailed": "删除失败" + }, "email": { "title": "邮件", "subtitle": "管理邮件提供方绑定,查看收件箱和发件箱。", diff --git a/packages/web/src/pages/bots/components/bot-files.vue b/packages/web/src/pages/bots/components/bot-files.vue new file mode 100644 index 00000000..5325a3d2 --- /dev/null +++ b/packages/web/src/pages/bots/components/bot-files.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/web/src/pages/bots/detail.vue b/packages/web/src/pages/bots/detail.vue index a1487512..52f9a47a 100644 --- a/packages/web/src/pages/bots/detail.vue +++ b/packages/web/src/pages/bots/detail.vue @@ -127,7 +127,20 @@ - + + + + + + + +
+ {{ t('bots.files.previewNotAvailable') }} +
+ {{ t('bots.files.confirmDeleteMessage', { name: deleteTarget?.name ?? '' }) }} +