会话列表

会话列表是应用自己渲染的 SessionList 组件,不由适配器提供:会话目录(有哪些会话、标题、新建、改名、删除)由应用维护,可来自业务后端;SessionList 只展示传入的数组并调用应用的回调。当前会话 ID 由路由或状态传给 useChatSessions 的 activeSessionId,切换后原会话继续在后台接收回答。

sessions.tsx
import { useDemoSessions } from '../sessions';

import { Chatkit, SessionList } from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import '@volcengine/chatkit/styles.css';
import { useState } from 'react';

const adapter = createMockAdapter();

type Placement = 'left' | 'right' | 'drawer';
const PLACEMENTS: { id: Placement; label: string }[] = [
  { id: 'left', label: '列表在左' },
  { id: 'right', label: '列表在右' },
  { id: 'drawer', label: '收进抽屉' },
];

// 会话列表是单独的组件,和聊天区用同一个 useChatSessions 的返回值;摆在哪由页面自己的布局决定。
// 切换会话不打断回答:发一句话后马上切到别的会话,列表里原来那一行显示运行中。
export default function Sessions() {
  const { chat: sessions, listProps } = useDemoSessions({ adapter });
  const [placement, setPlacement] = useState<Placement>('left');
  const [open, setOpen] = useState(false);
  return (
    <div className="demo-column demo-session-layout" data-placement={placement} data-open={open}>
      <div className="demo-bar">
        {/* 窄屏统一用抽屉;宽屏可以比较三种布局。 */}
        {PLACEMENTS.map(item => (
          <label key={item.id} className="demo-session-placement">
            <input
              type="radio"
              checked={placement === item.id}
              onChange={() => {
                setPlacement(item.id);
                setOpen(false);
              }}
            />{' '}
            {item.label}
          </label>
        ))}
        <button
          type="button"
          className="demo-chip demo-session-toggle"
          aria-expanded={open}
          aria-controls="demo-sessions"
          onClick={() => setOpen(value => !value)}
        >
          {open ? '收起会话' : '全部会话'}
        </button>
      </div>
      <div className="demo-row">
        <div id="demo-sessions" className="demo-session-sidebar">
          <SessionList {...listProps} style={{ width: '100%' }} />
        </div>
        <div className="demo-main">
          <Chatkit sessions={sessions} settings={{ welcome: '发一句话试试' }} />
        </div>
      </div>
    </div>
  );
}
外部目录与多会话运行时新窗口打开
const [items, setItems] = useState<SessionInfo[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string>();
const chat = useChatSessions({ adapter, activeSessionId });
const opened = useSyncExternalStore(chat.store.subscribe, chat.store.getSnapshot, chat.store.getSnapshot);
const visibleItems = items.map(item => ({
  ...item,
  status: opened.openedSessions.find(open => open.id === item.id)?.status ?? item.status,
}));

<SessionList sessions={visibleItems} activeSessionId={activeSessionId} onSelect={setActiveSessionId} />;
<Chatkit sessions={chat} />;
  • 不提供 onCreate、onRename、onRemove、onLoadMore 时隐藏对应入口;fork、收藏、归档等业务操作用 itemActions / itemBadge 加。
  • 不需要会话列表时不渲染 SessionList 即可,它不在 features 里。
  • 只用 useChatRuntime(单个对话)时一般不放会话列表;用它切换 sessionId 会释放旧运行时。

会话状态

已打开会话的实时状态来自 chat.store.getSnapshot().openedSessions。把它按 id 合并到应用维护的目录后再传给 SessionList。

session-statuses.tsx
import '@volcengine/chatkit/styles.css';

import { type ChatkitSessionView, SessionList } from '@volcengine/chatkit';

const sessions = [
  { id: 'running', title: '正在生成回答', status: 'running' },
  { id: 'completed', title: '周报已生成,等待查看', status: 'completed' },
  { id: 'error', title: '数据同步失败,等待查看', status: 'error' },
  { id: 'idle', title: '普通空闲会话', status: 'idle', updatedAt: Date.now() },
  { id: 'unknown', title: '历史状态待确认', status: 'unknown' },
] satisfies readonly ChatkitSessionView[];

export default function SessionStatuses() {
  return (
    <div style={{ width: 280, maxWidth: '100%', height: '100%' }}>
      <SessionList sessions={sessions} activeSessionId="idle" onSelect={() => undefined} />
    </div>
  );
}
五种会话状态;悬停图标查看说明新窗口打开
状态含义列表展示
running存在运行中的 Run,回答还没有完全返回蓝色空心呼吸环
completed后台最后一轮正常结束,用户还没有查看蓝色实心圆点
error后台最后一轮失败,用户还没有查看红色问号
idle没有运行中的 Run,也没有未查看的结束结果不显示状态图标;有时间时显示相对时间
unknown尚不能确认状态,例如会话历史仍在初始化黄色感叹号

选中 completed 或 error 会确认结果并变为 idle;选中仍在运行的会话不会清除 running。当前正在查看的会话结束时直接进入 idle。状态图标悬停时显示文字说明。

只有已经打开的 runtime 才会进入 openedSessions。尚未打开的目录项使用应用传入的 status。status 为 idle 或没有传入时,列表不显示状态图标,并在提供 updatedAt / createdAt 时显示相对时间;其他状态用图标替代时间。未查看状态默认只存在于当前页面内存中,需要跨刷新或跨设备保留时由应用持久化。

这里的会话状态用于列表展示,不等同于单轮执行的 run.status。run.status 还包含 starting、attaching、interrupting、failed、interrupted 等执行细节;useChatSessions 会结合当前活跃 Run、最近一轮结果、用户是否已经查看以及后端会话摘要,归并成上面的五种列表状态。应用通常应直接使用 openedSessions[].status,不要自行把某一个 run.status 当成会话状态。

标题、重命名和删除

  • 标题溢出时保持单行省略,鼠标悬停后横向滚动;系统开启“减少动态效果”时不播放滚动动画。
  • 提供 onRename 后,可从行菜单选择“重命名”,也可双击标题进入同一个行内编辑框。回车或失焦提交,Esc 取消;空标题和没有变化的标题不会调用回调。
  • 提供 onRemove 后,删除入口会先打开独立的确认弹层,不会撑开会话行。回调返回 false 或抛错时保留该会话。
  • 对话流等无关区域滚动不会关闭会话菜单;页面或菜单触发元素所在的滚动容器移动时会关闭,避免弹层脱离触发位置。

列表筛选掉一个会话不意味着它已被删除,也不会释放其 runtime。SDK 不回写标题或目录数组;应用根据业务接口或运行事件刷新元数据。

完整的 props、useChatSessions 返回值、新建/删除/fork 的做法和历史加载见会话与历史从哪来。

列表的主题与样式

SessionList 自建 .chatkit-wrapper,className/style 直接作用于这个 wrapper。它不要求 Chatkit 或 Provider;无 Provider 时跟随最近 DOM 祖先的 data-chatkit-theme,有 Provider 时先使用 Provider 的 theme.appearance。列表没有单独的 theme prop。

<div data-chatkit-theme="dark">
  <SessionList
    sessions={items}
    activeSessionId={activeSessionId}
    onSelect={setActiveSessionId}
    className="my-session-list"
  />
</div>
.chatkit-wrapper.my-session-list {
  --chatkit-color-bg: #182532;
  --chatkit-color-bg-hover: #26394b;
}

即使放在 Provider 内,也必须传 sessions 数组和 onSelect;Provider 的 sessions 是会话运行时管理对象,不是目录数组。空数组时 wrapper 仍在;宿主条件卸载列表后再挂载,会重新解析主题。Provider 的 root 或相邻 Chatkit 的品牌色覆写不会自动复制到列表。列表行和操作菜单读取本列表的 token,覆写时请直接命中列表自己的 wrapper,见主题与样式覆盖。