@tablestore/tablestore-agent-cli 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/dashboard-runtime/dashboard/.next/BUILD_ID +1 -1
  2. package/dist/dashboard-runtime/dashboard/.next/build-manifest.json +3 -3
  3. package/dist/dashboard-runtime/dashboard/.next/prerender-manifest.json +3 -3
  4. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/chat/page.js.nft.json +1 -1
  5. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/chat/page_client-reference-manifest.js +1 -1
  6. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/documents/page.js.nft.json +1 -1
  7. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/documents/page_client-reference-manifest.js +1 -1
  8. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/page.js.nft.json +1 -1
  9. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/page_client-reference-manifest.js +1 -1
  10. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/qa/page.js.nft.json +1 -1
  11. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/qa/page_client-reference-manifest.js +1 -1
  12. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/retrieval/page.js.nft.json +1 -1
  13. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/retrieval/page_client-reference-manifest.js +1 -1
  14. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/settings/page.js.nft.json +1 -1
  15. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/settings/page_client-reference-manifest.js +1 -1
  16. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/create/page_client-reference-manifest.js +1 -1
  17. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/page.js.nft.json +1 -1
  18. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/page_client-reference-manifest.js +1 -1
  19. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/[id]/page_client-reference-manifest.js +1 -1
  20. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/new/page_client-reference-manifest.js +1 -1
  21. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/page.js.nft.json +1 -1
  22. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/page_client-reference-manifest.js +1 -1
  23. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/messages/page.js.nft.json +1 -1
  24. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/messages/page_client-reference-manifest.js +1 -1
  25. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/page.js.nft.json +1 -1
  26. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/page_client-reference-manifest.js +1 -1
  27. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/requests/page.js.nft.json +1 -1
  28. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/requests/page_client-reference-manifest.js +1 -1
  29. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/search/page.js.nft.json +1 -1
  30. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/search/page_client-reference-manifest.js +1 -1
  31. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/settings/page.js.nft.json +1 -1
  32. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/settings/page_client-reference-manifest.js +1 -1
  33. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/units/page.js.nft.json +1 -1
  34. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/units/page_client-reference-manifest.js +1 -1
  35. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/create/page_client-reference-manifest.js +1 -1
  36. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/page.js.nft.json +1 -1
  37. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/page_client-reference-manifest.js +1 -1
  38. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/projects/page.js.nft.json +1 -1
  39. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/projects/page_client-reference-manifest.js +1 -1
  40. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.html +1 -1
  41. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.rsc +1 -1
  42. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  43. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  44. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  45. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  46. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  48. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.html +1 -1
  49. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.rsc +2 -2
  50. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  51. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  52. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  53. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  54. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  55. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  56. package/dist/dashboard-runtime/dashboard/.next/server/app/api/v1/knowledge-bases/[name]/documents/upload/route.js.nft.json +1 -1
  57. package/dist/dashboard-runtime/dashboard/.next/server/app/index.html +1 -1
  58. package/dist/dashboard-runtime/dashboard/.next/server/app/index.rsc +2 -2
  59. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  60. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_full.segment.rsc +2 -2
  61. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  62. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
  63. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  64. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  65. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.html +1 -1
  66. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.rsc +3 -3
  67. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_full.segment.rsc +3 -3
  68. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_head.segment.rsc +1 -1
  69. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_index.segment.rsc +2 -2
  70. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_tree.segment.rsc +2 -2
  71. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +2 -2
  72. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/onboarding.segment.rsc +1 -1
  73. package/dist/dashboard-runtime/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  74. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/0b1r_app_(dashboard)_knowledge-base_[name]_documents_documents-client_tsx_0g2uxmt._.js +1 -1
  75. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__09_30hi._.js +3 -0
  76. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0kaiok7._.js +3 -0
  77. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0l937k4._.js +3 -0
  78. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0-dlo4s._.js +1 -1
  79. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_01pp~xx._.js +3 -0
  80. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0476v7r._.js → _08j6p-z._.js} +2 -2
  81. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0ct0n55._.js → _09eom0_._.js} +2 -2
  82. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0mzxmfh._.js → _0f-may2._.js} +2 -2
  83. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0fp_nnb._.js +3 -0
  84. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0l6jz-5._.js +3 -0
  85. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0fg6iap._.js → _0ntv66s._.js} +2 -2
  86. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0sc0tpz._.js +3 -0
  87. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0tyxwr3._.js +3 -0
  88. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0u9md36._.js +1 -1
  89. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/dashboard_src_065m1.j._.js +1 -1
  90. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/dashboard_src_app_(dashboard)_knowledge-base_[name]_layout-client_tsx_05momc5._.js +1 -1
  91. package/dist/dashboard-runtime/dashboard/.next/server/middleware-build-manifest.js +3 -3
  92. package/dist/dashboard-runtime/dashboard/.next/server/pages/404.html +1 -1
  93. package/dist/dashboard-runtime/dashboard/.next/server/pages/500.html +1 -1
  94. package/dist/dashboard-runtime/dashboard/.next/server/server-reference-manifest.js +1 -1
  95. package/dist/dashboard-runtime/dashboard/.next/server/server-reference-manifest.json +1 -1
  96. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0-xqk6wfme5fu.js +1 -0
  97. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{0ltmkqogn8g3t.js → 00.fyg_t3xjc~.js} +1 -1
  98. package/dist/dashboard-runtime/dashboard/.next/static/chunks/01k-.wkndubwy.js +138 -0
  99. package/dist/dashboard-runtime/dashboard/.next/static/chunks/07r22x5uhjxjb.js +1 -0
  100. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0cbu3udngtx-v.js +4 -0
  101. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0h7d69g2fdgs6.js +138 -0
  102. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0iv0g360z0ja5.js +1 -0
  103. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0jf4ytf.izmzh.js +1 -0
  104. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{17nx_vdk_qemm.js → 0m42mmyxmgn-i.js} +1 -1
  105. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0odz1whf-ozqn.css +1 -0
  106. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0ouxv_2qvf6.9.js +1 -0
  107. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0rbkgj._oqsb3.js +1 -0
  108. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0u6u.649ou67b.js +1 -0
  109. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0vf7~eysvh9bq.js +1 -0
  110. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{09qsmnfvk8dm3.js → 0wmaq.ay~r2sn.js} +1 -1
  111. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{0i5aqiffpm9jr.js → 0xb0_9t-vmo6e.js} +1 -1
  112. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0yp_2ea75d19v.js +1 -0
  113. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{0s3mvx9_dr13c.js → 11cizim1aug~b.js} +1 -1
  114. package/dist/dashboard-runtime/dashboard/.next/static/chunks/13-2ei295hl0-.js +1 -0
  115. package/dist/dashboard-runtime/dashboard/src/app/(dashboard)/knowledge-base/[name]/qa/page.tsx +23 -1
  116. package/dist/dashboard-runtime/dashboard/src/components/chat/chat-mount-config.tsx +29 -8
  117. package/dist/dashboard-runtime/dashboard/src/components/documents/doc-upload-dialog.tsx +370 -91
  118. package/dist/dashboard-runtime/dashboard/src/components/knowledge-base/kb-card.tsx +8 -5
  119. package/dist/dashboard-runtime/dashboard/src/components/knowledge-base/kb-header.tsx +23 -13
  120. package/dist/dashboard-runtime/dashboard/src/components/memory/memory-header.tsx +8 -12
  121. package/dist/dashboard-runtime/dashboard/src/components/qa/qa-chat-panel.tsx +45 -4
  122. package/dist/dashboard-runtime/dashboard/src/components/qa/qa-message.tsx +636 -36
  123. package/dist/dashboard-runtime/dashboard/src/components/shared/confirm-dialog.tsx +8 -1
  124. package/dist/dashboard-runtime/dashboard/src/components/shared/refresh-button.tsx +102 -86
  125. package/dist/dashboard-runtime/dashboard/src/hooks/use-qa-chat.ts +261 -58
  126. package/dist/dashboard-runtime/dashboard/src/lib/chat-message-ops.ts +76 -0
  127. package/dist/dashboard-runtime/dashboard/src/lib/dev-flags.ts +24 -0
  128. package/dist/dashboard-runtime/dashboard/src/types/llm-chat.ts +19 -0
  129. package/package.json +1 -1
  130. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0i0y9.r._.js +0 -3
  131. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0r_6wpx._.js +0 -3
  132. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0y8rrcu._.js +0 -3
  133. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_02e0c00._.js +0 -3
  134. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0doczxg._.js +0 -3
  135. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0e9y.xc._.js +0 -3
  136. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0ti~om_._.js +0 -3
  137. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0~s~e-0._.js +0 -3
  138. package/dist/dashboard-runtime/dashboard/.next/static/chunks/055_bo6_3nt-s.js +0 -1
  139. package/dist/dashboard-runtime/dashboard/.next/static/chunks/07l4dodn5t6l4.js +0 -4
  140. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0a5ass8u78k90.js +0 -1
  141. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0j~63ld9lbb25.js +0 -1
  142. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0l6znnjqw.4yw.js +0 -1
  143. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0o-c2g8y5pw-c.js +0 -1
  144. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0o0i2ke2jbdvq.js +0 -138
  145. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0prl6hk4v0k3i.js +0 -1
  146. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0u2lo2lzizxy_.js +0 -1
  147. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0v.hno.6q74nn.css +0 -1
  148. package/dist/dashboard-runtime/dashboard/.next/static/chunks/14ti2gx~1fiub.js +0 -1
  149. package/dist/dashboard-runtime/dashboard/.next/static/chunks/14vqmdm7sosj2.js +0 -1
  150. package/dist/dashboard-runtime/dashboard/.next/static/chunks/17h~onnkksf44.js +0 -138
  151. package/dist/dashboard-runtime/dashboard/.next/static/chunks/186jubvt5xyuj.js +0 -1
  152. /package/dist/dashboard-runtime/dashboard/.next/static/{MjeKF5uLzEvAqWkR9O6Ts → BGnMIk4OPjFx_qvLFMTBc}/_buildManifest.js +0 -0
  153. /package/dist/dashboard-runtime/dashboard/.next/static/{MjeKF5uLzEvAqWkR9O6Ts → BGnMIk4OPjFx_qvLFMTBc}/_clientMiddlewareManifest.js +0 -0
  154. /package/dist/dashboard-runtime/dashboard/.next/static/{MjeKF5uLzEvAqWkR9O6Ts → BGnMIk4OPjFx_qvLFMTBc}/_ssgManifest.js +0 -0
@@ -1,13 +1,30 @@
1
1
  "use client";
2
2
 
3
- import { memo, useState } from "react";
4
- import { AlertCircle, BookOpen, Brain, ChevronDown, Search } from "lucide-react";
3
+ import { memo, useCallback, useEffect, useState } from "react";
4
+ import {
5
+ AlertCircle,
6
+ AlertTriangle,
7
+ BookOpen,
8
+ Brain,
9
+ ChevronDown,
10
+ Copy,
11
+ Pencil,
12
+ RotateCcw,
13
+ Search,
14
+ Send,
15
+ Trash2,
16
+ } from "lucide-react";
17
+ import { toast } from "sonner";
5
18
 
19
+ import { Button } from "@/components/ui/button";
20
+ import { Checkbox } from "@/components/ui/checkbox";
6
21
  import {
7
22
  Collapsible,
8
23
  CollapsibleContent,
9
24
  CollapsibleTrigger,
10
25
  } from "@/components/ui/collapsible";
26
+ import { Textarea } from "@/components/ui/textarea";
27
+ import { ConfirmDialog } from "@/components/shared/confirm-dialog";
11
28
  import { cn } from "@/lib/utils";
12
29
  import type {
13
30
  ChatMessage,
@@ -19,48 +36,467 @@ import { Markdown } from "./markdown";
19
36
 
20
37
  interface QaMessageProps {
21
38
  message: ChatMessage;
39
+ /** Whether ANY turn in the session is currently streaming. All action
40
+ * buttons disable during streaming to avoid racing the message list
41
+ * against in-flight SSE flushes. */
42
+ busy: boolean;
43
+ onDelete(id: string, opts?: { withNextAssistant?: boolean }): void;
44
+ onResend(userId: string): void;
45
+ onRegenerate(assistantId: string): void;
46
+ onEditSave(userId: string, nextContent: string): void;
22
47
  }
23
48
 
24
- function QaMessageImpl({ message }: QaMessageProps) {
49
+ function QaMessageImpl({
50
+ message,
51
+ busy,
52
+ onDelete,
53
+ onResend,
54
+ onRegenerate,
55
+ onEditSave,
56
+ }: QaMessageProps) {
25
57
  if (message.role === "user") {
26
- return <UserBubble content={message.content} />;
58
+ return (
59
+ <UserBubble
60
+ message={message}
61
+ busy={busy}
62
+ onDelete={onDelete}
63
+ onResend={onResend}
64
+ onEditSave={onEditSave}
65
+ />
66
+ );
27
67
  }
28
- return <AssistantBlock message={message} />;
68
+ return (
69
+ <AssistantBlock
70
+ message={message}
71
+ busy={busy}
72
+ onDelete={onDelete}
73
+ onRegenerate={onRegenerate}
74
+ />
75
+ );
29
76
  }
30
77
 
31
78
  /**
32
79
  * Memoised so that streaming a delta to message N does not re-render
33
80
  * messages 0..N-1. Reference equality on the message object is enough
34
81
  * because useQaChat replaces the touched message via map() and keeps
35
- * untouched ones identical.
82
+ * untouched ones identical. The action callbacks are stable from the
83
+ * parent (chat hook returns stable refs), so we only diff on the message
84
+ * + busy flag.
36
85
  */
37
86
  export const QaMessage = memo(
38
87
  QaMessageImpl,
39
- (prev, next) => prev.message === next.message,
88
+ (prev, next) => prev.message === next.message && prev.busy === next.busy,
40
89
  );
41
90
  QaMessage.displayName = "QaMessage";
42
91
 
43
- function UserBubble({ content }: { content: string }) {
92
+ // ---------------------------------------------------------------------------
93
+ // Hover-revealed action row + tiny button — shared by both bubble variants.
94
+ // ---------------------------------------------------------------------------
95
+
96
+ type MessageActionsProps = {
97
+ align: "left" | "right";
98
+ /** Disable all buttons (e.g. while another turn is streaming). */
99
+ disabled: boolean;
100
+ onCopy: () => void;
101
+ onDelete: () => void;
102
+ /** User-bubble specific. Falsy → button not rendered. */
103
+ onEdit?: () => void;
104
+ onResend?: () => void;
105
+ /** Assistant-bubble specific. */
106
+ onRegenerate?: () => void;
107
+ };
108
+
109
+ /**
110
+ * Hover-revealed icon row beneath a chat bubble. Default state is invisible
111
+ * (`opacity-0`) so the message list reads cleanly at rest; hover/focus on the
112
+ * parent bubble (controlled via `group-hover:` / `group-focus-within:`)
113
+ * fades the row in within 150ms — the ChatGPT/Claude convention. Mobile:
114
+ * parents add `data-touched="true"` after first tap to keep the row visible.
115
+ */
116
+ function MessageActions({
117
+ align,
118
+ disabled,
119
+ onCopy,
120
+ onDelete,
121
+ onEdit,
122
+ onResend,
123
+ onRegenerate,
124
+ }: MessageActionsProps) {
125
+ return (
126
+ <div
127
+ className={cn(
128
+ "mt-1 flex items-center gap-0.5 opacity-0 transition-opacity duration-150",
129
+ "group-hover:opacity-100 group-focus-within:opacity-100",
130
+ "group-data-[touched=true]:opacity-100",
131
+ align === "right" ? "justify-end" : "justify-start",
132
+ )}
133
+ >
134
+ <ActionButton title="复制" disabled={disabled} onClick={onCopy}>
135
+ <Copy className="h-3.5 w-3.5" />
136
+ </ActionButton>
137
+ {onEdit && (
138
+ <ActionButton title="编辑" disabled={disabled} onClick={onEdit}>
139
+ <Pencil className="h-3.5 w-3.5" />
140
+ </ActionButton>
141
+ )}
142
+ {onResend && (
143
+ <ActionButton title="重新发送" disabled={disabled} onClick={onResend}>
144
+ <Send className="h-3.5 w-3.5" />
145
+ </ActionButton>
146
+ )}
147
+ {onRegenerate && (
148
+ <ActionButton
149
+ title="重新生成"
150
+ disabled={disabled}
151
+ onClick={onRegenerate}
152
+ >
153
+ <RotateCcw className="h-3.5 w-3.5" />
154
+ </ActionButton>
155
+ )}
156
+ <ActionButton
157
+ title="删除"
158
+ disabled={disabled}
159
+ onClick={onDelete}
160
+ destructive
161
+ >
162
+ <Trash2 className="h-3.5 w-3.5" />
163
+ </ActionButton>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ function ActionButton({
169
+ title,
170
+ disabled,
171
+ destructive,
172
+ onClick,
173
+ children,
174
+ }: {
175
+ title: string;
176
+ disabled: boolean;
177
+ destructive?: boolean;
178
+ onClick: () => void;
179
+ children: React.ReactNode;
180
+ }) {
181
+ return (
182
+ <Button
183
+ type="button"
184
+ variant="ghost"
185
+ size="icon"
186
+ title={title}
187
+ aria-label={title}
188
+ disabled={disabled}
189
+ onClick={onClick}
190
+ className={cn(
191
+ "h-7 w-7 text-muted-foreground hover:text-foreground",
192
+ destructive && "hover:text-[hsl(var(--destructive))] hover:bg-[hsl(var(--destructive))]/10",
193
+ )}
194
+ >
195
+ {children}
196
+ </Button>
197
+ );
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // In-message warning chips for non-fatal mid-flow failures (rewriter died,
202
+ // some KBs returned errors). Static — these are passive notices, not
203
+ // in-progress state, so no shimmer/pulse to compete with the brand-tinted
204
+ // thinking pill.
205
+ // ---------------------------------------------------------------------------
206
+
207
+ type TurnNoticesProps = {
208
+ rewriteFailed?: boolean;
209
+ rewriteError?: string;
210
+ failedKbCount?: number;
211
+ failedKbDetails?: Array<{ name: string; reason: string }>;
212
+ };
213
+
214
+ function TurnNotices({
215
+ rewriteFailed,
216
+ rewriteError,
217
+ failedKbCount,
218
+ failedKbDetails,
219
+ }: TurnNoticesProps) {
220
+ const hasRewrite = !!rewriteFailed;
221
+ const hasRetrieval = (failedKbCount ?? 0) > 0;
222
+ if (!hasRewrite && !hasRetrieval) return null;
223
+
44
224
  return (
45
- <div className="flex justify-end">
46
- <div className="max-w-[85%] rounded-2xl rounded-tr-sm bg-[hsl(var(--muted)/0.7)] px-4 py-2.5 text-sm leading-relaxed text-foreground whitespace-pre-wrap">
47
- {content}
225
+ <div className="space-y-1.5">
226
+ {hasRewrite && (
227
+ <NoticeChip
228
+ label="意图重写失败 · 已回退到原问题"
229
+ detail={rewriteError ?? "rewriter 返回空结果或抛错"}
230
+ />
231
+ )}
232
+ {hasRetrieval && failedKbDetails && (
233
+ <NoticeChip
234
+ label={`${failedKbCount} 个知识库检索失败`}
235
+ detail={failedKbDetails
236
+ .map((d) => `• ${d.name}: ${d.reason}`)
237
+ .join("\n")}
238
+ />
239
+ )}
240
+ </div>
241
+ );
242
+ }
243
+
244
+ function NoticeChip({ label, detail }: { label: string; detail: string }) {
245
+ const [open, setOpen] = useState(false);
246
+ return (
247
+ <div className="rounded-md border border-amber-500/30 bg-amber-500/5">
248
+ <button
249
+ type="button"
250
+ onClick={() => setOpen((v) => !v)}
251
+ className={cn(
252
+ "flex w-full items-center gap-1.5 px-2 py-1 text-xs",
253
+ "text-amber-700 hover:bg-amber-500/10 dark:text-amber-300",
254
+ )}
255
+ >
256
+ <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
257
+ <span className="flex-1 text-left">{label}</span>
258
+ <ChevronDown
259
+ className={cn(
260
+ "h-3 w-3 opacity-60 transition-transform duration-150",
261
+ open && "rotate-180",
262
+ )}
263
+ />
264
+ </button>
265
+ {open && (
266
+ <pre className="border-t border-amber-500/20 px-2 py-1.5 text-[11px] leading-relaxed text-amber-700/80 dark:text-amber-300/80 whitespace-pre-wrap font-mono">
267
+ {detail}
268
+ </pre>
269
+ )}
270
+ </div>
271
+ );
272
+ }
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // User bubble — copy / edit / resend / delete.
276
+ // ---------------------------------------------------------------------------
277
+
278
+ function UserBubble({
279
+ message,
280
+ busy,
281
+ onDelete,
282
+ onResend,
283
+ onEditSave,
284
+ }: {
285
+ message: ChatMessage;
286
+ busy: boolean;
287
+ onDelete: QaMessageProps["onDelete"];
288
+ onResend: QaMessageProps["onResend"];
289
+ onEditSave: QaMessageProps["onEditSave"];
290
+ }) {
291
+ const [touched, setTouched] = useState(false);
292
+ const [editing, setEditing] = useState(false);
293
+ const [draft, setDraft] = useState(message.content);
294
+ const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
295
+ const [confirmResendOpen, setConfirmResendOpen] = useState(false);
296
+ const [withNextAssistant, setWithNextAssistant] = useState(true);
297
+
298
+ const handleCopy = useCallback(() => {
299
+ void navigator.clipboard.writeText(message.content);
300
+ toast.success("已复制");
301
+ }, [message.content]);
302
+
303
+ const handleSaveEdit = useCallback(() => {
304
+ const trimmed = draft.trim();
305
+ if (!trimmed || trimmed === message.content) {
306
+ setEditing(false);
307
+ setDraft(message.content);
308
+ return;
309
+ }
310
+ setEditing(false);
311
+ onEditSave(message.id, trimmed);
312
+ }, [draft, message.content, message.id, onEditSave]);
313
+
314
+ const handleCancelEdit = useCallback(() => {
315
+ setDraft(message.content);
316
+ setEditing(false);
317
+ }, [message.content]);
318
+
319
+ const handleEditKeyDown = useCallback(
320
+ (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
321
+ if (e.key === "Escape") {
322
+ e.preventDefault();
323
+ handleCancelEdit();
324
+ } else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
325
+ e.preventDefault();
326
+ handleSaveEdit();
327
+ }
328
+ },
329
+ [handleSaveEdit, handleCancelEdit],
330
+ );
331
+
332
+ return (
333
+ <div
334
+ className="group flex justify-end"
335
+ data-touched={touched ? "true" : undefined}
336
+ onClickCapture={() => setTouched(true)}
337
+ >
338
+ <div className="flex max-w-[85%] flex-col items-end">
339
+ {editing ? (
340
+ <div className="w-full max-w-2xl rounded-2xl border border-[hsl(var(--primary))] bg-[hsl(var(--background))] p-2">
341
+ <Textarea
342
+ autoFocus
343
+ value={draft}
344
+ onChange={(e) => setDraft(e.target.value)}
345
+ onKeyDown={handleEditKeyDown}
346
+ className="min-h-[80px] resize-y border-0 p-2 text-sm shadow-none focus-visible:ring-0"
347
+ />
348
+ <div className="mt-1 flex justify-end gap-2">
349
+ <Button
350
+ type="button"
351
+ variant="ghost"
352
+ size="sm"
353
+ onClick={handleCancelEdit}
354
+ >
355
+ 取消
356
+ </Button>
357
+ <Button
358
+ type="button"
359
+ size="sm"
360
+ disabled={!draft.trim() || draft.trim() === message.content}
361
+ onClick={handleSaveEdit}
362
+ >
363
+ 保存并重发
364
+ </Button>
365
+ </div>
366
+ </div>
367
+ ) : (
368
+ <div className="rounded-2xl rounded-tr-sm bg-[hsl(var(--muted)/0.7)] px-4 py-2.5 text-sm leading-relaxed text-foreground whitespace-pre-wrap">
369
+ {message.content}
370
+ </div>
371
+ )}
372
+ {!editing && (
373
+ <MessageActions
374
+ align="right"
375
+ disabled={busy}
376
+ onCopy={handleCopy}
377
+ onEdit={() => {
378
+ setDraft(message.content);
379
+ setEditing(true);
380
+ }}
381
+ onResend={() => setConfirmResendOpen(true)}
382
+ onDelete={() => setConfirmDeleteOpen(true)}
383
+ />
384
+ )}
48
385
  </div>
386
+
387
+ <ConfirmDialog
388
+ open={confirmDeleteOpen}
389
+ onOpenChange={setConfirmDeleteOpen}
390
+ title="删除此消息?"
391
+ description="该操作不可撤销。"
392
+ confirmText="删除"
393
+ variant="danger"
394
+ onConfirm={() => {
395
+ onDelete(message.id, { withNextAssistant });
396
+ setConfirmDeleteOpen(false);
397
+ }}
398
+ >
399
+ <label className="flex items-center gap-2 text-sm text-foreground">
400
+ <Checkbox
401
+ checked={withNextAssistant}
402
+ onCheckedChange={(v) => setWithNextAssistant(v === true)}
403
+ />
404
+ 同时删除下方的 AI 回复
405
+ </label>
406
+ </ConfirmDialog>
407
+
408
+ <ConfirmDialog
409
+ open={confirmResendOpen}
410
+ onOpenChange={setConfirmResendOpen}
411
+ title="重新发送此消息?"
412
+ description="将丢弃下方所有消息并重新生成回答。"
413
+ confirmText="重新发送"
414
+ onConfirm={() => {
415
+ setConfirmResendOpen(false);
416
+ onResend(message.id);
417
+ }}
418
+ />
49
419
  </div>
50
420
  );
51
421
  }
52
422
 
53
- function AssistantBlock({ message }: { message: ChatMessage }) {
423
+ function AssistantBlock({
424
+ message,
425
+ busy,
426
+ onDelete,
427
+ onRegenerate,
428
+ }: {
429
+ message: ChatMessage;
430
+ busy: boolean;
431
+ onDelete: QaMessageProps["onDelete"];
432
+ onRegenerate: QaMessageProps["onRegenerate"];
433
+ }) {
434
+ const [touched, setTouched] = useState(false);
435
+ const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
436
+ const [confirmRegenOpen, setConfirmRegenOpen] = useState(false);
437
+
54
438
  const isStreaming = message.status === "streaming";
55
439
  const hasError = message.status === "error";
440
+ const hasReasoningText = (message.reasoning?.length ?? 0) > 0;
441
+ const hasContent = (message.content?.length ?? 0) > 0;
442
+
443
+ // Render the pill the moment a turn is in flight, **before** any reasoning
444
+ // chunk arrives. The server's RAG path runs query rewriting (~1-3s LLM
445
+ // call) and retrieval (~0.5-2s) before opening the upstream model stream,
446
+ // so `reasoning.delta` can be 2-5s away from the user's send. Without
447
+ // this early-render the assistant area sits blank during the entire
448
+ // pre-stream gap and the latency reads as a UI bug.
449
+ //
450
+ // For non-reasoning models the pill stays visible until the first
451
+ // `content.delta` arrives — `hasContent` flipping true makes both branches
452
+ // false (no reasoning text, content present) and the pill cleanly hands
453
+ // off to the streaming text below.
454
+ const showReasoningPill = hasReasoningText || (isStreaming && !hasContent);
455
+
456
+ // Stage of the pre-reasoning phase, derived from server events that arrive
457
+ // before the model's first token. Lets the label move through「检索意图
458
+ // 提取 → 正在检索知识库 → 正在思考 → 深度思考中」instead of sitting on
459
+ // a single generic word for the multiple seconds the server spends
460
+ // rewriting and retrieving.
461
+ const intentExtracting = !!message.usingRag && !message.retrievalQuery;
462
+ const retrievalInFlight =
463
+ !!message.retrievalQuery && !message.retrievedChunks;
464
+
465
+ // Aggregate per-KB retrieval errors so the TurnNotices chip can surface
466
+ // them without forcing the user to expand the chunks panel.
467
+ const failedKbDetails = (message.retrievalGroups ?? [])
468
+ .filter((g) => g.error)
469
+ .map((g) => ({ name: g.knowledgeBaseName, reason: g.error ?? "" }));
470
+
471
+ const handleCopy = useCallback(() => {
472
+ void navigator.clipboard.writeText(message.content);
473
+ toast.success("已复制");
474
+ }, [message.content]);
56
475
 
57
476
  return (
58
- <div className="space-y-3 text-sm leading-relaxed">
59
- {/* Reasoning collapsible (only present on reasoning-capable models). */}
60
- {message.reasoning ? (
61
- <ReasoningBlock reasoning={message.reasoning} streaming={isStreaming} />
477
+ <div
478
+ className="group space-y-3 text-sm leading-relaxed"
479
+ data-touched={touched ? "true" : undefined}
480
+ onClickCapture={() => setTouched(true)}
481
+ >
482
+ {showReasoningPill ? (
483
+ <ReasoningBlock
484
+ reasoning={message.reasoning ?? ""}
485
+ streaming={isStreaming}
486
+ startedAt={message.reasoningStartedAt}
487
+ completedAt={message.reasoningCompletedAt}
488
+ intentExtracting={intentExtracting}
489
+ retrievalInFlight={retrievalInFlight}
490
+ />
62
491
  ) : null}
63
492
 
493
+ <TurnNotices
494
+ rewriteFailed={message.queryRewriteFailed}
495
+ rewriteError={message.queryRewriteError}
496
+ failedKbCount={failedKbDetails.length}
497
+ failedKbDetails={failedKbDetails}
498
+ />
499
+
64
500
  {/* Rewritten retrieval query — only shown when it differs from the
65
501
  raw user input, so first-turn questions stay clean. */}
66
502
  {message.retrievalQueryRewritten && message.retrievalQuery ? (
@@ -97,54 +533,218 @@ function AssistantBlock({ message }: { message: ChatMessage }) {
97
533
  ) : null}
98
534
  </div>
99
535
  )}
536
+
537
+ {/* Hover-revealed action row. Hidden during streaming so users can't
538
+ trigger destructive ops while SSE is mid-flush. */}
539
+ <MessageActions
540
+ align="left"
541
+ disabled={busy || isStreaming}
542
+ onCopy={handleCopy}
543
+ onRegenerate={() => setConfirmRegenOpen(true)}
544
+ onDelete={() => setConfirmDeleteOpen(true)}
545
+ />
546
+
547
+ <ConfirmDialog
548
+ open={confirmDeleteOpen}
549
+ onOpenChange={setConfirmDeleteOpen}
550
+ title="删除此回复?"
551
+ description="该操作不可撤销。"
552
+ confirmText="删除"
553
+ variant="danger"
554
+ onConfirm={() => {
555
+ onDelete(message.id);
556
+ setConfirmDeleteOpen(false);
557
+ }}
558
+ />
559
+
560
+ <ConfirmDialog
561
+ open={confirmRegenOpen}
562
+ onOpenChange={setConfirmRegenOpen}
563
+ title="重新生成此回复?"
564
+ description="将丢弃下方所有消息并重新生成。"
565
+ confirmText="重新生成"
566
+ onConfirm={() => {
567
+ setConfirmRegenOpen(false);
568
+ onRegenerate(message.id);
569
+ }}
570
+ />
100
571
  </div>
101
572
  );
102
573
  }
103
574
 
575
+ /**
576
+ * Live-ticking elapsed-seconds counter. Returns null while `start` is
577
+ * undefined (reasoning hasn't begun); freezes when `end` is set so the
578
+ * "thought for X seconds" indicator stops climbing the moment the model
579
+ * moves on. Tick cadence is 250ms — fine enough to feel live, coarse
580
+ * enough that React doesn't burn frames re-rendering a string.
581
+ */
582
+ function useElapsedMs(start?: number, end?: number): number | null {
583
+ const [now, setNow] = useState<number>(() => Date.now());
584
+ useEffect(() => {
585
+ if (!start || end) return;
586
+ const id = window.setInterval(() => setNow(Date.now()), 250);
587
+ return () => window.clearInterval(id);
588
+ }, [start, end]);
589
+ if (!start) return null;
590
+ return Math.max(0, (end ?? now) - start);
591
+ }
592
+
593
+ /** Format elapsed milliseconds for the thinking pill. While the model is
594
+ * still working the format is `mm:ss` (live counter feel); after settle it
595
+ * becomes the friendlier "X 秒" / "X 分 Y 秒" reading conversational
596
+ * agents like DeepSeek and Doubao have made the de-facto Chinese standard. */
597
+ function formatElapsed(ms: number, settled: boolean): string {
598
+ const totalSec = Math.floor(ms / 1000);
599
+ const m = Math.floor(totalSec / 60);
600
+ const s = totalSec % 60;
601
+ if (settled) {
602
+ if (m === 0) return `用时 ${s} 秒`;
603
+ return `用时 ${m} 分 ${s} 秒`;
604
+ }
605
+ return `${m}:${String(s).padStart(2, "0")}`;
606
+ }
607
+
104
608
  function ReasoningBlock({
105
609
  reasoning,
106
610
  streaming,
611
+ startedAt,
612
+ completedAt,
613
+ intentExtracting,
614
+ retrievalInFlight,
107
615
  }: {
108
616
  reasoning: string;
109
617
  streaming: boolean;
618
+ startedAt?: number;
619
+ completedAt?: number;
620
+ intentExtracting?: boolean;
621
+ retrievalInFlight?: boolean;
110
622
  }) {
111
623
  // Collapse by default; let the user opt in explicitly. Avoids the
112
624
  // distracting auto-expand while the model is still producing tokens.
113
625
  const [open, setOpen] = useState(false);
114
626
 
627
+ // Elapsed counter is anchored to the first reasoning chunk, NOT the send
628
+ // time — measuring "thought for N seconds" should exclude server-side
629
+ // pre-thinking work (query rewrite, retrieval). When startedAt is
630
+ // undefined the hook returns null, which we render as no time chip.
631
+ const elapsedMs = useElapsedMs(startedAt, completedAt);
632
+ const settled = !streaming || Boolean(completedAt);
633
+ const elapsedLabel =
634
+ elapsedMs != null ? formatElapsed(elapsedMs, settled) : null;
635
+ const active = streaming && !completedAt;
636
+ const hasReasoningText = reasoning.length > 0;
637
+
638
+ // Active label escalates as more state lands. The pre-reasoning labels are
639
+ // important: without them, RAG turns sit on a single label for several
640
+ // seconds before any model token arrives, which reads as a hang.
641
+ //
642
+ // Order matters — the most specific (and earliest-firing) state wins:
643
+ // intent → server is running the rewriter LLM (RAG turns only)
644
+ // retrieve → KB queries are in-flight
645
+ // thinking → retrieval done, model preparing first token
646
+ // deep → reasoning tokens are streaming
647
+ let activeLabel = "正在思考";
648
+ if (hasReasoningText) {
649
+ activeLabel = "深度思考中";
650
+ } else if (retrievalInFlight) {
651
+ activeLabel = "正在检索知识库";
652
+ } else if (intentExtracting) {
653
+ activeLabel = "检索意图提取";
654
+ }
655
+
115
656
  return (
116
657
  <Collapsible open={open} onOpenChange={setOpen}>
117
658
  <CollapsibleTrigger asChild>
118
659
  <button
119
660
  type="button"
661
+ // A pill with no body to expand should not pretend to be expandable
662
+ // — disable the trigger and drop the chevron until reasoning text
663
+ // actually exists.
664
+ disabled={!hasReasoningText}
120
665
  className={cn(
121
- "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium",
122
- "text-muted-foreground transition-colors hover:bg-[hsl(var(--accent))] hover:text-foreground",
666
+ // Pill chrome: rounded-full with proportional padding, sitting on
667
+ // a soft tinted backdrop. The active state turns the backdrop
668
+ // primary-tinted with a hairline ring so the badge clearly reads
669
+ // as "live", matching the convention popularised by Claude and
670
+ // DeepSeek for in-progress reasoning.
671
+ "group inline-flex items-center gap-2 rounded-full pl-1.5 pr-2.5 py-1 text-xs font-medium transition-all duration-200",
672
+ active
673
+ ? "bg-[hsl(var(--primary)/0.08)] text-[hsl(var(--primary))] ring-1 ring-[hsl(var(--primary)/0.25)] shadow-[0_0_0_4px_hsl(var(--primary)/0.06)]"
674
+ : "bg-[hsl(var(--muted)/0.6)] text-muted-foreground hover:bg-[hsl(var(--accent))] hover:text-foreground",
675
+ // Don't show the disabled cursor on the no-content active pill —
676
+ // it correctly conveys "this isn't clickable yet" without making
677
+ // the live indicator feel broken.
678
+ !hasReasoningText && "cursor-default",
123
679
  )}
680
+ aria-live={active ? "polite" : "off"}
124
681
  >
125
- <Brain className="h-3.5 w-3.5" />
126
- {streaming ? (
127
- // Claude-style shimmer: a gradient sweeps across the text to
128
- // convey "thinking in progress" without being distracting.
129
- <span className="animate-text-shimmer bg-[linear-gradient(90deg,hsl(var(--muted-foreground))_0%,hsl(var(--foreground))_50%,hsl(var(--muted-foreground))_100%)] bg-[length:200%_100%] bg-clip-text text-transparent">
130
- 深度思考中
682
+ {/*
683
+ Brain avatar with an aurora halo. The halo is a separate blurred
684
+ element behind the icon so its scale-and-fade animation can run
685
+ independently of the icon's own breathing pulse — composing two
686
+ transforms on the same element would force them to share a
687
+ transform property.
688
+ */}
689
+ <span className="relative inline-flex h-5 w-5 items-center justify-center">
690
+ {active && (
691
+ <span
692
+ aria-hidden
693
+ className="absolute inset-0 rounded-full bg-[hsl(var(--primary))] opacity-40 blur-[6px] animate-thinking-aurora"
694
+ />
695
+ )}
696
+ <Brain
697
+ className={cn(
698
+ "relative h-3.5 w-3.5",
699
+ active && "animate-thinking-icon text-[hsl(var(--primary))]",
700
+ )}
701
+ />
702
+ </span>
703
+
704
+ {active ? (
705
+ // Brand-tinted shimmer band sweeping through neutral text. Much
706
+ // higher contrast than the previous muted→foreground gradient,
707
+ // so the motion actually reads at a glance. The label text still
708
+ // stays legible because the gradient stops keep the bookends
709
+ // saturated rather than transparent.
710
+ <span className="animate-text-shimmer bg-[linear-gradient(90deg,hsl(var(--primary)/0.55)_0%,hsl(var(--primary))_50%,hsl(var(--primary)/0.55)_100%)] bg-[length:200%_100%] bg-clip-text text-transparent">
711
+ {activeLabel}
131
712
  </span>
132
713
  ) : (
133
- "深度思考"
714
+ <span>已深度思考</span>
715
+ )}
716
+
717
+ {elapsedLabel && (
718
+ <span
719
+ className={cn(
720
+ "tabular-nums text-[11px]",
721
+ active
722
+ ? "text-[hsl(var(--primary)/0.75)]"
723
+ : "text-muted-foreground/80",
724
+ )}
725
+ >
726
+ · {elapsedLabel}
727
+ </span>
728
+ )}
729
+
730
+ {hasReasoningText && (
731
+ <ChevronDown
732
+ className={cn(
733
+ "h-3 w-3 opacity-60 transition-transform duration-200",
734
+ open && "rotate-180",
735
+ active && "text-[hsl(var(--primary))]",
736
+ )}
737
+ />
134
738
  )}
135
- <ChevronDown
136
- className={cn(
137
- "h-3 w-3 transition-transform duration-200",
138
- open && "rotate-180",
139
- )}
140
- />
141
739
  </button>
142
740
  </CollapsibleTrigger>
143
- <CollapsibleContent>
144
- <div className="mt-2 border-l-2 border-[hsl(var(--border))] pl-3 text-xs leading-relaxed text-muted-foreground whitespace-pre-wrap">
145
- {reasoning}
146
- </div>
147
- </CollapsibleContent>
741
+ {hasReasoningText && (
742
+ <CollapsibleContent>
743
+ <div className="mt-2 border-l-2 border-[hsl(var(--border))] pl-3 text-xs leading-relaxed text-muted-foreground whitespace-pre-wrap">
744
+ {reasoning}
745
+ </div>
746
+ </CollapsibleContent>
747
+ )}
148
748
  </Collapsible>
149
749
  );
150
750
  }