sessionOutline.ts 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import type { UiMessage } from '@/components/messages/types'
  2. export type SessionOutlineItem = {
  3. id: string
  4. type: 'user' | 'heading'
  5. content: string
  6. messageId: string
  7. level: 0 | 1 | 2 | 3
  8. anchorId: string
  9. createdAt?: UiMessage['createdAt']
  10. }
  11. function cleanMessageText(value: string): string {
  12. return value.replace(/<think>[\s\S]*?<\/think>/gi, '').trim()
  13. }
  14. function userQuestion(message: UiMessage): string {
  15. const firstLine = cleanMessageText(message.content).split('\n').map(line => line.trim()).find(Boolean) || '用户消息'
  16. return firstLine.length > 50 ? `${firstLine.slice(0, 50)}…` : firstLine
  17. }
  18. function assistantHeadings(message: UiMessage): SessionOutlineItem[] {
  19. const result: SessionOutlineItem[] = []
  20. let headingIndex = 0
  21. let fence = ''
  22. for (const line of cleanMessageText(message.content).split('\n')) {
  23. const trimmed = line.trim()
  24. const fenceMatch = trimmed.match(/^(```+|~~~+)/)
  25. if (fenceMatch) {
  26. fence = fence ? '' : fenceMatch[1]![0]!
  27. continue
  28. }
  29. if (fence) continue
  30. const match = trimmed.match(/^(#{1,3})\s+(.+?)\s*#*$/)
  31. if (!match) continue
  32. headingIndex += 1
  33. const level = match[1]!.length as 1 | 2 | 3
  34. result.push({
  35. id: `outline-${message.id}-heading-${headingIndex}`,
  36. type: 'heading',
  37. content: match[2]!.trim(),
  38. messageId: message.id,
  39. level,
  40. anchorId: `outline-${message.id}-heading-${headingIndex}`,
  41. createdAt: message.createdAt,
  42. })
  43. }
  44. return result
  45. }
  46. /** Builds the loaded-session outline from user turns and assistant Markdown headings. */
  47. export function buildSessionOutline(messages: UiMessage[]): SessionOutlineItem[] {
  48. const visible = messages.filter(message => (message.role === 'user' || message.role === 'assistant') && !message.timelineKind)
  49. const items: SessionOutlineItem[] = []
  50. for (let index = 0; index < visible.length; index += 1) {
  51. const message = visible[index]!
  52. if (message.role !== 'user') continue
  53. items.push({
  54. id: `outline-user-${message.id}`,
  55. type: 'user',
  56. content: userQuestion(message),
  57. messageId: message.id,
  58. level: 0,
  59. anchorId: `message-${message.id}`,
  60. createdAt: message.createdAt,
  61. })
  62. for (let replyIndex = index + 1; replyIndex < visible.length && visible[replyIndex]?.role !== 'user'; replyIndex += 1) {
  63. items.push(...assistantHeadings(visible[replyIndex]!))
  64. }
  65. }
  66. return items
  67. }