@@ -545,7 +564,22 @@ async function previewTextFile(path: string, fileName: string): Promise
{
transition: opacity 0.15s ease;
}
- &:hover .att-download-icon {
+ .att-download-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ color: inherit;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ }
+
+ &:hover .att-download-icon,
+ .att-download-btn:hover .att-download-icon {
opacity: 1;
}
}
@@ -654,6 +688,40 @@ async function previewTextFile(path: string, fileName: string): Promise {
white-space: pre-wrap;
word-break: break-all;
color: $text-primary;
- background: $code-bg;
+}
+
+.text-preview-markdown {
+ padding: 16px;
+ overflow: auto;
+}
+
+.markdown-text-preview-drawer {
+ max-width: 100vw;
+
+ .n-drawer-content,
+ .n-drawer-body-content-wrapper {
+ max-width: 100vw;
+ }
+}
+
+@media (max-width: $breakpoint-mobile) {
+ .markdown-text-preview-drawer {
+ max-width: 100vw;
+
+ .n-drawer-content,
+ .n-drawer-body-content-wrapper {
+ max-width: 100vw;
+ }
+ }
+
+ .text-preview-body {
+ padding: 12px;
+ max-width: 100vw;
+ }
+
+ .text-preview-markdown {
+ padding: 12px;
+ max-width: 100vw;
+ }
}
diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts
index 31e61a9..abfb770 100644
--- a/packages/client/src/i18n/locales/de.ts
+++ b/packages/client/src/i18n/locales/de.ts
@@ -1161,6 +1161,7 @@ jobTriggered: 'Job ausgelost',
backendTimeout: 'Zeituberschreitung beim Lesen der Datei',
unsupportedBackend: 'Aktuelles Terminal-Backend unterstutzt keine Datei-Downloads',
invalidPath: 'Ungultiger Dateipfad',
+ contentDisplay: 'Inhalt anzeigen',
download: 'Herunterladen',
downloadFile: 'Datei herunterladen',
},
diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts
index 99dedc6..85df13a 100644
--- a/packages/client/src/i18n/locales/en.ts
+++ b/packages/client/src/i18n/locales/en.ts
@@ -1196,6 +1196,7 @@ export default {
backendTimeout: 'File read timed out',
unsupportedBackend: 'Current terminal backend does not support file download',
invalidPath: 'Invalid file path',
+ contentDisplay: 'Content display',
download: 'Download',
downloadFile: 'Download file',
},
diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts
index 3720957..679f4ab 100644
--- a/packages/client/src/i18n/locales/es.ts
+++ b/packages/client/src/i18n/locales/es.ts
@@ -1161,6 +1161,7 @@ jobTriggered: 'Job ejecutado',
backendTimeout: 'Tiempo de lectura del archivo agotado',
unsupportedBackend: 'El backend del terminal actual no admite la descarga de archivos',
invalidPath: 'Ruta de archivo invalida',
+ contentDisplay: 'Contenido',
download: 'Descargar',
downloadFile: 'Descargar archivo',
},
diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts
index aa5bcb4..dc300f7 100644
--- a/packages/client/src/i18n/locales/fr.ts
+++ b/packages/client/src/i18n/locales/fr.ts
@@ -1161,6 +1161,7 @@ jobTriggered: 'Job declenche',
backendTimeout: 'Delai de lecture du fichier depasse',
unsupportedBackend: 'Le backend de terminal actuel ne prend pas en charge le telechargement de fichiers',
invalidPath: 'Chemin de fichier invalide',
+ contentDisplay: 'Affichage du contenu',
download: 'Telecharger',
downloadFile: 'Telecharger le fichier',
},
diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts
index 709fc0f..fd7da5c 100644
--- a/packages/client/src/i18n/locales/ja.ts
+++ b/packages/client/src/i18n/locales/ja.ts
@@ -1161,6 +1161,7 @@ export default {
backendTimeout: 'ファイルの読み取りがタイムアウトしました',
unsupportedBackend: '現在のターミナルバックエンドはファイルのダウンロードに対応していません',
invalidPath: '無効なファイルパス',
+ contentDisplay: '内容表示',
download: 'ダウンロード',
downloadFile: 'ファイルをダウンロード',
},
diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts
index 59ccc47..c349ba8 100644
--- a/packages/client/src/i18n/locales/ko.ts
+++ b/packages/client/src/i18n/locales/ko.ts
@@ -1161,6 +1161,7 @@ export default {
backendTimeout: '파일 읽기 시간 초과',
unsupportedBackend: '현재 터미널 백엔드는 파일 다운로드를 지원하지 않습니다',
invalidPath: '잘못된 파일 경로',
+ contentDisplay: '내용 표시',
download: '다운로드',
downloadFile: '파일 다운로드',
},
diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts
index 0009e4f..d133fec 100644
--- a/packages/client/src/i18n/locales/pt.ts
+++ b/packages/client/src/i18n/locales/pt.ts
@@ -1161,6 +1161,7 @@ jobTriggered: 'Job acionado',
backendTimeout: 'Tempo esgotado para ler o arquivo',
unsupportedBackend: 'O backend de terminal atual nao suporta download de arquivos',
invalidPath: 'Caminho de arquivo invalido',
+ contentDisplay: 'Exibicao de conteudo',
download: 'Baixar',
downloadFile: 'Baixar arquivo',
},
diff --git a/packages/client/src/i18n/locales/zh-TW.ts b/packages/client/src/i18n/locales/zh-TW.ts
index d943688..4da2417 100644
--- a/packages/client/src/i18n/locales/zh-TW.ts
+++ b/packages/client/src/i18n/locales/zh-TW.ts
@@ -1201,6 +1201,7 @@ export default {
backendTimeout: '檔案讀取逾時',
unsupportedBackend: '目前 terminal backend 暫不支援檔案下載',
invalidPath: '無效的檔案路徑',
+ contentDisplay: '內容展示',
download: '下載',
downloadFile: '下載檔案',
},
diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts
index f9400e2..00e51d9 100644
--- a/packages/client/src/i18n/locales/zh.ts
+++ b/packages/client/src/i18n/locales/zh.ts
@@ -1198,6 +1198,7 @@ export default {
backendTimeout: '文件读取超时',
unsupportedBackend: '当前 terminal backend 暂不支持文件下载',
invalidPath: '无效的文件路径',
+ contentDisplay: '内容展示',
download: '下载',
downloadFile: '下载文件',
},
diff --git a/tests/client/markdown-rendering-mermaid-import-timeout.test.ts b/tests/client/markdown-rendering-mermaid-import-timeout.test.ts
index 026f531..23a4347 100644
--- a/tests/client/markdown-rendering-mermaid-import-timeout.test.ts
+++ b/tests/client/markdown-rendering-mermaid-import-timeout.test.ts
@@ -12,6 +12,16 @@ vi.mock('vue-i18n', () => ({
}))
vi.mock('naive-ui', () => ({
+ NDrawer: {
+ props: ['show'],
+ template: '
',
+ },
+ NDrawerContent: {
+ template: '',
+ },
+ NSpin: {
+ template: '
',
+ },
useMessage: () => ({
error: vi.fn(),
success: vi.fn(),
diff --git a/tests/client/markdown-rendering.test.ts b/tests/client/markdown-rendering.test.ts
index b00deb2..224a04e 100644
--- a/tests/client/markdown-rendering.test.ts
+++ b/tests/client/markdown-rendering.test.ts
@@ -10,6 +10,12 @@ const mermaidMock = vi.hoisted(() => ({
})),
}))
+const downloadApiMock = vi.hoisted(() => ({
+ downloadFile: vi.fn(() => Promise.resolve()),
+ fetchFileText: vi.fn(() => Promise.resolve('preview content')),
+ getDownloadUrl: vi.fn((path: string) => `http://test.local/api/hermes/download?path=${encodeURIComponent(path)}`),
+}))
+
vi.mock('mermaid', () => ({
default: mermaidMock,
}))
@@ -28,6 +34,22 @@ vi.mock('vue-i18n', () => ({
}))
vi.mock('naive-ui', () => ({
+ NDrawer: {
+ props: ['show', 'width'],
+ template: '
',
+ },
+ NDrawerContent: {
+ props: {
+ title: { type: String, default: '' },
+ closable: { type: Boolean, default: false },
+ bodyContentStyle: { type: [Object, String], default: undefined },
+ },
+ template: '',
+ },
+ NSpin: {
+ props: ['show'],
+ template: '
',
+ },
useMessage: () => ({
error: vi.fn(),
success: vi.fn(),
@@ -37,8 +59,9 @@ vi.mock('naive-ui', () => ({
}))
vi.mock('@/api/hermes/download', () => ({
- downloadFile: vi.fn(),
- getDownloadUrl: (path: string) => `http://test.local/api/hermes/download?path=${encodeURIComponent(path)}`,
+ downloadFile: downloadApiMock.downloadFile,
+ fetchFileText: downloadApiMock.fetchFileText,
+ getDownloadUrl: downloadApiMock.getDownloadUrl,
}))
import MarkdownRenderer from '@/components/hermes/chat/MarkdownRenderer.vue'
@@ -51,9 +74,15 @@ describe('MarkdownRenderer', () => {
beforeEach(() => {
mermaidMock.initialize.mockClear()
mermaidMock.render.mockClear()
+ downloadApiMock.downloadFile.mockClear()
+ downloadApiMock.fetchFileText.mockClear()
+ downloadApiMock.getDownloadUrl.mockClear()
mermaidMock.render.mockImplementation(async (id: string, source: string) => ({
svg: ``,
}))
+ downloadApiMock.downloadFile.mockResolvedValue(undefined)
+ downloadApiMock.fetchFileText.mockResolvedValue('preview content')
+ downloadApiMock.getDownloadUrl.mockImplementation((path: string) => `http://test.local/api/hermes/download?path=${encodeURIComponent(path)}`)
Object.defineProperty(window, 'isSecureContext', {
configurable: true,
@@ -253,6 +282,69 @@ describe('MarkdownRenderer', () => {
expect(img.attributes('alt')).toBe('桌面截图')
})
+ it('downloads local text files when the file card download icon is clicked', async () => {
+ const wrapper = mount(MarkdownRenderer, {
+ props: {
+ content: '[notes.txt](/tmp/notes.txt)',
+ },
+ })
+
+ expect(wrapper.find('.markdown-file-card').exists()).toBe(true)
+ expect(wrapper.find('.att-download-btn .att-download-icon').exists()).toBe(true)
+
+ await wrapper.find('.att-download-btn').trigger('click')
+ await Promise.resolve()
+
+ expect(downloadApiMock.downloadFile).toHaveBeenCalledTimes(1)
+ expect(downloadApiMock.downloadFile).toHaveBeenCalledWith('/tmp/notes.txt', 'notes.txt')
+ expect(downloadApiMock.fetchFileText).not.toHaveBeenCalled()
+ expect(wrapper.find('.n-drawer-stub').exists()).toBe(false)
+ })
+
+ it('opens text previews in a responsive drawer with a close control', async () => {
+ const wrapper = mount(MarkdownRenderer, {
+ props: {
+ content: '[notes.txt](/tmp/notes.txt)',
+ },
+ })
+
+ await wrapper.find('.markdown-file-card').trigger('click')
+ await Promise.resolve()
+ await nextTick()
+
+ const drawer = wrapper.find('.n-drawer-stub')
+ expect(drawer.exists()).toBe(true)
+ expect(drawer.attributes('data-width')).toBe('min(800px, 100vw)')
+ expect(drawer.find('.n-drawer-content-stub').attributes('data-body-padding')).toBe('0')
+ expect(drawer.text()).toContain('download.contentDisplay')
+ expect(downloadApiMock.fetchFileText).toHaveBeenCalledWith('/tmp/notes.txt', 'notes.txt')
+
+ await drawer.find('.n-drawer-close-stub').trigger('click')
+ await nextTick()
+
+ expect(wrapper.find('.n-drawer-stub').exists()).toBe(false)
+ })
+
+ it('renders markdown file previews as markdown content', async () => {
+ downloadApiMock.fetchFileText.mockResolvedValue('# Preview Title\n\n**bold text**')
+ const wrapper = mount(MarkdownRenderer, {
+ props: {
+ content: '[notes.md](/tmp/notes.md)',
+ },
+ })
+
+ await wrapper.find('.markdown-file-card').trigger('click')
+ await Promise.resolve()
+ await nextTick()
+
+ const drawer = wrapper.find('.n-drawer-stub')
+ expect(drawer.exists()).toBe(true)
+ expect(drawer.find('.text-preview-markdown').exists()).toBe(true)
+ expect(drawer.find('.text-preview-body').exists()).toBe(false)
+ expect(drawer.find('.text-preview-markdown h1').text()).toBe('Preview Title')
+ expect(drawer.find('.text-preview-markdown strong').text()).toBe('bold text')
+ })
+
it('keeps tilde-fenced markdown examples with nested tilde fences intact', () => {
const wrapper = mount(MarkdownRenderer, {
props: {