chore: add v0.4.8 changelog and improve scroll behavior (#234)
* chore: add v0.4.8 changelog and improve scroll behavior - Add v0.4.8 changelog entries for recent fixes - Fix forced scroll to bottom when returning from other tabs - Smooth session switch with loading transition overlay - Auto-scroll to bottom after mermaid diagram rendering - Bump version to 0.4.8 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace blob URLs with persistent download URLs and add image preview - Replace blob URLs with /api/hermes/download URLs after upload so attachments survive page refresh - Add click-to-preview overlay for image attachments - Move upload directory from /tmp to ~/.hermes-web-ui/upload Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace findLast with reverse+find for ES2022 compat Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump TypeScript lib target from ES2022 to ES2023 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add changelog entries for blob URL fix, image preview and upload dir Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hermes-web-ui",
|
"name": "hermes-web-ui",
|
||||||
"version": "0.4.7",
|
"version": "0.4.8",
|
||||||
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model (Claude, GPT, Gemini, DeepSeek) web UI with Telegram, Discord, Slack, WhatsApp integration",
|
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model (Claude, GPT, Gemini, DeepSeek) web UI with Telegram, Discord, Slack, WhatsApp integration",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -84,6 +84,19 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string):
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScrollParent(el: HTMLElement | null): HTMLElement | null {
|
||||||
|
if (!el) return null
|
||||||
|
let current: HTMLElement | null = el.parentElement
|
||||||
|
while (current) {
|
||||||
|
const { overflow, overflowY } = getComputedStyle(current)
|
||||||
|
if (overflow === 'auto' || overflow === 'scroll' || overflowY === 'auto' || overflowY === 'scroll') {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function cleanupMermaidRenderArtifacts(id: string): void {
|
function cleanupMermaidRenderArtifacts(id: string): void {
|
||||||
document.getElementById(id)?.remove()
|
document.getElementById(id)?.remove()
|
||||||
document.getElementById(`d${id}`)?.remove()
|
document.getElementById(`d${id}`)?.remove()
|
||||||
@@ -158,6 +171,13 @@ async function renderMermaidDiagrams(): Promise<void> {
|
|||||||
element.removeAttribute('data-mermaid-pending')
|
element.removeAttribute('data-mermaid-pending')
|
||||||
element.removeAttribute('data-mermaid-source')
|
element.removeAttribute('data-mermaid-source')
|
||||||
element.innerHTML = result.svg
|
element.innerHTML = result.svg
|
||||||
|
// After mermaid renders, scroll the nearest scrollable ancestor to bottom
|
||||||
|
nextTick(() => {
|
||||||
|
const scrollParent = getScrollParent(markdownBody.value)
|
||||||
|
if (scrollParent) {
|
||||||
|
scrollParent.scrollTop = scrollParent.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
cleanupMermaidRenderArtifacts(`${componentId}-${generation}-${index}`)
|
cleanupMermaidRenderArtifacts(`${componentId}-${generation}-${index}`)
|
||||||
if (unmounted || generation !== renderGeneration || !root.contains(element)) return
|
if (unmounted || generation !== renderGeneration || !root.contains(element)) return
|
||||||
@@ -329,6 +349,10 @@ function handleMarkdownClick(event: MouseEvent): void {
|
|||||||
color: $text-secondary;
|
color: $text-secondary;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-family: $font-code;
|
font-family: $font-code;
|
||||||
|
min-height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const toast = useMessage();
|
|||||||
|
|
||||||
const isSystem = computed(() => props.message.role === "system");
|
const isSystem = computed(() => props.message.role === "system");
|
||||||
const toolExpanded = ref(false);
|
const toolExpanded = ref(false);
|
||||||
|
const previewUrl = ref<string | null>(null);
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
@@ -338,6 +339,7 @@ const renderedToolResult = computed(() => {
|
|||||||
:src="att.url"
|
:src="att.url"
|
||||||
:alt="att.name"
|
:alt="att.name"
|
||||||
class="msg-attachment-thumb"
|
class="msg-attachment-thumb"
|
||||||
|
@click="previewUrl = att.url"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -417,6 +419,11 @@ const renderedToolResult = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="previewUrl" class="image-preview-overlay" @click.self="previewUrl = null">
|
||||||
|
<img :src="previewUrl" class="image-preview-img" @click="previewUrl = null" />
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -531,6 +538,7 @@ const renderedToolResult = computed(() => {
|
|||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
max-height: 160px;
|
max-height: 160px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-attachment-file {
|
.msg-attachment-file {
|
||||||
@@ -776,6 +784,24 @@ const renderedToolResult = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-preview-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-img {
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-mobile) {
|
@media (max-width: $breakpoint-mobile) {
|
||||||
.message.user .msg-body {
|
.message.user .msg-body {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const chatStore = useChatStore();
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const listRef = ref<HTMLElement>();
|
const listRef = ref<HTMLElement>();
|
||||||
|
const isTransitioning = ref(false);
|
||||||
|
let transitionTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
const displayMessages = computed(() =>
|
const displayMessages = computed(() =>
|
||||||
chatStore.messages.filter((m) => m.role !== "tool"),
|
chatStore.messages.filter((m) => m.role !== "tool"),
|
||||||
@@ -54,20 +56,61 @@ function scrollToMessage(messageId: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll to bottom once when messages are first loaded
|
// Scroll to bottom once when messages are first loaded after session switch
|
||||||
|
let pendingScrollToBottom = false
|
||||||
|
let isInitialLoad = true
|
||||||
|
|
||||||
|
function showTransition(): void {
|
||||||
|
if (isInitialLoad) return
|
||||||
|
if (transitionTimer) clearTimeout(transitionTimer)
|
||||||
|
isTransitioning.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideTransition(): void {
|
||||||
|
if (transitionTimer) clearTimeout(transitionTimer)
|
||||||
|
transitionTimer = setTimeout(() => {
|
||||||
|
isTransitioning.value = false
|
||||||
|
transitionTimer = null
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => chatStore.activeSessionId,
|
() => chatStore.activeSessionId,
|
||||||
(id) => {
|
(id) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
if (chatStore.focusMessageId) {
|
if (isInitialLoad) {
|
||||||
scrollToMessage(chatStore.focusMessageId);
|
isInitialLoad = false
|
||||||
|
if (!chatStore.focusMessageId) {
|
||||||
|
nextTick(() => scrollToBottom())
|
||||||
|
} else {
|
||||||
|
nextTick(() => scrollToMessage(chatStore.focusMessageId!))
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
scrollToBottom();
|
if (chatStore.focusMessageId) {
|
||||||
|
nextTick(() => scrollToMessage(chatStore.focusMessageId!));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingScrollToBottom = true
|
||||||
|
showTransition()
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Safety: ensure overlay is always removed once messages load
|
||||||
|
watch(
|
||||||
|
() => chatStore.messages.length,
|
||||||
|
() => {
|
||||||
|
if (pendingScrollToBottom && chatStore.messages.length > 0) {
|
||||||
|
pendingScrollToBottom = false
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
hideTransition()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => chatStore.focusMessageId,
|
() => chatStore.focusMessageId,
|
||||||
(messageId) => {
|
(messageId) => {
|
||||||
@@ -92,7 +135,15 @@ watch(
|
|||||||
scrollToMessage(chatStore.focusMessageId);
|
scrollToMessage(chatStore.focusMessageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!chatStore.isStreaming) { scrollToBottom(); return; }
|
if (pendingScrollToBottom) {
|
||||||
|
pendingScrollToBottom = false
|
||||||
|
// Wait a moment for mermaid diagrams to render, then reveal and scroll
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
hideTransition()
|
||||||
|
}, 300)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!isNearBottom()) return;
|
if (!isNearBottom()) return;
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
},
|
},
|
||||||
@@ -102,15 +153,14 @@ watch(currentToolCalls, () => {
|
|||||||
scrollToMessage(chatStore.focusMessageId);
|
scrollToMessage(chatStore.focusMessageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!chatStore.isStreaming) { scrollToBottom(); return; }
|
|
||||||
if (!isNearBottom()) return;
|
if (!isNearBottom()) return;
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="listRef" class="message-list">
|
<div ref="listRef" class="message-list" :class="{ 'is-transitioning': isTransitioning }">
|
||||||
<div v-if="chatStore.messages.length === 0" class="empty-state">
|
<div v-if="chatStore.messages.length === 0 && !isTransitioning" class="empty-state">
|
||||||
<img src="/logo.png" alt="Hermes" class="empty-logo" />
|
<img src="/logo.png" alt="Hermes" class="empty-logo" />
|
||||||
<p>{{ t("chat.emptyState") }}</p>
|
<p>{{ t("chat.emptyState") }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,10 +228,16 @@ watch(currentToolCalls, () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
background-color: $bg-card;
|
background-color: $bg-card;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-transitioning {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ export interface ChangelogEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const changelog: ChangelogEntry[] = [
|
export const changelog: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: '0.4.8',
|
||||||
|
date: '2026-04-26',
|
||||||
|
changes: ['changelog.new_0_4_8_1', 'changelog.new_0_4_8_2', 'changelog.new_0_4_8_3', 'changelog.new_0_4_8_4', 'changelog.new_0_4_8_5', 'changelog.new_0_4_8_6', 'changelog.new_0_4_8_7', 'changelog.new_0_4_8_8', 'changelog.new_0_4_8_9', 'changelog.new_0_4_8_10'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: '0.4.7',
|
version: '0.4.7',
|
||||||
date: '2026-04-25',
|
date: '2026-04-25',
|
||||||
|
|||||||
@@ -529,6 +529,16 @@ export default {
|
|||||||
|
|
||||||
// Anderungsprotokoll
|
// Anderungsprotokoll
|
||||||
changelog: {
|
changelog: {
|
||||||
|
new_0_4_8_1: 'Safe Mermaid diagram rendering with async render and timeout fallback',
|
||||||
|
new_0_4_8_2: 'Fix nested markdown fence rendering truncation',
|
||||||
|
new_0_4_8_3: 'Fix compressed session lineage projection and search',
|
||||||
|
new_0_4_8_4: 'Optimize session list N+1 queries and fix search 500 on non-CJK input',
|
||||||
|
new_0_4_8_5: 'Fix forced scroll to bottom when switching back from other tabs',
|
||||||
|
new_0_4_8_6: 'Smooth session switch with loading transition overlay',
|
||||||
|
new_0_4_8_7: 'Fix login token validation using Hermes session endpoint',
|
||||||
|
new_0_4_8_8: 'Fix image attachments broken after page refresh (blob URL persistence)',
|
||||||
|
new_0_4_8_9: 'Click image attachments to preview in fullscreen overlay',
|
||||||
|
new_0_4_8_10: 'Move upload directory from temp to ~/.hermes-web-ui/upload',
|
||||||
new_0_4_7_1: 'Echtzeit-Streaming-Anzeige von Denk-/Argumentationsblocken',
|
new_0_4_7_1: 'Echtzeit-Streaming-Anzeige von Denk-/Argumentationsblocken',
|
||||||
new_0_4_7_2: 'Prepare-Skript wahrend Docker-Build uberspringen',
|
new_0_4_7_2: 'Prepare-Skript wahrend Docker-Build uberspringen',
|
||||||
new_0_4_7_3: 'Gruppenchat-Mobile-UX-Verbesserungen und UI-Aufpolierung',
|
new_0_4_7_3: 'Gruppenchat-Mobile-UX-Verbesserungen und UI-Aufpolierung',
|
||||||
|
|||||||
@@ -680,6 +680,16 @@ export default {
|
|||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
changelog: {
|
changelog: {
|
||||||
|
new_0_4_8_1: 'Safe Mermaid diagram rendering with async render and timeout fallback',
|
||||||
|
new_0_4_8_2: 'Fix nested markdown fence rendering truncation',
|
||||||
|
new_0_4_8_3: 'Fix compressed session lineage projection and search',
|
||||||
|
new_0_4_8_4: 'Optimize session list N+1 queries and fix search 500 on non-CJK input',
|
||||||
|
new_0_4_8_5: 'Fix forced scroll to bottom when switching back from other tabs',
|
||||||
|
new_0_4_8_6: 'Smooth session switch with loading transition overlay',
|
||||||
|
new_0_4_8_7: 'Fix login token validation using Hermes session endpoint',
|
||||||
|
new_0_4_8_8: 'Fix image attachments broken after page refresh (blob URL persistence)',
|
||||||
|
new_0_4_8_9: 'Click image attachments to preview in fullscreen overlay',
|
||||||
|
new_0_4_8_10: 'Move upload directory from temp to ~/.hermes-web-ui/upload',
|
||||||
new_0_4_7_1: 'Real-time streaming display of thinking/reasoning blocks',
|
new_0_4_7_1: 'Real-time streaming display of thinking/reasoning blocks',
|
||||||
new_0_4_7_2: 'Skip prepare script during Docker build',
|
new_0_4_7_2: 'Skip prepare script during Docker build',
|
||||||
new_0_4_7_3: 'Group chat mobile UX improvements and UI polish',
|
new_0_4_7_3: 'Group chat mobile UX improvements and UI polish',
|
||||||
|
|||||||
@@ -529,6 +529,16 @@ export default {
|
|||||||
|
|
||||||
// Registro de cambios
|
// Registro de cambios
|
||||||
changelog: {
|
changelog: {
|
||||||
|
new_0_4_8_1: 'Safe Mermaid diagram rendering with async render and timeout fallback',
|
||||||
|
new_0_4_8_2: 'Fix nested markdown fence rendering truncation',
|
||||||
|
new_0_4_8_3: 'Fix compressed session lineage projection and search',
|
||||||
|
new_0_4_8_4: 'Optimize session list N+1 queries and fix search 500 on non-CJK input',
|
||||||
|
new_0_4_8_5: 'Fix forced scroll to bottom when switching back from other tabs',
|
||||||
|
new_0_4_8_6: 'Smooth session switch with loading transition overlay',
|
||||||
|
new_0_4_8_7: 'Fix login token validation using Hermes session endpoint',
|
||||||
|
new_0_4_8_8: 'Fix image attachments broken after page refresh (blob URL persistence)',
|
||||||
|
new_0_4_8_9: 'Click image attachments to preview in fullscreen overlay',
|
||||||
|
new_0_4_8_10: 'Move upload directory from temp to ~/.hermes-web-ui/upload',
|
||||||
new_0_4_7_1: 'Visualizacion en streaming en tiempo real de bloques de pensamiento/razonamiento',
|
new_0_4_7_1: 'Visualizacion en streaming en tiempo real de bloques de pensamiento/razonamiento',
|
||||||
new_0_4_7_2: 'Omitir script de preparacion durante la construccion Docker',
|
new_0_4_7_2: 'Omitir script de preparacion durante la construccion Docker',
|
||||||
new_0_4_7_3: 'Mejoras en la experiencia movil del chat grupal y pulido de UI',
|
new_0_4_7_3: 'Mejoras en la experiencia movil del chat grupal y pulido de UI',
|
||||||
|
|||||||
@@ -529,6 +529,16 @@ export default {
|
|||||||
|
|
||||||
// Journal des modifications
|
// Journal des modifications
|
||||||
changelog: {
|
changelog: {
|
||||||
|
new_0_4_8_1: 'Safe Mermaid diagram rendering with async render and timeout fallback',
|
||||||
|
new_0_4_8_2: 'Fix nested markdown fence rendering truncation',
|
||||||
|
new_0_4_8_3: 'Fix compressed session lineage projection and search',
|
||||||
|
new_0_4_8_4: 'Optimize session list N+1 queries and fix search 500 on non-CJK input',
|
||||||
|
new_0_4_8_5: 'Fix forced scroll to bottom when switching back from other tabs',
|
||||||
|
new_0_4_8_6: 'Smooth session switch with loading transition overlay',
|
||||||
|
new_0_4_8_7: 'Fix login token validation using Hermes session endpoint',
|
||||||
|
new_0_4_8_8: 'Fix image attachments broken after page refresh (blob URL persistence)',
|
||||||
|
new_0_4_8_9: 'Click image attachments to preview in fullscreen overlay',
|
||||||
|
new_0_4_8_10: 'Move upload directory from temp to ~/.hermes-web-ui/upload',
|
||||||
new_0_4_7_1: 'Affichage en streaming en temps reel des blocs de reflexion/raisonnement',
|
new_0_4_7_1: 'Affichage en streaming en temps reel des blocs de reflexion/raisonnement',
|
||||||
new_0_4_7_2: 'Ignorer le script de preparation lors du build Docker',
|
new_0_4_7_2: 'Ignorer le script de preparation lors du build Docker',
|
||||||
new_0_4_7_3: 'Ameliorations UX mobile du chat de groupe et polissage de l\'interface',
|
new_0_4_7_3: 'Ameliorations UX mobile du chat de groupe et polissage de l\'interface',
|
||||||
|
|||||||
@@ -529,6 +529,16 @@ export default {
|
|||||||
|
|
||||||
// 更新履歴
|
// 更新履歴
|
||||||
changelog: {
|
changelog: {
|
||||||
|
new_0_4_8_1: 'Safe Mermaid diagram rendering with async render and timeout fallback',
|
||||||
|
new_0_4_8_2: 'Fix nested markdown fence rendering truncation',
|
||||||
|
new_0_4_8_3: 'Fix compressed session lineage projection and search',
|
||||||
|
new_0_4_8_4: 'Optimize session list N+1 queries and fix search 500 on non-CJK input',
|
||||||
|
new_0_4_8_5: 'Fix forced scroll to bottom when switching back from other tabs',
|
||||||
|
new_0_4_8_6: 'Smooth session switch with loading transition overlay',
|
||||||
|
new_0_4_8_7: 'Fix login token validation using Hermes session endpoint',
|
||||||
|
new_0_4_8_8: 'Fix image attachments broken after page refresh (blob URL persistence)',
|
||||||
|
new_0_4_8_9: 'Click image attachments to preview in fullscreen overlay',
|
||||||
|
new_0_4_8_10: 'Move upload directory from temp to ~/.hermes-web-ui/upload',
|
||||||
new_0_4_7_1: '思考/推論ブロックのリアルタイムストリーミング表示',
|
new_0_4_7_1: '思考/推論ブロックのリアルタイムストリーミング表示',
|
||||||
new_0_4_7_2: 'Dockerビルド時にprepareスクリプトをスキップ',
|
new_0_4_7_2: 'Dockerビルド時にprepareスクリプトをスキップ',
|
||||||
new_0_4_7_3: 'グループチャットのモバイルUX改善とUIのブラッシュアップ',
|
new_0_4_7_3: 'グループチャットのモバイルUX改善とUIのブラッシュアップ',
|
||||||
|
|||||||
@@ -529,6 +529,16 @@ export default {
|
|||||||
|
|
||||||
// 변경 이력
|
// 변경 이력
|
||||||
changelog: {
|
changelog: {
|
||||||
|
new_0_4_8_1: 'Safe Mermaid diagram rendering with async render and timeout fallback',
|
||||||
|
new_0_4_8_2: 'Fix nested markdown fence rendering truncation',
|
||||||
|
new_0_4_8_3: 'Fix compressed session lineage projection and search',
|
||||||
|
new_0_4_8_4: 'Optimize session list N+1 queries and fix search 500 on non-CJK input',
|
||||||
|
new_0_4_8_5: 'Fix forced scroll to bottom when switching back from other tabs',
|
||||||
|
new_0_4_8_6: 'Smooth session switch with loading transition overlay',
|
||||||
|
new_0_4_8_7: 'Fix login token validation using Hermes session endpoint',
|
||||||
|
new_0_4_8_8: 'Fix image attachments broken after page refresh (blob URL persistence)',
|
||||||
|
new_0_4_8_9: 'Click image attachments to preview in fullscreen overlay',
|
||||||
|
new_0_4_8_10: 'Move upload directory from temp to ~/.hermes-web-ui/upload',
|
||||||
new_0_4_7_1: '생각/추론 블록의 실시간 스트리밍 표시',
|
new_0_4_7_1: '생각/추론 블록의 실시간 스트리밍 표시',
|
||||||
new_0_4_7_2: 'Docker 빌드 중 prepare 스크립트 건너뛰기',
|
new_0_4_7_2: 'Docker 빌드 중 prepare 스크립트 건너뛰기',
|
||||||
new_0_4_7_3: '그룹 채팅 모바일 UX 개선 및 UI 다듬기',
|
new_0_4_7_3: '그룹 채팅 모바일 UX 개선 및 UI 다듬기',
|
||||||
|
|||||||
@@ -529,6 +529,16 @@ export default {
|
|||||||
|
|
||||||
// Registro de alteracoes
|
// Registro de alteracoes
|
||||||
changelog: {
|
changelog: {
|
||||||
|
new_0_4_8_1: 'Safe Mermaid diagram rendering with async render and timeout fallback',
|
||||||
|
new_0_4_8_2: 'Fix nested markdown fence rendering truncation',
|
||||||
|
new_0_4_8_3: 'Fix compressed session lineage projection and search',
|
||||||
|
new_0_4_8_4: 'Optimize session list N+1 queries and fix search 500 on non-CJK input',
|
||||||
|
new_0_4_8_5: 'Fix forced scroll to bottom when switching back from other tabs',
|
||||||
|
new_0_4_8_6: 'Smooth session switch with loading transition overlay',
|
||||||
|
new_0_4_8_7: 'Fix login token validation using Hermes session endpoint',
|
||||||
|
new_0_4_8_8: 'Fix image attachments broken after page refresh (blob URL persistence)',
|
||||||
|
new_0_4_8_9: 'Click image attachments to preview in fullscreen overlay',
|
||||||
|
new_0_4_8_10: 'Move upload directory from temp to ~/.hermes-web-ui/upload',
|
||||||
new_0_4_7_1: 'Exibicao em streaming em tempo real de blocos de pensamento/razoamento',
|
new_0_4_7_1: 'Exibicao em streaming em tempo real de blocos de pensamento/razoamento',
|
||||||
new_0_4_7_2: 'Ignorar script de preparacao durante o build Docker',
|
new_0_4_7_2: 'Ignorar script de preparacao durante o build Docker',
|
||||||
new_0_4_7_3: 'Melhorias na UX mobile do chat em grupo e polimento da UI',
|
new_0_4_7_3: 'Melhorias na UX mobile do chat em grupo e polimento da UI',
|
||||||
|
|||||||
@@ -682,6 +682,16 @@ export default {
|
|||||||
|
|
||||||
// 更新日志
|
// 更新日志
|
||||||
changelog: {
|
changelog: {
|
||||||
|
new_0_4_8_1: '安全渲染 Mermaid 图表,支持异步渲染和超时降级',
|
||||||
|
new_0_4_8_2: '修复嵌套 Markdown 代码块导致渲染截断',
|
||||||
|
new_0_4_8_3: '修复压缩续接会话投影和搜索问题',
|
||||||
|
new_0_4_8_4: '优化会话列表 N+1 查询,修复非 CJK 搜索 500 错误',
|
||||||
|
new_0_4_8_5: '修复切换标签页返回时强制滚动到底部',
|
||||||
|
new_0_4_8_6: '切换会话时添加加载过渡动画',
|
||||||
|
new_0_4_8_7: '修复登录 Token 验证使用正确的会话端点',
|
||||||
|
new_0_4_8_8: '修复刷新页面后图片附件失效问题',
|
||||||
|
new_0_4_8_9: '点击图片附件可全屏预览',
|
||||||
|
new_0_4_8_10: '上传目录从临时目录迁移到 ~/.hermes-web-ui/upload',
|
||||||
new_0_4_7_1: '实时流式显示思考/推理过程',
|
new_0_4_7_1: '实时流式显示思考/推理过程',
|
||||||
new_0_4_7_2: 'Docker 构建时跳过 prepare 脚本',
|
new_0_4_7_2: 'Docker 构建时跳过 prepare 脚本',
|
||||||
new_0_4_7_3: '群聊移动端体验改进和 UI 优化',
|
new_0_4_7_3: '群聊移动端体验改进和 UI 优化',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { startRun, streamRunEvents, type ChatMessage, type RunEvent } from '@/api/hermes/chat'
|
import { startRun, streamRunEvents, type ChatMessage, type RunEvent } from '@/api/hermes/chat'
|
||||||
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, fetchSessionUsageSingle, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions'
|
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, fetchSessionUsageSingle, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions'
|
||||||
|
import { getApiKey } from '@/api/client'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useAppStore } from './app'
|
import { useAppStore } from './app'
|
||||||
@@ -797,7 +798,21 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
let inputText = content.trim()
|
let inputText = content.trim()
|
||||||
if (attachments && attachments.length > 0) {
|
if (attachments && attachments.length > 0) {
|
||||||
const uploaded = await uploadFiles(attachments)
|
const uploaded = await uploadFiles(attachments)
|
||||||
const pathParts = uploaded.map(f => `[File: ${f.name}](${f.path})`)
|
// Replace blob URLs with persistent download URLs on the user message
|
||||||
|
const token = getApiKey()
|
||||||
|
const urlMap = new Map(uploaded.map(f => {
|
||||||
|
const base = `/api/hermes/download?path=${encodeURIComponent(f.path)}&name=${encodeURIComponent(f.name)}`
|
||||||
|
return [f.name, token ? `${base}&token=${encodeURIComponent(token)}` : base]
|
||||||
|
}))
|
||||||
|
const msgs = getSessionMsgs(sid)
|
||||||
|
const lastUser = msgs.findLast(m => m.id === userMsg.id)
|
||||||
|
if (lastUser?.attachments) {
|
||||||
|
lastUser.attachments = lastUser.attachments.map(a => {
|
||||||
|
const dl = urlMap.get(a.name)
|
||||||
|
return dl ? { ...a, url: dl } : a
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const pathParts = uploaded.map(f => `[File: ${f.name}](${urlMap.get(f.name)})`)
|
||||||
inputText = inputText ? inputText + '\n\n' + pathParts.join('\n') : pathParts.join('\n')
|
inputText = inputText ? inputText + '\n\n' + pathParts.join('\n') : pathParts.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import { tmpdir } from 'os'
|
import { homedir } from 'os'
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
port: parseInt(process.env.PORT || '8648', 10),
|
port: parseInt(process.env.PORT || '8648', 10),
|
||||||
upstream: process.env.UPSTREAM || 'http://127.0.0.1:8642',
|
upstream: process.env.UPSTREAM || 'http://127.0.0.1:8642',
|
||||||
uploadDir: process.env.UPLOAD_DIR || resolve(tmpdir(), 'hermes-uploads'),
|
uploadDir: process.env.UPLOAD_DIR || resolve(homedir(), '.hermes-web-ui', 'upload'),
|
||||||
dataDir: resolve(__dirname, '..', 'data'),
|
dataDir: resolve(__dirname, '..', 'data'),
|
||||||
corsOrigins: process.env.CORS_ORIGINS || '*',
|
corsOrigins: process.env.CORS_ORIGINS || '*',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"lib": ["ES2025", "DOM", "DOM.Iterable"],
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"types": ["vite/client", "vitest/globals"],
|
"types": ["vite/client", "vitest/globals"],
|
||||||
"ignoreDeprecations": "6.0",
|
"ignoreDeprecations": "6.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user