Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/web-split-conversation-toc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Extract the beta conversation outline (table of contents) into its own component.
213 changes: 11 additions & 202 deletions apps/kimi-web/src/components/ConversationPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ChatHeader from './ChatHeader.vue';
import Composer from './Composer.vue';
import SwarmCard from './SwarmCard.vue';
import ChatDock from './ChatDock.vue';
import ConversationToc, { type ConversationTocItem } from './ConversationToc.vue';
import { getVisibleWorkspaces } from '../lib/workspacePicker';
import { safeRemove, STORAGE_KEYS } from '../lib/storage';

Expand Down Expand Up @@ -235,13 +236,6 @@ watch(hasDockWork, (hasWork) => {
if (!hasWork) closeDockPanel();
});

interface ConversationTocItem {
id: string;
role: ChatTurn['role'];
no: number;
title: string;
}

function tocTitle(turn: ChatTurn): string {
if (turn.role === 'compaction') return t('conversation.compactedPlain');
if (turn.role === 'user') {
Expand Down Expand Up @@ -304,11 +298,6 @@ const tocTotalHeight = computed(() =>

const activeTurnId = ref<string | null>(null);
const tocViewport = ref<{ top: number; height: number } | null>(null);
const tooltip = ref<{ visible: boolean; text: string; top: number }>({
visible: false,
text: '',
top: 0,
});

function updateTocViewport(): void {
const pane = panesRef.value;
Expand Down Expand Up @@ -341,22 +330,6 @@ function updateTocViewport(): void {
};
}

function showTooltip(text: string, event: MouseEvent): void {
const target = event.currentTarget as HTMLElement | null;
if (!target) return;
tooltip.value = { visible: true, text, top: target.offsetTop };
}

function hideTooltip(): void {
tooltip.value.visible = false;
}

const showConversationToc = computed(() =>
!props.mobile &&
!props.sessionLoading &&
conversationTocItems.value.length > 1,
);

// The first pending question (if any)
const pendingQuestion = computed<UIQuestion | undefined>(() =>
props.questions && props.questions.length > 0 ? props.questions[0] : undefined,
Expand Down Expand Up @@ -905,42 +878,16 @@ defineExpose({ loadComposerForEdit });
/>

<!-- Beta conversation outline: right edge, proportional bubbles, viewport indicator, hover tooltip. -->
<nav
v-if="showConversationToc && betaToc"
class="conversation-toc"
:aria-label="t('conversation.toc')"
>
<div class="toc-track">
<button
v-for="(item, index) in conversationTocItems"
:key="item.id"
type="button"
class="toc-bubble"
:class="[item.role, { active: activeTurnId === item.id }]"
:style="{ height: tocMetrics[index]?.height + 'px' }"
:aria-label="`#${item.no} ${item.title}`"
@mouseenter="(e: MouseEvent) => showTooltip(item.title, e)"
@mouseleave="hideTooltip"
@click="scrollToTurn(item.id)"
>
<span class="toc-no">{{ item.no }}</span>
</button>
<div
v-if="tocViewport"
class="toc-viewport"
:style="{ top: tocViewport.top + 'px', height: tocViewport.height + 'px' }"
/>
</div>
<Transition name="toc-tip">
<div
v-show="tooltip.visible"
class="toc-tooltip"
:style="{ top: tooltip.top + 'px' }"
>
{{ tooltip.text }}
</div>
</Transition>
</nav>
<ConversationToc
v-if="betaToc"
:items="conversationTocItems"
:metrics="tocMetrics"
:active-turn-id="activeTurnId"
:viewport="tocViewport"
:mobile="mobile"
:session-loading="sessionLoading"
@select="scrollToTurn"
/>

<div class="chat-layout">
<div
Expand Down Expand Up @@ -1245,144 +1192,6 @@ defineExpose({ loadComposerForEdit });
min-width: 0;
}
}
.conversation-toc {
position: absolute;
z-index: 8;
display: flex;
flex-direction: column;
padding: 0;
top: 86px;
bottom: auto;
left: calc(50% + (var(--read-max) / 2) + 8px);
width: 46px;
max-height: calc(100% - 86px - 130px);
opacity: 0.45;
transition: opacity 0.18s ease;
}
.conversation-toc:hover {
opacity: 1;
}
.toc-track {
flex: none;
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
padding: 6px 4px;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: none;
max-height: 100%;
position: relative;
}
.toc-track::-webkit-scrollbar {
display: none;
}
.toc-bubble {
appearance: none;
position: relative;
flex-shrink: 0;
border: 0;
padding: 0;
width: 34px;
border-radius: 8px;
background: transparent;
cursor: pointer;
opacity: 0.85;
transition: opacity 0.14s ease, transform 0.14s ease, box-shadow 0.14s ease;
}
.toc-bubble.active {
opacity: 1;
}
.toc-bubble:hover,
.toc-bubble:focus-visible {
opacity: 1;
transform: translateX(2px) scale(1.05);
outline: none;
}
.toc-bubble.user {
background: var(--blue);
box-shadow: none;
}
.toc-bubble.assistant {
background: var(--panel2);
box-shadow: inset 0 0 0 1px var(--line);
}
.toc-bubble.compaction {
height: 10px;
background: transparent;
box-shadow: inset 0 0 0 1px var(--faint);
border-radius: 999px;
}
.toc-bubble.active::after {
content: '';
position: absolute;
inset: -2px;
border: 2px solid var(--blue);
border-radius: 10px;
pointer-events: none;
opacity: 0.35;
}
.toc-no {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.toc-viewport {
position: absolute;
left: 0;
right: 0;
background: color-mix(in srgb, var(--blue) 10%, transparent);
pointer-events: none;
border-radius: 4px;
z-index: 0;
}
.toc-tooltip {
position: absolute;
right: calc(100% + 8px);
top: 0;
z-index: 20;
max-width: 240px;
padding: 6px 10px;
background: var(--bg);
color: var(--ink);
border: 1px solid var(--line);
border-radius: 8px;
font-size: var(--ui-font-size-xs);
line-height: 1.45;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}
.toc-tooltip::before {
content: '';
position: absolute;
left: auto;
right: -5px;
top: 10px;
border-width: 5px 0 5px 5px;
border-style: solid;
border-color: transparent transparent transparent var(--bg);
}
.toc-tip-enter-active,
.toc-tip-leave-active {
transition: opacity 0.12s ease, transform 0.12s ease;
}
.toc-tip-enter-from,
.toc-tip-leave-to {
opacity: 0;
transform: translateX(4px);
}
@container (max-width: 920px) {
.conversation-toc {
display: none;
}
}
.swarm-stack {
padding: 0 18px 16px;
}
Expand Down
Loading
Loading