Adjust outline user item background (#836)
* feat:新增大纲功能 * Adjust outline user item background --------- Co-authored-by: chenxusheng <chenxusheng@haizhi.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import ConversationMonitorPane from "./ConversationMonitorPane.vue";
|
|||||||
import MessageList from "./MessageList.vue";
|
import MessageList from "./MessageList.vue";
|
||||||
import SessionListItem from "./SessionListItem.vue";
|
import SessionListItem from "./SessionListItem.vue";
|
||||||
import DrawerPanel from "./DrawerPanel.vue";
|
import DrawerPanel from "./DrawerPanel.vue";
|
||||||
|
import OutlinePanel from "./OutlinePanel.vue";
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
@@ -33,6 +34,7 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const showDrawer = ref(false);
|
const showDrawer = ref(false);
|
||||||
const drawerActiveTab = ref<"terminal" | "files">("files");
|
const drawerActiveTab = ref<"terminal" | "files">("files");
|
||||||
|
const showOutline = ref(false);
|
||||||
|
|
||||||
const currentMode = ref<"chat" | "live">("chat");
|
const currentMode = ref<"chat" | "live">("chat");
|
||||||
|
|
||||||
@@ -988,6 +990,30 @@ async function handleSessionModelCustomSubmit() {
|
|||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<!-- chat/live mode toggle hidden -->
|
<!-- chat/live mode toggle hidden -->
|
||||||
<template v-if="currentMode === 'chat'">
|
<template v-if="currentMode === 'chat'">
|
||||||
|
<NTooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<NButton
|
||||||
|
quaternary
|
||||||
|
size="small"
|
||||||
|
@click="showOutline = !showOutline"
|
||||||
|
circle
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path d="M3 12h18M3 6h18M3 18h18" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
{{ t("chat.outlineTitle") }}
|
||||||
|
</NTooltip>
|
||||||
<NTooltip trigger="hover">
|
<NTooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton
|
<NButton
|
||||||
@@ -1042,7 +1068,12 @@ async function handleSessionModelCustomSubmit() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<template v-if="currentMode === 'chat'">
|
<template v-if="currentMode === 'chat'">
|
||||||
<MessageList />
|
<div class="chat-content-wrapper">
|
||||||
|
<div class="chat-main-content">
|
||||||
|
<MessageList />
|
||||||
|
</div>
|
||||||
|
<OutlinePanel v-if="showOutline" :messages="chatStore.messages" />
|
||||||
|
</div>
|
||||||
<div v-if="visibleApproval" class="approval-bar">
|
<div v-if="visibleApproval" class="approval-bar">
|
||||||
<div class="approval-icon" aria-hidden="true">
|
<div class="approval-icon" aria-hidden="true">
|
||||||
<svg
|
<svg
|
||||||
@@ -1637,6 +1668,21 @@ async function handleSessionModelCustomSubmit() {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-content-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-main-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ import { downloadFile, getDownloadUrl } from '@/api/hermes/download'
|
|||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
content: string
|
content: string
|
||||||
mentionNames?: string[]
|
mentionNames?: string[]
|
||||||
|
headingIdPrefix?: string
|
||||||
}>(), {
|
}>(), {
|
||||||
mentionNames: () => [],
|
mentionNames: () => [],
|
||||||
|
headingIdPrefix: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -68,6 +70,27 @@ function normalizeLocalFilePath(path: string): string {
|
|||||||
const renderedHtml = computed(() => {
|
const renderedHtml = computed(() => {
|
||||||
let html = md.render(repairNestedMarkdownFences(props.content))
|
let html = md.render(repairNestedMarkdownFences(props.content))
|
||||||
|
|
||||||
|
// Add IDs to headings for anchor links
|
||||||
|
const prefix = props.headingIdPrefix ? `${props.headingIdPrefix}-` : ''
|
||||||
|
let headingCounter = 0
|
||||||
|
// Match any h1-h6 tags, with or without attributes
|
||||||
|
html = html.replace(/<(h[1-6])([^>]*)>/g, (match, tag, attrs) => {
|
||||||
|
headingCounter++
|
||||||
|
const id = `${prefix}heading-${headingCounter}`
|
||||||
|
|
||||||
|
// Check if id attribute already exists
|
||||||
|
if (attrs.includes('id=')) {
|
||||||
|
// Replace existing id
|
||||||
|
return match.replace(/id="[^"]*"/, `id="${id}"`).replace(/id='[^']*'/, `id="${id}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new id
|
||||||
|
if (attrs.trim() === '') {
|
||||||
|
return `<${tag} id="${id}">`
|
||||||
|
}
|
||||||
|
return `<${tag} ${attrs.trim()} id="${id}">`
|
||||||
|
})
|
||||||
|
|
||||||
// Replace image src paths with download URLs
|
// Replace image src paths with download URLs
|
||||||
html = html.replace(/\bsrc=(["'])([^"']+)\1/g, (match, quote, path) => {
|
html = html.replace(/\bsrc=(["'])([^"']+)\1/g, (match, quote, path) => {
|
||||||
if (!isLocalFilePath(path)) return match
|
if (!isLocalFilePath(path)) return match
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ const JSON_MAX_KEYS_PER_OBJECT = 50;
|
|||||||
const JSON_MAX_ITEMS_PER_ARRAY = 50;
|
const JSON_MAX_ITEMS_PER_ARRAY = 50;
|
||||||
const JSON_TRUNCATED_KEY = "__truncated__";
|
const JSON_TRUNCATED_KEY = "__truncated__";
|
||||||
|
|
||||||
const props = defineProps<{ message: Message; highlight?: boolean }>();
|
const props = defineProps<{ message: Message; highlight?: boolean; headingIdPrefix?: string }>();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const toast = useMessage();
|
const toast = useMessage();
|
||||||
|
|
||||||
const isSystem = computed(() => props.message.role === "system");
|
const isSystem = computed(() => props.message.role === "system");
|
||||||
|
|
||||||
|
const effectiveHeadingIdPrefix = computed(() => props.headingIdPrefix || `msg-${props.message.id}`);
|
||||||
const isCommandMessage = computed(() => props.message.role === "command" || props.message.systemType === "command");
|
const isCommandMessage = computed(() => props.message.role === "command" || props.message.systemType === "command");
|
||||||
const isCommandError = computed(() => props.message.role === "command" && props.message.systemType === "error");
|
const isCommandError = computed(() => props.message.role === "command" && props.message.systemType === "error");
|
||||||
const isStatusCommand = computed(() => isCommandMessage.value && props.message.commandAction === "status");
|
const isStatusCommand = computed(() => isCommandMessage.value && props.message.commandAction === "status");
|
||||||
@@ -868,6 +870,7 @@ onBeforeUnmount(() => {
|
|||||||
<MarkdownRenderer
|
<MarkdownRenderer
|
||||||
v-if="parsedThinking.body && message.role === 'assistant'"
|
v-if="parsedThinking.body && message.role === 'assistant'"
|
||||||
:content="parsedThinking.body"
|
:content="parsedThinking.body"
|
||||||
|
:heading-id-prefix="effectiveHeadingIdPrefix"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Render user message content -->
|
<!-- Render user message content -->
|
||||||
@@ -915,6 +918,7 @@ onBeforeUnmount(() => {
|
|||||||
<MarkdownRenderer
|
<MarkdownRenderer
|
||||||
v-if="message.role === 'assistant' && message.content && !parsedThinking.body"
|
v-if="message.role === 'assistant' && message.content && !parsedThinking.body"
|
||||||
:content="message.content"
|
:content="message.content"
|
||||||
|
:heading-id-prefix="effectiveHeadingIdPrefix"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Render system message content -->
|
<!-- Render system message content -->
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { Message } from '@/stores/hermes/chat'
|
||||||
|
|
||||||
|
interface OutlineItem {
|
||||||
|
id: string
|
||||||
|
type: 'user' | 'outline'
|
||||||
|
content: string
|
||||||
|
messageId: string
|
||||||
|
level: number
|
||||||
|
anchorId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
messages: Message[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
function extractAllHeadings(text: string, messageId: string): OutlineItem[] {
|
||||||
|
const items: OutlineItem[] = []
|
||||||
|
let cleanedText = text.replace(/<think>[\s\S]*?<\/think>/g, '')
|
||||||
|
const lines = cleanedText.split('\n')
|
||||||
|
|
||||||
|
let headingIndex = 0
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
const h1Match = trimmed.match(/^#\s+(.+)/)
|
||||||
|
const h2Match = trimmed.match(/^##\s+(.+)/)
|
||||||
|
const h3Match = trimmed.match(/^###\s+(.+)/)
|
||||||
|
|
||||||
|
if (h1Match) {
|
||||||
|
headingIndex++
|
||||||
|
items.push({
|
||||||
|
id: `outline-${messageId}-h${headingIndex}`,
|
||||||
|
type: 'outline',
|
||||||
|
content: h1Match[1].trim(),
|
||||||
|
messageId,
|
||||||
|
level: 1,
|
||||||
|
anchorId: `msg-${messageId}-heading-${headingIndex}`
|
||||||
|
})
|
||||||
|
} else if (h2Match) {
|
||||||
|
headingIndex++
|
||||||
|
items.push({
|
||||||
|
id: `outline-${messageId}-h${headingIndex}`,
|
||||||
|
type: 'outline',
|
||||||
|
content: h2Match[1].trim(),
|
||||||
|
messageId,
|
||||||
|
level: 2,
|
||||||
|
anchorId: `msg-${messageId}-heading-${headingIndex}`
|
||||||
|
})
|
||||||
|
} else if (h3Match) {
|
||||||
|
headingIndex++
|
||||||
|
items.push({
|
||||||
|
id: `outline-${messageId}-h${headingIndex}`,
|
||||||
|
type: 'outline',
|
||||||
|
content: h3Match[1].trim(),
|
||||||
|
messageId,
|
||||||
|
level: 3,
|
||||||
|
anchorId: `msg-${messageId}-heading-${headingIndex}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractUserQuestion(text: string): string {
|
||||||
|
const cleanedText = text.replace(/<think>[\s\S]*?<\/think>/g, '')
|
||||||
|
const firstLine = cleanedText.split('\n')[0] || ''
|
||||||
|
if (firstLine.length > 50) {
|
||||||
|
return firstLine.slice(0, 50) + '...'
|
||||||
|
}
|
||||||
|
return firstLine || t('chat.outlineUserQuestion')
|
||||||
|
}
|
||||||
|
|
||||||
|
const outlineItems = computed<OutlineItem[]>(() => {
|
||||||
|
const items: OutlineItem[] = []
|
||||||
|
let i = 0
|
||||||
|
const filteredMessages = props.messages.filter(m => m.role === 'user' || m.role === 'assistant')
|
||||||
|
|
||||||
|
while (i < filteredMessages.length) {
|
||||||
|
const msg = filteredMessages[i]
|
||||||
|
if (msg.role === 'user') {
|
||||||
|
items.push({
|
||||||
|
id: `user-${msg.id}`,
|
||||||
|
type: 'user',
|
||||||
|
content: extractUserQuestion(msg.content || ''),
|
||||||
|
messageId: msg.id,
|
||||||
|
level: 0,
|
||||||
|
anchorId: `message-${msg.id}`
|
||||||
|
})
|
||||||
|
i++
|
||||||
|
while (i < filteredMessages.length && filteredMessages[i].role !== 'assistant') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if (i < filteredMessages.length) {
|
||||||
|
const assistantMsg = filteredMessages[i]
|
||||||
|
const headings = extractAllHeadings(assistantMsg.content || '', assistantMsg.id)
|
||||||
|
items.push(...headings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
function scrollToTarget(anchorId: string) {
|
||||||
|
console.log('Attempting to scroll to anchor:', anchorId)
|
||||||
|
nextTick(() => {
|
||||||
|
const el = document.getElementById(anchorId)
|
||||||
|
console.log('Found element:', el)
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
} else {
|
||||||
|
// Debug: log all heading elements with IDs
|
||||||
|
console.log('All heading elements on page:')
|
||||||
|
document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(el => {
|
||||||
|
console.log(' -', el.id, ':', el.textContent?.slice(0, 50))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="outline-panel">
|
||||||
|
<div class="outline-header">
|
||||||
|
<span class="outline-title">{{ t('chat.outlineTitle') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="outline-content">
|
||||||
|
<template v-if="outlineItems.length > 0">
|
||||||
|
<template v-for="item in outlineItems" :key="item.id">
|
||||||
|
<div
|
||||||
|
v-if="item.type === 'user'"
|
||||||
|
class="outline-item user-item"
|
||||||
|
@click="scrollToTarget(item.anchorId)"
|
||||||
|
>
|
||||||
|
<div class="user-question">
|
||||||
|
<span class="q-label">Q:</span>
|
||||||
|
<span class="q-text">{{ item.content }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="outline-item outline-heading-item"
|
||||||
|
:class="`level-${item.level}`"
|
||||||
|
@click="scrollToTarget(item.anchorId)"
|
||||||
|
>
|
||||||
|
<div class="heading-item">
|
||||||
|
<span class="heading-text">{{ item.content }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<div v-else class="outline-empty">{{ t('chat.outlineEmpty') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/styles/variables" as *;
|
||||||
|
|
||||||
|
.outline-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: $bg-card;
|
||||||
|
border-left: 1px solid $border-color;
|
||||||
|
width: 280px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-mobile) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: min(280px, 86vw);
|
||||||
|
z-index: 8;
|
||||||
|
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-header {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-item {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-question {
|
||||||
|
background-color: $bg-secondary;
|
||||||
|
color: $text-primary;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: $bg-input;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-label {
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-text {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-heading-item {
|
||||||
|
&.level-1 {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-2 {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-3 {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-1 & {
|
||||||
|
.heading-marker {
|
||||||
|
color: $text-primary;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.heading-text {
|
||||||
|
color: $text-primary;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-2 & {
|
||||||
|
.heading-marker {
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
.heading-text {
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-3 & {
|
||||||
|
.heading-marker {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
.heading-text {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-text {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -120,6 +120,9 @@ export default {
|
|||||||
contextEditSuccess: 'Kontextlänge aktualisiert',
|
contextEditSuccess: 'Kontextlänge aktualisiert',
|
||||||
contextEditFailed: 'Aktualisierung fehlgeschlagen',
|
contextEditFailed: 'Aktualisierung fehlgeschlagen',
|
||||||
emptyState: 'Starten Sie eine Konversation mit Hermes Agent',
|
emptyState: 'Starten Sie eine Konversation mit Hermes Agent',
|
||||||
|
outlineTitle: 'Konversationsübersicht',
|
||||||
|
outlineEmpty: 'Kein Konversationsinhalt',
|
||||||
|
outlineUserQuestion: 'Benutzerfrage',
|
||||||
inputPlaceholder: 'Nachricht eingeben... (Enter zum Senden, Shift+Enter fur neue Zeile)',
|
inputPlaceholder: 'Nachricht eingeben... (Enter zum Senden, Shift+Enter fur neue Zeile)',
|
||||||
slashCommandArgs: {
|
slashCommandArgs: {
|
||||||
message: '<Nachricht>',
|
message: '<Nachricht>',
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ export default {
|
|||||||
contextEditFailed: 'Update failed',
|
contextEditFailed: 'Update failed',
|
||||||
emptyState: 'Start a conversation with Hermes Agent',
|
emptyState: 'Start a conversation with Hermes Agent',
|
||||||
cliEmptyState: 'Start a CLI chat session',
|
cliEmptyState: 'Start a CLI chat session',
|
||||||
|
outlineTitle: 'Conversation Outline',
|
||||||
|
outlineEmpty: 'No conversation content',
|
||||||
|
outlineUserQuestion: 'User question',
|
||||||
inputPlaceholder: 'Type a message... (Enter to send, Shift+Enter for new line)',
|
inputPlaceholder: 'Type a message... (Enter to send, Shift+Enter for new line)',
|
||||||
slashCommandArgs: {
|
slashCommandArgs: {
|
||||||
message: '<message>',
|
message: '<message>',
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ export default {
|
|||||||
contextEditSuccess: 'Longitud del contexto actualizada',
|
contextEditSuccess: 'Longitud del contexto actualizada',
|
||||||
contextEditFailed: 'Error en la actualización',
|
contextEditFailed: 'Error en la actualización',
|
||||||
emptyState: 'Inicia una conversacion con Hermes Agent',
|
emptyState: 'Inicia una conversacion con Hermes Agent',
|
||||||
|
outlineTitle: 'Esquema de la conversación',
|
||||||
|
outlineEmpty: 'Sin contenido de conversación',
|
||||||
|
outlineUserQuestion: 'Pregunta del usuario',
|
||||||
inputPlaceholder: 'Escribe un mensaje... (Enter para enviar, Shift+Enter para nueva linea)',
|
inputPlaceholder: 'Escribe un mensaje... (Enter para enviar, Shift+Enter para nueva linea)',
|
||||||
slashCommandArgs: {
|
slashCommandArgs: {
|
||||||
message: '<mensaje>',
|
message: '<mensaje>',
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ export default {
|
|||||||
contextEditSuccess: 'Longueur du contexte mise à jour',
|
contextEditSuccess: 'Longueur du contexte mise à jour',
|
||||||
contextEditFailed: 'Échec de la mise à jour',
|
contextEditFailed: 'Échec de la mise à jour',
|
||||||
emptyState: 'Demarrer une conversation avec Hermes Agent',
|
emptyState: 'Demarrer une conversation avec Hermes Agent',
|
||||||
|
outlineTitle: 'Plan de la conversation',
|
||||||
|
outlineEmpty: 'Aucun contenu de conversation',
|
||||||
|
outlineUserQuestion: 'Question utilisateur',
|
||||||
inputPlaceholder: 'Tapez un message... (Entree pour envoyer, Shift+Entree pour un saut de ligne)',
|
inputPlaceholder: 'Tapez un message... (Entree pour envoyer, Shift+Entree pour un saut de ligne)',
|
||||||
slashCommandArgs: {
|
slashCommandArgs: {
|
||||||
message: '<message>',
|
message: '<message>',
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ export default {
|
|||||||
contextEditSuccess: 'コンテキスト長を更新しました',
|
contextEditSuccess: 'コンテキスト長を更新しました',
|
||||||
contextEditFailed: '更新に失敗しました',
|
contextEditFailed: '更新に失敗しました',
|
||||||
emptyState: 'Hermes Agent と会話を開始しましょう',
|
emptyState: 'Hermes Agent と会話を開始しましょう',
|
||||||
|
outlineTitle: '会話アウトライン',
|
||||||
|
outlineEmpty: '会話内容はありません',
|
||||||
|
outlineUserQuestion: 'ユーザーの質問',
|
||||||
inputPlaceholder: 'メッセージを入力... (Enter で送信、Shift+Enter で改行)',
|
inputPlaceholder: 'メッセージを入力... (Enter で送信、Shift+Enter で改行)',
|
||||||
slashCommandArgs: {
|
slashCommandArgs: {
|
||||||
message: '<メッセージ>',
|
message: '<メッセージ>',
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ export default {
|
|||||||
contextEditSuccess: '컨텍스트 길이가 업데이트되었습니다',
|
contextEditSuccess: '컨텍스트 길이가 업데이트되었습니다',
|
||||||
contextEditFailed: '업데이트 실패',
|
contextEditFailed: '업데이트 실패',
|
||||||
emptyState: 'Hermes Agent와 대화를 시작하세요',
|
emptyState: 'Hermes Agent와 대화를 시작하세요',
|
||||||
|
outlineTitle: '대화 개요',
|
||||||
|
outlineEmpty: '대화 내용이 없습니다',
|
||||||
|
outlineUserQuestion: '사용자 질문',
|
||||||
inputPlaceholder: '메시지를 입력하세요... (Enter로 전송, Shift+Enter로 줄바꿈)',
|
inputPlaceholder: '메시지를 입력하세요... (Enter로 전송, Shift+Enter로 줄바꿈)',
|
||||||
slashCommandArgs: {
|
slashCommandArgs: {
|
||||||
message: '<메시지>',
|
message: '<메시지>',
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ export default {
|
|||||||
contextEditSuccess: 'Tamanho do contexto atualizado',
|
contextEditSuccess: 'Tamanho do contexto atualizado',
|
||||||
contextEditFailed: 'Falha na atualização',
|
contextEditFailed: 'Falha na atualização',
|
||||||
emptyState: 'Inicie uma conversa com o Hermes Agent',
|
emptyState: 'Inicie uma conversa com o Hermes Agent',
|
||||||
|
outlineTitle: 'Esboço da conversa',
|
||||||
|
outlineEmpty: 'Nenhum conteúdo da conversa',
|
||||||
|
outlineUserQuestion: 'Pergunta do usuário',
|
||||||
inputPlaceholder: 'Digite uma mensagem... (Enter para enviar, Shift+Enter para nova linha)',
|
inputPlaceholder: 'Digite uma mensagem... (Enter para enviar, Shift+Enter para nova linha)',
|
||||||
slashCommandArgs: {
|
slashCommandArgs: {
|
||||||
message: '<mensagem>',
|
message: '<mensagem>',
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ export default {
|
|||||||
contextEditSuccess: '上下文長度已更新',
|
contextEditSuccess: '上下文長度已更新',
|
||||||
contextEditFailed: '更新失敗',
|
contextEditFailed: '更新失敗',
|
||||||
emptyState: '開始與 Hermes Agent 對話',
|
emptyState: '開始與 Hermes Agent 對話',
|
||||||
|
outlineTitle: '會話大綱',
|
||||||
|
outlineEmpty: '暫無會話內容',
|
||||||
|
outlineUserQuestion: '使用者問題',
|
||||||
inputPlaceholder: '輸入訊息... (Enter 發送,Shift+Enter 換行)',
|
inputPlaceholder: '輸入訊息... (Enter 發送,Shift+Enter 換行)',
|
||||||
slashCommandArgs: {
|
slashCommandArgs: {
|
||||||
message: '<訊息>',
|
message: '<訊息>',
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ export default {
|
|||||||
contextEditFailed: '更新失败',
|
contextEditFailed: '更新失败',
|
||||||
emptyState: '开始与 Hermes Agent 对话',
|
emptyState: '开始与 Hermes Agent 对话',
|
||||||
cliEmptyState: '开始 CLI 对话',
|
cliEmptyState: '开始 CLI 对话',
|
||||||
|
outlineTitle: '会话大纲',
|
||||||
|
outlineEmpty: '暂无会话内容',
|
||||||
|
outlineUserQuestion: '用户问题',
|
||||||
inputPlaceholder: '输入消息... (Enter 发送,Shift+Enter 换行)',
|
inputPlaceholder: '输入消息... (Enter 发送,Shift+Enter 换行)',
|
||||||
slashCommandArgs: {
|
slashCommandArgs: {
|
||||||
message: '<消息>',
|
message: '<消息>',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getSourceLabel } from '@/shared/session-display'
|
|||||||
import { copyToClipboard } from '@/utils/clipboard'
|
import { copyToClipboard } from '@/utils/clipboard'
|
||||||
import HistoryMessageList from '@/components/hermes/chat/HistoryMessageList.vue'
|
import HistoryMessageList from '@/components/hermes/chat/HistoryMessageList.vue'
|
||||||
import SessionListItem from '@/components/hermes/chat/SessionListItem.vue'
|
import SessionListItem from '@/components/hermes/chat/SessionListItem.vue'
|
||||||
|
import OutlinePanel from '@/components/hermes/chat/OutlinePanel.vue'
|
||||||
import { fetchHermesSessions, fetchHermesSession, type SessionSummary } from '@/api/hermes/sessions'
|
import { fetchHermesSessions, fetchHermesSession, type SessionSummary } from '@/api/hermes/sessions'
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
@@ -26,6 +27,7 @@ const hermesSessionsLoaded = ref(false)
|
|||||||
// History page's own selected session (independent from chatStore)
|
// History page's own selected session (independent from chatStore)
|
||||||
const historySessionId = ref<string | null>(null)
|
const historySessionId = ref<string | null>(null)
|
||||||
const historySession = ref<Session | null>(null)
|
const historySession = ref<Session | null>(null)
|
||||||
|
const showOutline = ref(false)
|
||||||
|
|
||||||
async function loadHermesSessions() {
|
async function loadHermesSessions() {
|
||||||
if (hermesSessionsLoading.value) return
|
if (hermesSessionsLoading.value) return
|
||||||
@@ -340,6 +342,16 @@ async function copySessionId(id?: string) {
|
|||||||
<span v-if="historySession?.workspace" class="workspace-badge" :title="historySession.workspace">📁 {{ historySession.workspace.split('/').pop() || historySession.workspace }}</span>
|
<span v-if="historySession?.workspace" class="workspace-badge" :title="historySession.workspace">📁 {{ historySession.workspace.split('/').pop() || historySession.workspace }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<NTooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<NButton quaternary size="small" @click="showOutline = !showOutline" circle>
|
||||||
|
<template #icon>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 12h18M3 6h18M3 18h18"/></svg>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
{{ t('chat.outlineTitle') }}
|
||||||
|
</NTooltip>
|
||||||
<NTooltip trigger="hover">
|
<NTooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton quaternary size="small" @click="copySessionId()" circle>
|
<NButton quaternary size="small" @click="copySessionId()" circle>
|
||||||
@@ -353,7 +365,12 @@ async function copySessionId(id?: string) {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<HistoryMessageList :session="historySession" />
|
<div class="history-content-wrapper">
|
||||||
|
<div class="history-main-content">
|
||||||
|
<HistoryMessageList :session="historySession" />
|
||||||
|
</div>
|
||||||
|
<OutlinePanel v-if="showOutline && historySession" :messages="historySession.messages || []" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -367,6 +384,21 @@ async function copySessionId(id?: string) {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-content-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-main-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.session-list {
|
.session-list {
|
||||||
width: 220px;
|
width: 220px;
|
||||||
border-right: 1px solid $border-color;
|
border-right: 1px solid $border-color;
|
||||||
|
|||||||
Reference in New Issue
Block a user