@firstlovecenter/ai-chat 0.2.1 → 0.2.2
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/CHANGELOG.md +10 -0
- package/dist/ui/index.cjs +42 -4
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.js +42 -4
- package/dist/ui/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ All notable changes to `@firstlovecenter/ai-chat` are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.2] — 2026-05-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Live elapsed counter while the agent is thinking** — the inline thinking indicator now shows `Thinking… (Xs)` with a live ticker that advances every second. Suppresses sub-second values (no flicker on instant responses).
|
|
13
|
+
- **Final duration after the action buttons** — once the agent marks a turn done, the wall-clock duration appears to the right of the Copy / Retry buttons (`5s`, `1m`, `2m 30s` style). Restored history (sessions loaded from the DB) shows nothing here since durations weren't persisted.
|
|
14
|
+
|
|
15
|
+
The new `AnswerState.startedAt` and `AnswerState.durationMs` fields are stamped at submit-time and on every `done: true` flip respectively. Format helper `formatDuration(ms)` produces `Xs`, `Xm`, or `Xm Ys`.
|
|
16
|
+
|
|
8
17
|
## [0.2.1] — 2026-05-08
|
|
9
18
|
|
|
10
19
|
### Fixed
|
|
@@ -70,6 +79,7 @@ Initial public release on npm under `@firstlovecenter/ai-chat`.
|
|
|
70
79
|
- Dual ESM + CJS output via `tsup` with full `.d.ts` (and `.d.cts`) generation.
|
|
71
80
|
- UI bundle gets a post-build `'use client';` directive injection (tsup otherwise strips module-level directives during bundling, breaking RSC consumers that import the UI from a server file).
|
|
72
81
|
|
|
82
|
+
[0.2.2]: https://github.com/firstlovecenter/flc-ai-chat/compare/v0.2.1...v0.2.2
|
|
73
83
|
[0.2.1]: https://github.com/firstlovecenter/flc-ai-chat/compare/v0.2.0...v0.2.1
|
|
74
84
|
[0.2.0]: https://github.com/firstlovecenter/flc-ai-chat/compare/v0.1.1...v0.2.0
|
|
75
85
|
[0.1.1]: https://github.com/firstlovecenter/flc-ai-chat/compare/v0.1.0...v0.1.1
|
package/dist/ui/index.cjs
CHANGED
|
@@ -544,6 +544,14 @@ function AiLineChart({
|
|
|
544
544
|
))
|
|
545
545
|
] }) }) });
|
|
546
546
|
}
|
|
547
|
+
function formatDuration(ms) {
|
|
548
|
+
if (ms < 0) ms = 0;
|
|
549
|
+
const totalSec = Math.round(ms / 1e3);
|
|
550
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
551
|
+
const m = Math.floor(totalSec / 60);
|
|
552
|
+
const s = totalSec % 60;
|
|
553
|
+
return s === 0 ? `${m}m` : `${m}m ${s}s`;
|
|
554
|
+
}
|
|
547
555
|
var PROVIDER_LABELS = {
|
|
548
556
|
claude: "Claude",
|
|
549
557
|
grok: "Grok",
|
|
@@ -718,7 +726,7 @@ function AiChat({
|
|
|
718
726
|
}
|
|
719
727
|
setAnswers((prev) => [
|
|
720
728
|
...prev,
|
|
721
|
-
{ question: trimmed, blocks: [], done: false }
|
|
729
|
+
{ question: trimmed, blocks: [], done: false, startedAt: Date.now() }
|
|
722
730
|
]);
|
|
723
731
|
const ac = new AbortController();
|
|
724
732
|
abortRef.current = ac;
|
|
@@ -739,6 +747,7 @@ function AiChat({
|
|
|
739
747
|
(prev) => updateLast(prev, (a) => ({
|
|
740
748
|
...a,
|
|
741
749
|
done: true,
|
|
750
|
+
durationMs: a.startedAt != null ? Date.now() - a.startedAt : void 0,
|
|
742
751
|
error: { code: "NETWORK", message }
|
|
743
752
|
}))
|
|
744
753
|
);
|
|
@@ -750,6 +759,7 @@ function AiChat({
|
|
|
750
759
|
(prev) => updateLast(prev, (a) => ({
|
|
751
760
|
...a,
|
|
752
761
|
done: true,
|
|
762
|
+
durationMs: a.startedAt != null ? Date.now() - a.startedAt : void 0,
|
|
753
763
|
error: { code: "NO_BODY", message: "No response stream." }
|
|
754
764
|
}))
|
|
755
765
|
);
|
|
@@ -776,6 +786,7 @@ function AiChat({
|
|
|
776
786
|
(prev) => updateLast(prev, (a) => ({
|
|
777
787
|
...a,
|
|
778
788
|
done: true,
|
|
789
|
+
durationMs: a.startedAt != null ? Date.now() - a.startedAt : void 0,
|
|
779
790
|
error: { code: "STREAM", message }
|
|
780
791
|
}))
|
|
781
792
|
);
|
|
@@ -808,7 +819,7 @@ function AiChat({
|
|
|
808
819
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
809
820
|
"aside",
|
|
810
821
|
{
|
|
811
|
-
|
|
822
|
+
inert: !sidebarOpen,
|
|
812
823
|
className: cn(
|
|
813
824
|
"absolute inset-y-0 left-0 z-20 flex w-72 max-w-[85vw] flex-col border-r border-border bg-sidebar text-sidebar-foreground shadow-lg transition-transform duration-200 ease-out",
|
|
814
825
|
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
@@ -1074,6 +1085,14 @@ function AnswerView({
|
|
|
1074
1085
|
}, [answer.blocks, answer.error]);
|
|
1075
1086
|
const showActions = answer.done && (answer.blocks.length > 0 || answer.error != null);
|
|
1076
1087
|
const isThinking = !answer.done && answer.blocks.length === 0 && !answer.error;
|
|
1088
|
+
const [, forceTick] = React.useState(0);
|
|
1089
|
+
React.useEffect(() => {
|
|
1090
|
+
if (answer.done || answer.startedAt == null) return;
|
|
1091
|
+
const id = window.setInterval(() => forceTick((n) => n + 1), 1e3);
|
|
1092
|
+
return () => window.clearInterval(id);
|
|
1093
|
+
}, [answer.done, answer.startedAt]);
|
|
1094
|
+
const liveElapsed = answer.startedAt != null ? Date.now() - answer.startedAt : null;
|
|
1095
|
+
const finalDuration = answer.durationMs;
|
|
1077
1096
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1078
1097
|
"div",
|
|
1079
1098
|
{
|
|
@@ -1092,7 +1111,12 @@ function AnswerView({
|
|
|
1092
1111
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "size-4" }) }),
|
|
1093
1112
|
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "flex items-center text-sm text-muted-foreground", children: [
|
|
1094
1113
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "mr-1 inline size-3.5 animate-spin" }),
|
|
1095
|
-
"Thinking\u2026"
|
|
1114
|
+
"Thinking\u2026",
|
|
1115
|
+
liveElapsed != null && liveElapsed >= 1e3 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1 tabular-nums", children: [
|
|
1116
|
+
"(",
|
|
1117
|
+
formatDuration(liveElapsed),
|
|
1118
|
+
")"
|
|
1119
|
+
] })
|
|
1096
1120
|
] })
|
|
1097
1121
|
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3", children: [
|
|
1098
1122
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "size-4" }) }),
|
|
@@ -1125,6 +1149,14 @@ function AnswerView({
|
|
|
1125
1149
|
className: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
1126
1150
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { className: "size-4" })
|
|
1127
1151
|
}
|
|
1152
|
+
),
|
|
1153
|
+
finalDuration != null && finalDuration >= 1e3 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1154
|
+
"span",
|
|
1155
|
+
{
|
|
1156
|
+
className: "ml-1 text-xs text-muted-foreground tabular-nums",
|
|
1157
|
+
title: "Time taken to generate this response",
|
|
1158
|
+
children: formatDuration(finalDuration)
|
|
1159
|
+
}
|
|
1128
1160
|
)
|
|
1129
1161
|
] })
|
|
1130
1162
|
] })
|
|
@@ -1330,7 +1362,13 @@ function handleEvent(raw, setAnswers) {
|
|
|
1330
1362
|
})
|
|
1331
1363
|
);
|
|
1332
1364
|
} else if (event === "done") {
|
|
1333
|
-
setAnswers(
|
|
1365
|
+
setAnswers(
|
|
1366
|
+
(prev) => updateLast(prev, (a) => ({
|
|
1367
|
+
...a,
|
|
1368
|
+
done: true,
|
|
1369
|
+
durationMs: a.startedAt != null ? Date.now() - a.startedAt : void 0
|
|
1370
|
+
}))
|
|
1371
|
+
);
|
|
1334
1372
|
} else if (event === "error") {
|
|
1335
1373
|
setAnswers(
|
|
1336
1374
|
(prev) => updateLast(prev, (a) => ({
|