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 SessionListItem from "./SessionListItem.vue";
|
||||
import DrawerPanel from "./DrawerPanel.vue";
|
||||
import OutlinePanel from "./OutlinePanel.vue";
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const appStore = useAppStore();
|
||||
@@ -33,6 +34,7 @@ const { t } = useI18n();
|
||||
|
||||
const showDrawer = ref(false);
|
||||
const drawerActiveTab = ref<"terminal" | "files">("files");
|
||||
const showOutline = ref(false);
|
||||
|
||||
const currentMode = ref<"chat" | "live">("chat");
|
||||
|
||||
@@ -988,6 +990,30 @@ async function handleSessionModelCustomSubmit() {
|
||||
<div class="header-actions">
|
||||
<!-- chat/live mode toggle hidden -->
|
||||
<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">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
@@ -1042,7 +1068,12 @@ async function handleSessionModelCustomSubmit() {
|
||||
</header>
|
||||
|
||||
<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 class="approval-icon" aria-hidden="true">
|
||||
<svg
|
||||
@@ -1637,6 +1668,21 @@ async function handleSessionModelCustomSubmit() {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -19,8 +19,10 @@ import { downloadFile, getDownloadUrl } from '@/api/hermes/download'
|
||||
const props = withDefaults(defineProps<{
|
||||
content: string
|
||||
mentionNames?: string[]
|
||||
headingIdPrefix?: string
|
||||
}>(), {
|
||||
mentionNames: () => [],
|
||||
headingIdPrefix: '',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -68,6 +70,27 @@ function normalizeLocalFilePath(path: string): string {
|
||||
const renderedHtml = computed(() => {
|
||||
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
|
||||
html = html.replace(/\bsrc=(["'])([^"']+)\1/g, (match, quote, path) => {
|
||||
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_TRUNCATED_KEY = "__truncated__";
|
||||
|
||||
const props = defineProps<{ message: Message; highlight?: boolean }>();
|
||||
const props = defineProps<{ message: Message; highlight?: boolean; headingIdPrefix?: string }>();
|
||||
const { t } = useI18n();
|
||||
const toast = useMessage();
|
||||
|
||||
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 isCommandError = computed(() => props.message.role === "command" && props.message.systemType === "error");
|
||||
const isStatusCommand = computed(() => isCommandMessage.value && props.message.commandAction === "status");
|
||||
@@ -868,6 +870,7 @@ onBeforeUnmount(() => {
|
||||
<MarkdownRenderer
|
||||
v-if="parsedThinking.body && message.role === 'assistant'"
|
||||
:content="parsedThinking.body"
|
||||
:heading-id-prefix="effectiveHeadingIdPrefix"
|
||||
/>
|
||||
|
||||
<!-- Render user message content -->
|
||||
@@ -915,6 +918,7 @@ onBeforeUnmount(() => {
|
||||
<MarkdownRenderer
|
||||
v-if="message.role === 'assistant' && message.content && !parsedThinking.body"
|
||||
:content="message.content"
|
||||
:heading-id-prefix="effectiveHeadingIdPrefix"
|
||||
/>
|
||||
|
||||
<!-- 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',
|
||||
contextEditFailed: 'Aktualisierung fehlgeschlagen',
|
||||
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)',
|
||||
slashCommandArgs: {
|
||||
message: '<Nachricht>',
|
||||
|
||||
@@ -133,6 +133,9 @@ export default {
|
||||
contextEditFailed: 'Update failed',
|
||||
emptyState: 'Start a conversation with Hermes Agent',
|
||||
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)',
|
||||
slashCommandArgs: {
|
||||
message: '<message>',
|
||||
|
||||
@@ -120,6 +120,9 @@ export default {
|
||||
contextEditSuccess: 'Longitud del contexto actualizada',
|
||||
contextEditFailed: 'Error en la actualización',
|
||||
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)',
|
||||
slashCommandArgs: {
|
||||
message: '<mensaje>',
|
||||
|
||||
@@ -120,6 +120,9 @@ export default {
|
||||
contextEditSuccess: 'Longueur du contexte mise à jour',
|
||||
contextEditFailed: 'Échec de la mise à jour',
|
||||
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)',
|
||||
slashCommandArgs: {
|
||||
message: '<message>',
|
||||
|
||||
@@ -120,6 +120,9 @@ export default {
|
||||
contextEditSuccess: 'コンテキスト長を更新しました',
|
||||
contextEditFailed: '更新に失敗しました',
|
||||
emptyState: 'Hermes Agent と会話を開始しましょう',
|
||||
outlineTitle: '会話アウトライン',
|
||||
outlineEmpty: '会話内容はありません',
|
||||
outlineUserQuestion: 'ユーザーの質問',
|
||||
inputPlaceholder: 'メッセージを入力... (Enter で送信、Shift+Enter で改行)',
|
||||
slashCommandArgs: {
|
||||
message: '<メッセージ>',
|
||||
|
||||
@@ -120,6 +120,9 @@ export default {
|
||||
contextEditSuccess: '컨텍스트 길이가 업데이트되었습니다',
|
||||
contextEditFailed: '업데이트 실패',
|
||||
emptyState: 'Hermes Agent와 대화를 시작하세요',
|
||||
outlineTitle: '대화 개요',
|
||||
outlineEmpty: '대화 내용이 없습니다',
|
||||
outlineUserQuestion: '사용자 질문',
|
||||
inputPlaceholder: '메시지를 입력하세요... (Enter로 전송, Shift+Enter로 줄바꿈)',
|
||||
slashCommandArgs: {
|
||||
message: '<메시지>',
|
||||
|
||||
@@ -120,6 +120,9 @@ export default {
|
||||
contextEditSuccess: 'Tamanho do contexto atualizado',
|
||||
contextEditFailed: 'Falha na atualização',
|
||||
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)',
|
||||
slashCommandArgs: {
|
||||
message: '<mensagem>',
|
||||
|
||||
@@ -132,6 +132,9 @@ export default {
|
||||
contextEditSuccess: '上下文長度已更新',
|
||||
contextEditFailed: '更新失敗',
|
||||
emptyState: '開始與 Hermes Agent 對話',
|
||||
outlineTitle: '會話大綱',
|
||||
outlineEmpty: '暫無會話內容',
|
||||
outlineUserQuestion: '使用者問題',
|
||||
inputPlaceholder: '輸入訊息... (Enter 發送,Shift+Enter 換行)',
|
||||
slashCommandArgs: {
|
||||
message: '<訊息>',
|
||||
|
||||
@@ -133,6 +133,9 @@ export default {
|
||||
contextEditFailed: '更新失败',
|
||||
emptyState: '开始与 Hermes Agent 对话',
|
||||
cliEmptyState: '开始 CLI 对话',
|
||||
outlineTitle: '会话大纲',
|
||||
outlineEmpty: '暂无会话内容',
|
||||
outlineUserQuestion: '用户问题',
|
||||
inputPlaceholder: '输入消息... (Enter 发送,Shift+Enter 换行)',
|
||||
slashCommandArgs: {
|
||||
message: '<消息>',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getSourceLabel } from '@/shared/session-display'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import HistoryMessageList from '@/components/hermes/chat/HistoryMessageList.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'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
@@ -26,6 +27,7 @@ const hermesSessionsLoaded = ref(false)
|
||||
// History page's own selected session (independent from chatStore)
|
||||
const historySessionId = ref<string | null>(null)
|
||||
const historySession = ref<Session | null>(null)
|
||||
const showOutline = ref(false)
|
||||
|
||||
async function loadHermesSessions() {
|
||||
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>
|
||||
</div>
|
||||
<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">
|
||||
<template #trigger>
|
||||
<NButton quaternary size="small" @click="copySessionId()" circle>
|
||||
@@ -353,7 +365,12 @@ async function copySessionId(id?: string) {
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
@@ -367,6 +384,21 @@ async function copySessionId(id?: string) {
|
||||
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 {
|
||||
width: 220px;
|
||||
border-right: 1px solid $border-color;
|
||||
|
||||
Reference in New Issue
Block a user