@farming-labs/theme 0.0.3-beta.2 → 0.0.3-beta.4

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.
@@ -1,4 +1,8 @@
1
1
  //#region src/ai-search-dialog.d.ts
2
+ type AIModelOption = {
3
+ id: string;
4
+ label: string;
5
+ };
2
6
  type LoaderVariant = "shimmer-dots" | "circular" | "dots" | "typing" | "wave" | "bars" | "pulse" | "pulse-dot" | "terminal" | "text-blink" | "text-shimmer" | "loading-dots";
3
7
  declare function DocsSearchDialog({
4
8
  open,
@@ -7,7 +11,9 @@ declare function DocsSearchDialog({
7
11
  suggestedQuestions,
8
12
  aiLabel,
9
13
  loaderVariant,
10
- loadingComponentHtml
14
+ loadingComponentHtml,
15
+ models,
16
+ defaultModelId
11
17
  }: {
12
18
  open: boolean;
13
19
  onOpenChange: (open: boolean) => void;
@@ -16,6 +22,8 @@ declare function DocsSearchDialog({
16
22
  aiLabel?: string;
17
23
  loaderVariant?: LoaderVariant;
18
24
  loadingComponentHtml?: string;
25
+ models?: AIModelOption[];
26
+ defaultModelId?: string;
19
27
  }): any;
20
28
  type FloatingPosition = "bottom-right" | "bottom-left" | "bottom-center";
21
29
  type FloatingStyle = "panel" | "modal" | "popover" | "full-modal";
@@ -27,7 +35,9 @@ declare function FloatingAIChat({
27
35
  suggestedQuestions,
28
36
  aiLabel,
29
37
  loaderVariant,
30
- loadingComponentHtml
38
+ loadingComponentHtml,
39
+ models,
40
+ defaultModelId
31
41
  }: {
32
42
  api?: string;
33
43
  position?: FloatingPosition;
@@ -37,6 +47,8 @@ declare function FloatingAIChat({
37
47
  aiLabel?: string;
38
48
  loaderVariant?: LoaderVariant;
39
49
  loadingComponentHtml?: string;
50
+ models?: AIModelOption[];
51
+ defaultModelId?: string;
40
52
  }): any;
41
53
  declare function AIModalDialog({
42
54
  open,
@@ -45,7 +57,9 @@ declare function AIModalDialog({
45
57
  suggestedQuestions,
46
58
  aiLabel,
47
59
  loaderVariant,
48
- loadingComponentHtml
60
+ loadingComponentHtml,
61
+ models,
62
+ defaultModelId
49
63
  }: {
50
64
  open: boolean;
51
65
  onOpenChange: (open: boolean) => void;
@@ -54,6 +68,8 @@ declare function AIModalDialog({
54
68
  aiLabel?: string;
55
69
  loaderVariant?: LoaderVariant;
56
70
  loadingComponentHtml?: string;
71
+ models?: AIModelOption[];
72
+ defaultModelId?: string;
57
73
  }): any;
58
74
  //#endregion
59
75
  export { AIModalDialog, DocsSearchDialog, FloatingAIChat };
@@ -281,10 +281,77 @@ function InlineLoaderDots() {
281
281
  ]
282
282
  });
283
283
  }
284
- function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
284
+ function ModelSelector({ models, selectedId, onChange, disabled }) {
285
+ const [open, setOpen] = useState(false);
286
+ const ref = useRef(null);
287
+ useEffect(() => {
288
+ if (!open) return;
289
+ function handleClick(e) {
290
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
291
+ }
292
+ document.addEventListener("mousedown", handleClick);
293
+ return () => document.removeEventListener("mousedown", handleClick);
294
+ }, [open]);
295
+ const current = models.find((m) => m.id === selectedId) ?? models[0];
296
+ return /* @__PURE__ */ jsxs("div", {
297
+ ref,
298
+ className: "fd-ai-model-dropdown",
299
+ children: [/* @__PURE__ */ jsxs("button", {
300
+ type: "button",
301
+ className: "fd-ai-model-dropdown-btn",
302
+ onClick: () => !disabled && setOpen(!open),
303
+ "aria-expanded": open,
304
+ disabled,
305
+ children: [/* @__PURE__ */ jsx("span", { children: current?.label ?? "Select model" }), /* @__PURE__ */ jsx("svg", {
306
+ width: "12",
307
+ height: "12",
308
+ viewBox: "0 0 24 24",
309
+ fill: "none",
310
+ stroke: "currentColor",
311
+ strokeWidth: "2",
312
+ strokeLinecap: "round",
313
+ strokeLinejoin: "round",
314
+ children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
315
+ })]
316
+ }), open && /* @__PURE__ */ jsx("div", {
317
+ className: "fd-ai-model-dropdown-menu",
318
+ role: "menu",
319
+ children: models.map((m) => /* @__PURE__ */ jsxs("button", {
320
+ type: "button",
321
+ role: "menuitem",
322
+ className: "fd-ai-model-dropdown-item",
323
+ "data-active": m.id === selectedId,
324
+ onClick: () => {
325
+ onChange(m.id);
326
+ setOpen(false);
327
+ },
328
+ children: [/* @__PURE__ */ jsx("span", {
329
+ className: "fd-ai-model-dropdown-label",
330
+ children: m.label
331
+ }), m.id === selectedId && /* @__PURE__ */ jsx("svg", {
332
+ width: "14",
333
+ height: "14",
334
+ viewBox: "0 0 24 24",
335
+ fill: "none",
336
+ stroke: "currentColor",
337
+ strokeWidth: "2.5",
338
+ strokeLinecap: "round",
339
+ strokeLinejoin: "round",
340
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
341
+ })]
342
+ }, m.id))
343
+ })]
344
+ });
345
+ }
346
+ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
285
347
  const label = aiLabel || "AI";
286
348
  const aiInputRef = useRef(null);
287
349
  const messagesEndRef = useRef(null);
350
+ const [selectedModel, setSelectedModel] = useState(() => {
351
+ const trimmed = (defaultModelId ?? "").trim();
352
+ return trimmed.length > 0 ? trimmed : void 0;
353
+ });
354
+ const effectiveModelId = selectedModel || (Array.isArray(models) && models.length > 0 ? models[0].id : void 0);
288
355
  useEffect(() => {
289
356
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
290
357
  }, [messages]);
@@ -308,10 +375,13 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
308
375
  const res = await fetch(api, {
309
376
  method: "POST",
310
377
  headers: { "Content-Type": "application/json" },
311
- body: JSON.stringify({ messages: newMessages.map((m) => ({
312
- role: m.role,
313
- content: m.content
314
- })) })
378
+ body: JSON.stringify({
379
+ messages: newMessages.map((m) => ({
380
+ role: m.role,
381
+ content: m.content
382
+ })),
383
+ model: effectiveModelId
384
+ })
315
385
  });
316
386
  if (!res.ok) {
317
387
  let errMsg = "Something went wrong.";
@@ -367,7 +437,8 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
367
437
  isStreaming,
368
438
  setMessages,
369
439
  setAiInput,
370
- setIsStreaming
440
+ setIsStreaming,
441
+ effectiveModelId
371
442
  ]);
372
443
  const handleAskAI = useCallback(async () => {
373
444
  await submitQuestion(aiInput);
@@ -434,44 +505,56 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
434
505
  }, i)), /* @__PURE__ */ jsx("div", { ref: messagesEndRef })]
435
506
  }), /* @__PURE__ */ jsxs("div", {
436
507
  className: "fd-ai-chat-footer",
437
- children: [messages.length > 0 && /* @__PURE__ */ jsx("div", {
438
- style: {
439
- display: "flex",
440
- justifyContent: "flex-end",
441
- paddingBottom: 8
442
- },
443
- children: /* @__PURE__ */ jsx("button", {
444
- onClick: () => {
445
- setMessages([]);
446
- setAiInput("");
508
+ children: [
509
+ Array.isArray(models) && models.length > 0 && /* @__PURE__ */ jsx("div", {
510
+ className: "fd-ai-model-select-row",
511
+ children: /* @__PURE__ */ jsx(ModelSelector, {
512
+ models,
513
+ selectedId: effectiveModelId ?? models[0].id,
514
+ onChange: setSelectedModel,
515
+ disabled: isStreaming
516
+ })
517
+ }),
518
+ messages.length > 0 && /* @__PURE__ */ jsx("div", {
519
+ style: {
520
+ display: "flex",
521
+ justifyContent: "flex-end",
522
+ paddingBottom: 8
447
523
  },
448
- className: "fd-ai-clear-btn",
449
- children: "Clear chat"
524
+ children: /* @__PURE__ */ jsx("button", {
525
+ onClick: () => {
526
+ setMessages([]);
527
+ setAiInput("");
528
+ },
529
+ className: "fd-ai-clear-btn",
530
+ children: "Clear chat"
531
+ })
532
+ }),
533
+ /* @__PURE__ */ jsxs("div", {
534
+ className: "fd-ai-input-wrap",
535
+ children: [/* @__PURE__ */ jsx("input", {
536
+ ref: aiInputRef,
537
+ type: "text",
538
+ placeholder: "Ask a question...",
539
+ value: aiInput,
540
+ onChange: (e) => setAiInput(e.target.value),
541
+ onKeyDown: handleAIKeyDown,
542
+ disabled: isStreaming,
543
+ className: "fd-ai-input",
544
+ style: { opacity: isStreaming ? .5 : 1 }
545
+ }), /* @__PURE__ */ jsx("button", {
546
+ onClick: handleAskAI,
547
+ disabled: !canSend,
548
+ className: "fd-ai-send-btn",
549
+ "data-active": canSend,
550
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
551
+ })]
450
552
  })
451
- }), /* @__PURE__ */ jsxs("div", {
452
- className: "fd-ai-input-wrap",
453
- children: [/* @__PURE__ */ jsx("input", {
454
- ref: aiInputRef,
455
- type: "text",
456
- placeholder: "Ask a question...",
457
- value: aiInput,
458
- onChange: (e) => setAiInput(e.target.value),
459
- onKeyDown: handleAIKeyDown,
460
- disabled: isStreaming,
461
- className: "fd-ai-input",
462
- style: { opacity: isStreaming ? .5 : 1 }
463
- }), /* @__PURE__ */ jsx("button", {
464
- onClick: handleAskAI,
465
- disabled: !canSend,
466
- className: "fd-ai-send-btn",
467
- "data-active": canSend,
468
- children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
469
- })]
470
- })]
553
+ ]
471
554
  })]
472
555
  });
473
556
  }
474
- function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
557
+ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
475
558
  const [tab, setTab] = useState("search");
476
559
  const [searchQuery, setSearchQuery] = useState("");
477
560
  const [searchResults, setSearchResults] = useState([]);
@@ -481,6 +564,11 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
481
564
  const [messages, setMessages] = useState([]);
482
565
  const [aiInput, setAiInput] = useState("");
483
566
  const [isStreaming, setIsStreaming] = useState(false);
567
+ const [selectedModel, setSelectedModel] = useState(() => {
568
+ const trimmed = (defaultModelId ?? "").trim();
569
+ return trimmed.length > 0 ? trimmed : void 0;
570
+ });
571
+ const effectiveModelId = selectedModel || (Array.isArray(models) && models.length > 0 ? models[0].id : void 0);
484
572
  useEffect(() => {
485
573
  if (open) {
486
574
  setSearchQuery("");
@@ -647,7 +735,9 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
647
735
  suggestedQuestions,
648
736
  aiLabel,
649
737
  loaderVariant,
650
- loadingComponentHtml
738
+ loadingComponentHtml,
739
+ models,
740
+ defaultModelId: effectiveModelId
651
741
  })
652
742
  ]
653
743
  })] }), document.body);
@@ -721,7 +811,7 @@ function getContainerStyles(style, position) {
721
811
  function getAnimation(style) {
722
812
  return style === "modal" ? "fd-ai-float-center-in 200ms ease-out" : "fd-ai-float-in 200ms ease-out";
723
813
  }
724
- function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
814
+ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
725
815
  const [mounted, setMounted] = useState(false);
726
816
  const [isOpen, setIsOpen] = useState(false);
727
817
  const [messages, setMessages] = useState([]);
@@ -762,7 +852,9 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
762
852
  loaderVariant,
763
853
  loadingComponentHtml,
764
854
  triggerComponentHtml,
765
- position
855
+ position,
856
+ models,
857
+ defaultModelId
766
858
  });
767
859
  const btnPosition = BTN_POSITIONS[position] || BTN_POSITIONS["bottom-right"];
768
860
  const isModal = floatingStyle === "modal";
@@ -826,11 +918,16 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
826
918
  }))
827
919
  ] }), document.body);
828
920
  }
829
- function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, triggerComponentHtml, position }) {
921
+ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, triggerComponentHtml, position, models, defaultModelId }) {
830
922
  const label = aiLabel || "AI";
831
923
  const inputRef = useRef(null);
832
924
  const listRef = useRef(null);
833
925
  const btnPosition = BTN_POSITIONS[position] || BTN_POSITIONS["bottom-right"];
926
+ const [selectedModel, setSelectedModel] = useState(() => {
927
+ const trimmed = (defaultModelId ?? "").trim();
928
+ return trimmed.length > 0 ? trimmed : void 0;
929
+ });
930
+ const effectiveModelId = selectedModel || (Array.isArray(models) && models.length > 0 ? models[0].id : void 0);
834
931
  useEffect(() => {
835
932
  if (isOpen) setTimeout(() => inputRef.current?.focus(), 100);
836
933
  }, [isOpen]);
@@ -857,10 +954,13 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
857
954
  const res = await fetch(api, {
858
955
  method: "POST",
859
956
  headers: { "Content-Type": "application/json" },
860
- body: JSON.stringify({ messages: newMessages.map((m) => ({
861
- role: m.role,
862
- content: m.content
863
- })) })
957
+ body: JSON.stringify({
958
+ messages: newMessages.map((m) => ({
959
+ role: m.role,
960
+ content: m.content
961
+ })),
962
+ model: effectiveModelId
963
+ })
864
964
  });
865
965
  if (!res.ok) {
866
966
  let errMsg = "Something went wrong.";
@@ -916,7 +1016,8 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
916
1016
  isStreaming,
917
1017
  setMessages,
918
1018
  setAiInput,
919
- setIsStreaming
1019
+ setIsStreaming,
1020
+ effectiveModelId
920
1021
  ]);
921
1022
  const canSend = !!(aiInput.trim() && !isStreaming);
922
1023
  const showSuggestions = messages.length === 0 && !isStreaming;
@@ -977,6 +1078,15 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
977
1078
  }) : /* @__PURE__ */ jsxs("div", {
978
1079
  className: "fd-ai-fm-input-container",
979
1080
  children: [
1081
+ Array.isArray(models) && models.length > 0 && /* @__PURE__ */ jsx("div", {
1082
+ className: "fd-ai-model-select-row fd-ai-model-select-row--fm",
1083
+ children: /* @__PURE__ */ jsx(ModelSelector, {
1084
+ models,
1085
+ selectedId: effectiveModelId ?? models[0].id,
1086
+ onChange: setSelectedModel,
1087
+ disabled: isStreaming
1088
+ })
1089
+ }),
980
1090
  /* @__PURE__ */ jsxs("div", {
981
1091
  className: "fd-ai-fm-input-wrap",
982
1092
  children: [/* @__PURE__ */ jsx("textarea", {
@@ -1052,7 +1162,7 @@ function TrashIcon() {
1052
1162
  ]
1053
1163
  });
1054
1164
  }
1055
- function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
1165
+ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
1056
1166
  const [messages, setMessages] = useState([]);
1057
1167
  const [aiInput, setAiInput] = useState("");
1058
1168
  const [isStreaming, setIsStreaming] = useState(false);
@@ -1120,7 +1230,9 @@ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestio
1120
1230
  suggestedQuestions,
1121
1231
  aiLabel,
1122
1232
  loaderVariant,
1123
- loadingComponentHtml
1233
+ loadingComponentHtml,
1234
+ models,
1235
+ defaultModelId
1124
1236
  }),
1125
1237
  /* @__PURE__ */ jsx("div", {
1126
1238
  className: "fd-ai-modal-footer",
@@ -10,6 +10,11 @@ interface DocsAIFeaturesProps {
10
10
  aiLabel?: string;
11
11
  loaderVariant?: string;
12
12
  loadingComponentHtml?: string;
13
+ models?: {
14
+ id: string;
15
+ label: string;
16
+ }[];
17
+ defaultModelId?: string;
13
18
  }
14
19
  declare function DocsAIFeatures({
15
20
  mode,
@@ -19,7 +24,9 @@ declare function DocsAIFeatures({
19
24
  suggestedQuestions,
20
25
  aiLabel,
21
26
  loaderVariant,
22
- loadingComponentHtml
27
+ loadingComponentHtml,
28
+ models,
29
+ defaultModelId
23
30
  }: DocsAIFeaturesProps): react_jsx_runtime0.JSX.Element;
24
31
  //#endregion
25
32
  export { DocsAIFeatures };
@@ -19,18 +19,22 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
19
  * This component is rendered inside the docs layout so the user's root layout
20
20
  * never needs to be modified — AI features work purely from `docs.config.tsx`.
21
21
  */
22
- function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
22
+ function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
23
23
  if (mode === "search") return /* @__PURE__ */ jsx(SearchModeAI, {
24
24
  suggestedQuestions,
25
25
  aiLabel,
26
26
  loaderVariant,
27
- loadingComponentHtml
27
+ loadingComponentHtml,
28
+ models,
29
+ defaultModelId
28
30
  });
29
31
  if (mode === "sidebar-icon") return /* @__PURE__ */ jsx(SidebarIconModeAI, {
30
32
  suggestedQuestions,
31
33
  aiLabel,
32
34
  loaderVariant,
33
- loadingComponentHtml
35
+ loadingComponentHtml,
36
+ models,
37
+ defaultModelId
34
38
  });
35
39
  return /* @__PURE__ */ jsx(FloatingAIChat, {
36
40
  api: "/api/docs",
@@ -40,10 +44,12 @@ function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "pane
40
44
  suggestedQuestions,
41
45
  aiLabel,
42
46
  loaderVariant,
43
- loadingComponentHtml
47
+ loadingComponentHtml,
48
+ models,
49
+ defaultModelId
44
50
  });
45
51
  }
46
- function SearchModeAI({ suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
52
+ function SearchModeAI({ suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
47
53
  const [open, setOpen] = useState(false);
48
54
  useEffect(() => {
49
55
  function handler(e) {
@@ -79,10 +85,12 @@ function SearchModeAI({ suggestedQuestions, aiLabel, loaderVariant, loadingCompo
79
85
  suggestedQuestions,
80
86
  aiLabel,
81
87
  loaderVariant,
82
- loadingComponentHtml
88
+ loadingComponentHtml,
89
+ models,
90
+ defaultModelId
83
91
  });
84
92
  }
85
- function SidebarIconModeAI({ suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
93
+ function SidebarIconModeAI({ suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId }) {
86
94
  const [searchOpen, setSearchOpen] = useState(false);
87
95
  const [aiOpen, setAiOpen] = useState(false);
88
96
  useEffect(() => {
@@ -118,7 +126,9 @@ function SidebarIconModeAI({ suggestedQuestions, aiLabel, loaderVariant, loading
118
126
  suggestedQuestions,
119
127
  aiLabel,
120
128
  loaderVariant,
121
- loadingComponentHtml
129
+ loadingComponentHtml,
130
+ models,
131
+ defaultModelId
122
132
  }), /* @__PURE__ */ jsx(AIModalDialog, {
123
133
  open: aiOpen,
124
134
  onOpenChange: setAiOpen,
@@ -126,7 +136,9 @@ function SidebarIconModeAI({ suggestedQuestions, aiLabel, loaderVariant, loading
126
136
  suggestedQuestions,
127
137
  aiLabel,
128
138
  loaderVariant,
129
- loadingComponentHtml
139
+ loadingComponentHtml,
140
+ models,
141
+ defaultModelId
130
142
  })] });
131
143
  }
132
144
 
@@ -18,11 +18,27 @@
18
18
  * export const revalidate = false;
19
19
  * ```
20
20
  */
21
+ interface AIProviderConfig {
22
+ baseUrl: string;
23
+ apiKey?: string;
24
+ }
25
+ interface AIModelEntry {
26
+ id: string;
27
+ label: string;
28
+ provider?: string;
29
+ }
30
+ interface AIModelConfig {
31
+ models?: AIModelEntry[];
32
+ defaultModel?: string;
33
+ }
21
34
  interface AIOptions {
22
35
  enabled?: boolean;
23
- model?: string;
36
+ model?: string | AIModelConfig;
37
+ providers?: Record<string, AIProviderConfig>;
24
38
  systemPrompt?: string;
39
+ /** Default baseUrl when no per-model provider is configured. */
25
40
  baseUrl?: string;
41
+ /** Default apiKey when no per-model provider is configured. */
26
42
  apiKey?: string;
27
43
  maxResults?: number;
28
44
  }
package/dist/docs-api.mjs CHANGED
@@ -108,9 +108,26 @@ function scanDocsDir(docsDir, entry) {
108
108
  return indexes;
109
109
  }
110
110
  const DEFAULT_SYSTEM_PROMPT = `You are a helpful documentation assistant. Answer questions based on the provided documentation context. Be concise and accurate. If the answer is not in the context, say so honestly. Use markdown formatting for code examples and links.`;
111
+ function resolveModelAndProvider(aiConfig, requestedModelId) {
112
+ const raw = aiConfig.model;
113
+ const modelList = typeof raw === "object" && raw?.models || [];
114
+ let modelId = requestedModelId;
115
+ if (!modelId) {
116
+ if (typeof raw === "string") modelId = raw;
117
+ else if (typeof raw === "object") modelId = raw.defaultModel ?? raw.models?.[0]?.id;
118
+ if (!modelId) modelId = "gpt-4o-mini";
119
+ }
120
+ const providerKey = modelList.find((m) => m.id === modelId)?.provider;
121
+ const providerConfig = providerKey && aiConfig.providers?.[providerKey];
122
+ const baseUrl = (providerConfig && providerConfig.baseUrl || aiConfig.baseUrl || "https://api.openai.com/v1").replace(/\/$/, "");
123
+ const apiKey = providerConfig && providerConfig.apiKey || aiConfig.apiKey || process.env.OPENAI_API_KEY;
124
+ return {
125
+ model: modelId,
126
+ baseUrl,
127
+ apiKey
128
+ };
129
+ }
111
130
  async function handleAskAI(request, indexes, searchServer, aiConfig) {
112
- const apiKey = aiConfig.apiKey ?? process.env.OPENAI_API_KEY;
113
- if (!apiKey) return Response.json({ error: `AI is enabled but no API key was found. Either set apiKey in your docs.config or add OPENAI_API_KEY to your .env.local file.` }, { status: 500 });
114
131
  let body;
115
132
  try {
116
133
  body = await request.json();
@@ -139,16 +156,16 @@ async function handleAskAI(request, indexes, searchServer, aiConfig) {
139
156
  role: "system",
140
157
  content: context ? `${systemPrompt}\n\n---\n\nDocumentation context:\n\n${context}` : systemPrompt
141
158
  }, ...messages.filter((m) => m.role !== "system")];
142
- const baseUrl = (aiConfig.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
143
- const model = aiConfig.model ?? "gpt-4o-mini";
144
- const llmResponse = await fetch(`${baseUrl}/chat/completions`, {
159
+ const resolved = resolveModelAndProvider(aiConfig, typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0);
160
+ if (!resolved.apiKey) return Response.json({ error: `AI is enabled but no API key was found. Either set apiKey in your docs.config ai section, configure a provider, or add OPENAI_API_KEY to your .env.local file.` }, { status: 500 });
161
+ const llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
145
162
  method: "POST",
146
163
  headers: {
147
164
  "Content-Type": "application/json",
148
- Authorization: `Bearer ${apiKey}`
165
+ Authorization: `Bearer ${resolved.apiKey}`
149
166
  },
150
167
  body: JSON.stringify({
151
- model,
168
+ model: resolved.model,
152
169
  stream: true,
153
170
  messages: llmMessages
154
171
  })
@@ -434,6 +434,13 @@ function createDocsLayout(config) {
434
434
  const aiLabel = aiConfig?.aiLabel;
435
435
  const aiLoaderVariant = aiConfig?.loader;
436
436
  const aiLoadingComponentHtml = typeof aiConfig?.loadingComponent === "function" ? serializeIcon(aiConfig.loadingComponent({ name: aiLabel || "AI" })) : void 0;
437
+ const rawModelConfig = aiConfig?.model;
438
+ let aiModels = aiConfig?.models;
439
+ let aiDefaultModelId = aiConfig?.defaultModel ?? (typeof aiConfig?.model === "string" ? aiConfig.model : void 0);
440
+ if (rawModelConfig && typeof rawModelConfig === "object") {
441
+ aiModels = rawModelConfig.models ?? aiModels;
442
+ aiDefaultModelId = rawModelConfig.defaultModel ?? rawModelConfig.models?.[0]?.id ?? aiDefaultModelId;
443
+ }
437
444
  const lastModifiedMap = buildLastModifiedMap(config.entry);
438
445
  const descriptionMap = buildDescriptionMap(config.entry);
439
446
  return function DocsLayoutWrapper({ children }) {
@@ -467,7 +474,9 @@ function createDocsLayout(config) {
467
474
  suggestedQuestions: aiSuggestedQuestions,
468
475
  aiLabel,
469
476
  loaderVariant: aiLoaderVariant,
470
- loadingComponentHtml: aiLoadingComponentHtml
477
+ loadingComponentHtml: aiLoadingComponentHtml,
478
+ models: aiModels,
479
+ defaultModelId: aiDefaultModelId
471
480
  }),
472
481
  /* @__PURE__ */ jsx(DocsPageClient, {
473
482
  tocEnabled,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.0.3-beta.2",
3
+ "version": "0.0.3-beta.4",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -98,7 +98,7 @@
98
98
  "next": ">=14.0.0",
99
99
  "tsdown": "^0.20.3",
100
100
  "typescript": "^5.9.3",
101
- "@farming-labs/docs": "0.0.3-beta.2"
101
+ "@farming-labs/docs": "0.0.3-beta.4"
102
102
  },
103
103
  "peerDependencies": {
104
104
  "@farming-labs/docs": ">=0.0.1",