@tablestore/tablestore-agent-cli 0.1.7 → 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.
- package/dist/dashboard-runtime/dashboard/.next/BUILD_ID +1 -1
- package/dist/dashboard-runtime/dashboard/.next/build-manifest.json +3 -3
- package/dist/dashboard-runtime/dashboard/.next/prerender-manifest.json +3 -3
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/chat/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/chat/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/documents/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/documents/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/qa/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/qa/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/retrieval/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/retrieval/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/settings/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/settings/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/create/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/[id]/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/new/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/messages/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/messages/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/requests/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/requests/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/search/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/search/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/settings/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/settings/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/units/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/units/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/create/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/projects/page.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/projects/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.html +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.html +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/api/v1/knowledge-bases/[name]/documents/upload/route.js.nft.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/index.html +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/index.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.html +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.rsc +3 -3
- package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_full.segment.rsc +3 -3
- package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_head.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_index.segment.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_tree.segment.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/onboarding.segment.rsc +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/0b1r_app_(dashboard)_knowledge-base_[name]_documents_documents-client_tsx_0g2uxmt._.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__09_30hi._.js +3 -0
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0kaiok7._.js +3 -0
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0l937k4._.js +3 -0
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0-dlo4s._.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_01pp~xx._.js +3 -0
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0476v7r._.js → _08j6p-z._.js} +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0ct0n55._.js → _09eom0_._.js} +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0mzxmfh._.js → _0f-may2._.js} +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0fp_nnb._.js +3 -0
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0l6jz-5._.js +3 -0
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0fg6iap._.js → _0ntv66s._.js} +2 -2
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0sc0tpz._.js +3 -0
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0tyxwr3._.js +3 -0
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0u9md36._.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/dashboard_src_065m1.j._.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/dashboard_src_app_(dashboard)_knowledge-base_[name]_layout-client_tsx_05momc5._.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/middleware-build-manifest.js +3 -3
- package/dist/dashboard-runtime/dashboard/.next/server/pages/404.html +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/pages/500.html +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/server-reference-manifest.js +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/server-reference-manifest.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0-xqk6wfme5fu.js +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/{0jeb9kxtd_jq..js → 00.fyg_t3xjc~.js} +1 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/01k-.wkndubwy.js +138 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/07r22x5uhjxjb.js +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0cbu3udngtx-v.js +4 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0h7d69g2fdgs6.js +138 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0iv0g360z0ja5.js +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0jf4ytf.izmzh.js +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/{17nx_vdk_qemm.js → 0m42mmyxmgn-i.js} +1 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0odz1whf-ozqn.css +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0ouxv_2qvf6.9.js +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0rbkgj._oqsb3.js +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0u6u.649ou67b.js +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0vf7~eysvh9bq.js +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/{09qsmnfvk8dm3.js → 0wmaq.ay~r2sn.js} +1 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/{0i5aqiffpm9jr.js → 0xb0_9t-vmo6e.js} +1 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0yp_2ea75d19v.js +1 -0
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/{0s3mvx9_dr13c.js → 11cizim1aug~b.js} +1 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/13-2ei295hl0-.js +1 -0
- package/dist/dashboard-runtime/dashboard/src/app/(dashboard)/knowledge-base/[name]/qa/page.tsx +23 -1
- package/dist/dashboard-runtime/dashboard/src/components/documents/doc-upload-dialog.tsx +370 -91
- package/dist/dashboard-runtime/dashboard/src/components/knowledge-base/kb-card.tsx +8 -5
- package/dist/dashboard-runtime/dashboard/src/components/knowledge-base/kb-header.tsx +18 -10
- package/dist/dashboard-runtime/dashboard/src/components/memory/memory-header.tsx +8 -12
- package/dist/dashboard-runtime/dashboard/src/components/qa/qa-chat-panel.tsx +45 -4
- package/dist/dashboard-runtime/dashboard/src/components/qa/qa-message.tsx +636 -36
- package/dist/dashboard-runtime/dashboard/src/components/shared/confirm-dialog.tsx +8 -1
- package/dist/dashboard-runtime/dashboard/src/components/shared/refresh-button.tsx +102 -86
- package/dist/dashboard-runtime/dashboard/src/hooks/use-qa-chat.ts +261 -58
- package/dist/dashboard-runtime/dashboard/src/lib/chat-message-ops.ts +76 -0
- package/dist/dashboard-runtime/dashboard/src/lib/dev-flags.ts +24 -0
- package/dist/dashboard-runtime/dashboard/src/types/llm-chat.ts +19 -0
- package/package.json +1 -1
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0i0y9.r._.js +0 -3
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0r_6wpx._.js +0 -3
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0y8rrcu._.js +0 -3
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_02e0c00._.js +0 -3
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0doczxg._.js +0 -3
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0e9y.xc._.js +0 -3
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0ti~om_._.js +0 -3
- package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0~s~e-0._.js +0 -3
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/055_bo6_3nt-s.js +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/07l4dodn5t6l4.js +0 -4
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0a5ass8u78k90.js +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0j~63ld9lbb25.js +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0l6znnjqw.4yw.js +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0o-c2g8y5pw-c.js +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0o0i2ke2jbdvq.js +0 -138
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0u2lo2lzizxy_.js +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/0v.hno.6q74nn.css +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/100.ax5hitxko.js +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/14ti2gx~1fiub.js +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/14vqmdm7sosj2.js +0 -1
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/17h~onnkksf44.js +0 -138
- package/dist/dashboard-runtime/dashboard/.next/static/chunks/186jubvt5xyuj.js +0 -1
- /package/dist/dashboard-runtime/dashboard/.next/static/{wWmSSYUu4w4mIF1R_7UoM → BGnMIk4OPjFx_qvLFMTBc}/_buildManifest.js +0 -0
- /package/dist/dashboard-runtime/dashboard/.next/static/{wWmSSYUu4w4mIF1R_7UoM → BGnMIk4OPjFx_qvLFMTBc}/_clientMiddlewareManifest.js +0 -0
- /package/dist/dashboard-runtime/dashboard/.next/static/{wWmSSYUu4w4mIF1R_7UoM → BGnMIk4OPjFx_qvLFMTBc}/_ssgManifest.js +0 -0
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { memo, useState } from "react";
|
|
4
|
-
import {
|
|
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({
|
|
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
|
|
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
|
|
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
|
-
|
|
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="
|
|
46
|
-
|
|
47
|
-
|
|
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({
|
|
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
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
}
|