@firstlovecenter/ai-chat 0.5.0 → 0.6.0

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.
@@ -139,16 +139,26 @@ type ChatMessage = {
139
139
  createdAt: Date;
140
140
  };
141
141
  /**
142
- * Singleton row controlling the active tool-calling provider, the Vertex
143
- * region, and which chat UI (Custom vs. Vercel) renders globally. All three
142
+ * Singleton row controlling global AI runtime settings. All open-string
144
143
  * fields are validated at runtime against the registries the host configures
145
144
  * — they are not enums in the package's types so consumers can register
146
145
  * additional providers / interfaces without a schema change.
146
+ *
147
+ * `maxOutputTokens` caps both the agent loop's per-turn output AND each
148
+ * narrator's prose pass. Reasoning models (e.g. `xai/grok-4.1-fast-reasoning`)
149
+ * charge internal thinking against this budget — set it generously when
150
+ * those are in use.
151
+ *
152
+ * `rolePrompt` is the persona the assistant adopts. When non-null, it
153
+ * takes precedence over the host's static `rolePrompt` configureAiChat
154
+ * option, so admins can edit live in the settings UI.
147
155
  */
148
156
  type AiSettings = {
149
157
  toolProvider: string;
150
158
  gcpLocation: string;
151
159
  chatInterface: string;
160
+ maxOutputTokens: number;
161
+ rolePrompt: string | null;
152
162
  updatedAt: Date | null;
153
163
  updatedByUserId: number | null;
154
164
  };
@@ -242,6 +252,9 @@ type AiSettingsPatch = {
242
252
  toolProvider?: string;
243
253
  gcpLocation?: string;
244
254
  chatInterface?: string;
255
+ maxOutputTokens?: number;
256
+ /** Pass `null` to clear back to the host's static fallback. */
257
+ rolePrompt?: string | null;
245
258
  };
246
259
  /**
247
260
  * The whole reason this package is ORM-agnostic. Implemented by:
@@ -139,16 +139,26 @@ type ChatMessage = {
139
139
  createdAt: Date;
140
140
  };
141
141
  /**
142
- * Singleton row controlling the active tool-calling provider, the Vertex
143
- * region, and which chat UI (Custom vs. Vercel) renders globally. All three
142
+ * Singleton row controlling global AI runtime settings. All open-string
144
143
  * fields are validated at runtime against the registries the host configures
145
144
  * — they are not enums in the package's types so consumers can register
146
145
  * additional providers / interfaces without a schema change.
146
+ *
147
+ * `maxOutputTokens` caps both the agent loop's per-turn output AND each
148
+ * narrator's prose pass. Reasoning models (e.g. `xai/grok-4.1-fast-reasoning`)
149
+ * charge internal thinking against this budget — set it generously when
150
+ * those are in use.
151
+ *
152
+ * `rolePrompt` is the persona the assistant adopts. When non-null, it
153
+ * takes precedence over the host's static `rolePrompt` configureAiChat
154
+ * option, so admins can edit live in the settings UI.
147
155
  */
148
156
  type AiSettings = {
149
157
  toolProvider: string;
150
158
  gcpLocation: string;
151
159
  chatInterface: string;
160
+ maxOutputTokens: number;
161
+ rolePrompt: string | null;
152
162
  updatedAt: Date | null;
153
163
  updatedByUserId: number | null;
154
164
  };
@@ -242,6 +252,9 @@ type AiSettingsPatch = {
242
252
  toolProvider?: string;
243
253
  gcpLocation?: string;
244
254
  chatInterface?: string;
255
+ maxOutputTokens?: number;
256
+ /** Pass `null` to clear back to the host's static fallback. */
257
+ rolePrompt?: string | null;
245
258
  };
246
259
  /**
247
260
  * The whole reason this package is ORM-agnostic. Implemented by:
package/dist/ui/index.cjs CHANGED
@@ -684,6 +684,7 @@ function AiChat({
684
684
  const textareaRef = React.useRef(null);
685
685
  const lastAnswerRef = React.useRef(null);
686
686
  const prevAnswersLen = React.useRef(0);
687
+ const autoOpenedRef = React.useRef(false);
687
688
  React.useLayoutEffect(() => {
688
689
  const el = textareaRef.current;
689
690
  if (!el) return;
@@ -698,7 +699,34 @@ function AiChat({
698
699
  const res = await fetch("/api/chat/sessions", { cache: "no-store" });
699
700
  if (!res.ok) return;
700
701
  const data = await res.json();
701
- if (!cancelled) setSessions(data.sessions ?? []);
702
+ if (cancelled) return;
703
+ const list = data.sessions ?? [];
704
+ setSessions(list);
705
+ if (!autoOpenedRef.current && list.length > 0) {
706
+ autoOpenedRef.current = true;
707
+ const mostRecentId = list[0].id;
708
+ setLoadingSession(true);
709
+ setActiveSessionId(mostRecentId);
710
+ try {
711
+ const sres = await fetch(`/api/chat/sessions/${mostRecentId}`, {
712
+ cache: "no-store"
713
+ });
714
+ if (cancelled) return;
715
+ if (!sres.ok) {
716
+ setAnswers([]);
717
+ return;
718
+ }
719
+ const sdata = await sres.json();
720
+ if (cancelled) return;
721
+ setAnswers(messagesToAnswers(sdata.messages ?? []));
722
+ } catch {
723
+ if (!cancelled) setAnswers([]);
724
+ } finally {
725
+ if (!cancelled) setLoadingSession(false);
726
+ }
727
+ } else if (!autoOpenedRef.current) {
728
+ autoOpenedRef.current = true;
729
+ }
702
730
  } catch {
703
731
  }
704
732
  }
@@ -1475,6 +1503,7 @@ function VercelChat({
1475
1503
  const textareaRef = React.useRef(null);
1476
1504
  const lastAnswerRef = React.useRef(null);
1477
1505
  const prevAnswersLen = React.useRef(0);
1506
+ const autoOpenedRef = React.useRef(false);
1478
1507
  const activeSessionIdRef = React.useRef(activeSessionId);
1479
1508
  const providerRef = React.useRef(provider);
1480
1509
  React.useEffect(() => {
@@ -1529,7 +1558,49 @@ function VercelChat({
1529
1558
  const res = await fetch("/api/chat/sessions", { cache: "no-store" });
1530
1559
  if (!res.ok) return;
1531
1560
  const json = await res.json();
1532
- if (!cancelled) setSessions(json.sessions ?? []);
1561
+ if (cancelled) return;
1562
+ const list = json.sessions ?? [];
1563
+ setSessions(list);
1564
+ if (!autoOpenedRef.current && list.length > 0) {
1565
+ autoOpenedRef.current = true;
1566
+ const mostRecentId = list[0].id;
1567
+ setLoadingSession(true);
1568
+ setActiveSessionId(mostRecentId);
1569
+ try {
1570
+ const sres = await fetch(`/api/chat/sessions/${mostRecentId}`, {
1571
+ cache: "no-store"
1572
+ });
1573
+ if (cancelled) return;
1574
+ if (!sres.ok) {
1575
+ setMessages([]);
1576
+ setHydratedBlocks({});
1577
+ setHydratedProse({});
1578
+ setHydratedErrors({});
1579
+ return;
1580
+ }
1581
+ const sjson = await sres.json();
1582
+ if (cancelled) return;
1583
+ const { uiMessages, blocksMap, proseMap, errorsMap } = storedToUseChat(
1584
+ sjson.messages ?? []
1585
+ );
1586
+ setMessages(uiMessages);
1587
+ setHydratedBlocks(blocksMap);
1588
+ setHydratedProse(proseMap);
1589
+ setHydratedErrors(errorsMap);
1590
+ setStartedAt({});
1591
+ } catch {
1592
+ if (!cancelled) {
1593
+ setMessages([]);
1594
+ setHydratedBlocks({});
1595
+ setHydratedProse({});
1596
+ setHydratedErrors({});
1597
+ }
1598
+ } finally {
1599
+ if (!cancelled) setLoadingSession(false);
1600
+ }
1601
+ } else if (!autoOpenedRef.current) {
1602
+ autoOpenedRef.current = true;
1603
+ }
1533
1604
  } catch {
1534
1605
  }
1535
1606
  }
@@ -1537,7 +1608,7 @@ function VercelChat({
1537
1608
  return () => {
1538
1609
  cancelled = true;
1539
1610
  };
1540
- }, []);
1611
+ }, [setMessages]);
1541
1612
  const answers = React.useMemo(() => {
1542
1613
  const liveBlocks = [];
1543
1614
  const liveErrors = [];