@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 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
- "aria-hidden": !sidebarOpen,
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((prev) => updateLast(prev, (a) => ({ ...a, done: true })));
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) => ({