@farming-labs/theme 0.0.3-beta.3 → 0.0.3-beta.5

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.3",
3
+ "version": "0.0.3-beta.5",
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.3"
101
+ "@farming-labs/docs": "0.0.3-beta.5"
102
102
  },
103
103
  "peerDependencies": {
104
104
  "@farming-labs/docs": ">=0.0.1",
package/styles/ai.css CHANGED
@@ -345,6 +345,93 @@
345
345
  border-top: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.06));
346
346
  }
347
347
 
348
+ /* ─── Model selector dropdown (matches "Open in" page-action style) ── */
349
+
350
+ .fd-ai-model-select-row {
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: flex-end;
354
+ position: relative;
355
+ overflow: visible;
356
+ margin-bottom: 6px;
357
+ }
358
+
359
+ .fd-ai-model-select-row--fm {
360
+ padding: 8px 12px 0;
361
+ }
362
+
363
+ .fd-ai-model-dropdown {
364
+ position: relative;
365
+ margin-left: auto;
366
+ }
367
+
368
+ .fd-ai-model-dropdown-btn {
369
+ display: inline-flex;
370
+ align-items: center;
371
+ gap: 6px;
372
+ padding: 4px 10px;
373
+ font-size: 12px;
374
+ color: var(--color-fd-muted-foreground, #71717a);
375
+ background: transparent;
376
+ border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.12));
377
+ border-radius: var(--radius, 6px);
378
+ cursor: pointer;
379
+ transition: all 150ms;
380
+ }
381
+
382
+ .fd-ai-model-dropdown-btn:hover {
383
+ color: var(--color-fd-foreground, #e4e4e7);
384
+ border-color: var(--color-fd-border, rgba(255, 255, 255, 0.25));
385
+ }
386
+
387
+ .fd-ai-model-dropdown-btn:disabled {
388
+ opacity: 0.5;
389
+ cursor: not-allowed;
390
+ }
391
+
392
+ .fd-ai-model-dropdown-menu {
393
+ position: absolute;
394
+ bottom: calc(100% + 6px);
395
+ right: 0;
396
+ z-index: 99999;
397
+ min-width: 220px;
398
+ padding: 4px;
399
+ background: var(--color-fd-popover, var(--color-fd-background, #0c0c0c));
400
+ border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
401
+ border-radius: var(--radius, 8px);
402
+ box-shadow: 2px 2px 0 0 var(--color-fd-border, #262626);
403
+ animation: fd-ai-fade-in 100ms ease-out;
404
+ }
405
+
406
+
407
+ .fd-ai-model-dropdown-item {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 8px;
411
+ width: 100%;
412
+ padding: 6px 10px;
413
+ border-bottom: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
414
+ font-size: 13px;
415
+ font-family: inherit;
416
+ color: var(--color-fd-popover-foreground, var(--color-fd-foreground, #e4e4e7));
417
+ background: transparent;
418
+ cursor: pointer;
419
+ transition: background 100ms;
420
+ }
421
+
422
+ .fd-ai-model-dropdown-item:hover {
423
+ background: var(--color-fd-accent, rgba(255, 255, 255, 0.06));
424
+ }
425
+
426
+ .fd-ai-model-dropdown-item[data-active="true"] {
427
+ color: var(--color-fd-primary, #6366f1);
428
+ }
429
+
430
+ .fd-ai-model-dropdown-label {
431
+ flex: 1;
432
+ text-align: left;
433
+ }
434
+
348
435
  .fd-ai-clear-btn {
349
436
  font-size: 11px;
350
437
  color: var(--color-fd-muted-foreground, #71717a);
@@ -840,6 +927,43 @@
840
927
  min-height: 1.2em;
841
928
  }
842
929
 
930
+ :root:not(.dark) .fd-ai-code-block {
931
+ --sh-class: #b45309;
932
+ --sh-identifier: #1f2937;
933
+ --sh-keyword: #7c3aed;
934
+ --sh-string: #0d9488;
935
+ --sh-property: #dc2626;
936
+ --sh-entity: #059669;
937
+ --sh-sign: #374151;
938
+ --sh-comment: #6b7280;
939
+ --sh-jsxliterals: #7c3aed;
940
+ border-color: rgba(0, 0, 0, 0.12);
941
+ background: #f4f4f5;
942
+ }
943
+
944
+ :root:not(.dark) .fd-ai-code-header {
945
+ border-bottom-color: rgba(0, 0, 0, 0.1);
946
+ background: color-mix(in srgb, #e4e4e7 80%, transparent);
947
+ }
948
+
949
+ :root:not(.dark) .fd-ai-code-lang {
950
+ color: #52525b;
951
+ }
952
+
953
+ :root:not(.dark) .fd-ai-code-copy {
954
+ color: #52525b;
955
+ border-color: rgba(0, 0, 0, 0.15);
956
+ }
957
+
958
+ :root:not(.dark) .fd-ai-code-copy:hover {
959
+ color: #18181b;
960
+ background: color-mix(in srgb, #d4d4d8 60%, transparent);
961
+ }
962
+
963
+ :root:not(.dark) .fd-ai-code-block code {
964
+ color: #1f2937;
965
+ }
966
+
843
967
  /* ═══════════════════════════════════════════════════════════════════
844
968
  * Full-Modal (better-auth inspired) — fd-ai-fm-*
845
969
  * ═══════════════════════════════════════════════════════════════════ */
@@ -948,6 +1072,11 @@
948
1072
  font-size: 0.875em;
949
1073
  font-family: var(--fd-font-mono, ui-monospace, monospace);
950
1074
  }
1075
+ .fd-ai-code-block pre {
1076
+ padding-left: -10px !important;
1077
+ padding-right: -10px !important;
1078
+ padding-bottom: 2px !important;
1079
+ }
951
1080
 
952
1081
  .fd-ai-fm-msg-content .fd-ai-code-block code {
953
1082
  background: transparent;
@@ -1015,7 +1144,7 @@
1015
1144
  border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
1016
1145
  background: var(--color-fd-background, #0c0c0c);
1017
1146
  box-shadow: 0 20px 60px color-mix(in srgb, var(--color-fd-background, #000) 70%, transparent);
1018
- overflow: hidden;
1147
+ overflow: visible;
1019
1148
  }
1020
1149
 
1021
1150
  .fd-ai-fm-input-wrap {
package/styles/base.css CHANGED
@@ -440,6 +440,12 @@ figure.shiki:has(figcaption) figcaption {
440
440
  text-decoration: none;
441
441
  }
442
442
 
443
+ fd-ai-code-block pre {
444
+ padding-left: -10px !important;
445
+ padding-right: -10px !important;
446
+ padding-bottom: 2px !important;
447
+ }
448
+
443
449
  /* ─── Code block copy button: show on hover ────────────────────────── */
444
450
 
445
451
  figure.shiki > button,
@@ -431,6 +431,23 @@ article a[class*="text-fd-muted-foreground"] {
431
431
  border-radius: 2px;
432
432
  }
433
433
 
434
+ /* Light mode: AI code blocks — light bg + dark syntax */
435
+ :root:not(.dark) .fd-ai-code-block {
436
+ --sh-class: #b45309;
437
+ --sh-identifier: #1f2937;
438
+ --sh-keyword: #7c3aed;
439
+ --sh-string: #0d9488;
440
+ --sh-property: #dc2626;
441
+ --sh-entity: #059669;
442
+ --sh-sign: #374151;
443
+ --sh-comment: #6b7280;
444
+ --sh-jsxliterals: #7c3aed;
445
+ }
446
+
447
+ :root:not(.dark) .fd-ai-code-block code {
448
+ color: #1f2937;
449
+ }
450
+
434
451
  /* ─── Omni Command Palette (darksharp theme) ────────────────────── */
435
452
 
436
453
  .omni-content {
@@ -5,6 +5,9 @@
5
5
 
6
6
  .fd-page-action-btn {
7
7
  border-radius: 0.375rem;
8
+ box-shadow: none;
9
+ text-transform: capitalize;
10
+ font-family: var(--fd-font-sans, var(--font-geist-sans, ui-sans-serif, system-ui, sans-serif));
8
11
  }
9
12
 
10
13
  .fd-page-action-menu {
@@ -77,11 +77,11 @@ h3 {
77
77
  aside,
78
78
  #nd-sidebar,
79
79
  aside#nd-sidebar,
80
- #nd-docs-layout aside {
80
+ #nd-docs-layout aside,
81
+ .fd-sidebar {
81
82
  border: none;
82
- border-right: none;
83
+ border-right: 1px solid var(--color-fd-border);
83
84
  border-left: none;
84
- border-color: transparent;
85
85
  box-shadow: none;
86
86
  background: var(--color-fd-background);
87
87
  }
@@ -104,8 +104,8 @@ aside a[data-active]:hover {
104
104
  background: var(--color-fd-accent);
105
105
  }
106
106
 
107
- /* Active sidebar item — green background like Mintlify */
108
107
  aside a[data-active="true"] {
108
+ position: relative;
109
109
  color: var(--color-fd-primary);
110
110
  font-weight: 600;
111
111
  background: rgba(13, 147, 115, 0.08);
@@ -119,8 +119,15 @@ aside a[data-active="true"] {
119
119
  }
120
120
 
121
121
  aside a[data-active="true"]::before {
122
- background-color: transparent;
123
- width: 0;
122
+ content: "";
123
+ display: block;
124
+ position: absolute;
125
+ left: 9;
126
+ top: 20%;
127
+ bottom: 20%;
128
+ width: 3px;
129
+ border-radius: 2px;
130
+ background: var(--color-fd-primary);
124
131
  }
125
132
 
126
133
  /* ── Flat sidebar — category headings ─────────────────────────── */
@@ -162,10 +169,12 @@ aside button[class*="bg-fd-secondary"] {
162
169
  font-size: 0.875rem;
163
170
  }
164
171
 
165
- /* Sidebar footer border removal */
172
+ /* Sidebar footer border */
166
173
  aside .border-t,
167
- aside [class*="border-t"] {
168
- border-top-color: transparent;
174
+ aside [class*="border-t"],
175
+ .fd-sidebar .fd-sidebar-footer,
176
+ .fd-sidebar .fd-sidebar-footer-custom {
177
+ border-top: 1px solid var(--color-fd-border);
169
178
  }
170
179
 
171
180
  /* ═══════════════════════════════════════════════════════════════════
@@ -324,7 +333,6 @@ nav[class*="header"] {
324
333
  * ═══════════════════════════════════════════════════════════════════ */
325
334
 
326
335
  figure.shiki {
327
- border-radius: 10px;
328
336
  overflow: hidden;
329
337
  border: 1px solid var(--color-fd-border);
330
338
  }
@@ -475,6 +475,95 @@ hr {
475
475
  font-family: var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace)) !important;
476
476
  }
477
477
 
478
+ /* model selector */
479
+ .fd-ai-model-select-row {
480
+ display: flex;
481
+ align-items: center;
482
+ justify-content: flex-end;
483
+ position: relative;
484
+ overflow: visible;
485
+ margin-bottom: 6px;
486
+ }
487
+
488
+ .fd-ai-model-select-row--fm {
489
+ padding: 8px 12px 0;
490
+ }
491
+
492
+ .fd-ai-model-dropdown {
493
+ position: relative;
494
+ margin-left: auto;
495
+ }
496
+
497
+ .fd-ai-model-dropdown-btn {
498
+ display: inline-flex;
499
+ align-items: center;
500
+ gap: 6px;
501
+ padding: 4px 10px;
502
+ font-size: 12px;
503
+ text-transform: uppercase;
504
+ font-family: var(--fd-font-mono, ui-monospace, monospace);
505
+ color: var(--color-fd-muted-foreground, #71717a);
506
+ background: transparent;
507
+ border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.12));
508
+ border-radius: var(--radius, 6px);
509
+ cursor: pointer;
510
+ transition: all 150ms;
511
+ }
512
+
513
+ .fd-ai-model-dropdown-btn:hover {
514
+ color: var(--color-fd-foreground, #e4e4e7);
515
+ border-color: var(--color-fd-border, rgba(255, 255, 255, 0.25));
516
+ }
517
+
518
+ .fd-ai-model-dropdown-btn:disabled {
519
+ opacity: 0.5;
520
+ cursor: not-allowed;
521
+ }
522
+
523
+ .fd-ai-model-dropdown-menu {
524
+ position: absolute;
525
+ bottom: calc(100% + 6px);
526
+ right: 0;
527
+ z-index: 99999;
528
+ min-width: 220px;
529
+ padding: 4px;
530
+ background: var(--color-fd-popover, var(--color-fd-background, #0c0c0c));
531
+ border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
532
+ border-radius: var(--radius, 8px);
533
+ box-shadow: 2px 2px 0 0 var(--color-fd-border, #262626);
534
+ animation: fd-ai-fade-in 100ms ease-out;
535
+ }
536
+
537
+
538
+ .fd-ai-model-dropdown-item {
539
+ display: flex;
540
+ align-items: center;
541
+ gap: 8px;
542
+ width: 100%;
543
+ color: var(--color-fd-muted-foreground, #71717a);
544
+ padding: 6px 10px;
545
+ font-family: var(--fd-font-mono, ui-monospace, monospace);
546
+ text-transform: uppercase;
547
+ border-bottom: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
548
+ font-size: 11px;
549
+ background: transparent;
550
+ cursor: pointer;
551
+ transition: background 100ms;
552
+ }
553
+
554
+ .fd-ai-model-dropdown-item:hover {
555
+ background: var(--color-fd-accent, rgba(255, 255, 255, 0.06));
556
+ }
557
+
558
+ .fd-ai-model-dropdown-item[data-active="true"] {
559
+ color: var(--color-fd-primary, #6366f1);
560
+ }
561
+
562
+ .fd-ai-model-dropdown-label {
563
+ flex: 1;
564
+ text-align: left;
565
+ }
566
+
478
567
  /* ─── AI Chat (pixel-border — zero radius, pixel-art, monospace) ── */
479
568
 
480
569
  .fd-ai-dialog {
@@ -611,7 +700,6 @@ hr {
611
700
  font-size: 12px;
612
701
  box-shadow: 3px 3px 0 0 var(--color-fd-border, hsl(0 0% 15%));
613
702
  }
614
-
615
703
  .fd-ai-fm-trigger-btn:hover {
616
704
  transform: translate(-1px, -1px);
617
705
  box-shadow: 4px 4px 0 0 var(--color-fd-border, hsl(0 0% 15%));
@@ -687,6 +775,49 @@ hr {
687
775
  font-family: var(--fd-font-mono, ui-monospace, monospace);
688
776
  }
689
777
 
778
+ :root:not(.dark) .fd-ai-code-block {
779
+ background: #f4f4f5 !important;
780
+ padding: 0 !important;
781
+ box-shadow: 3px 3px 0 0 hsl(0 0% 75%);
782
+ --sh-class: #b45309;
783
+ --sh-identifier: #1f2937;
784
+ --sh-keyword: #7c3aed;
785
+ --sh-string: #0d9488;
786
+ --sh-property: #dc2626;
787
+ --sh-entity: #059669;
788
+ --sh-sign: #374151;
789
+ --sh-comment: #6b7280;
790
+ --sh-jsxliterals: #7c3aed;
791
+ }
792
+ .fd-ai-code-block pre {
793
+ padding-left: -10px !important;
794
+ padding-right: -10px !important;
795
+ padding-bottom: 2px !important;
796
+ }
797
+
798
+ :root:not(.dark) .fd-ai-code-header {
799
+ border-bottom-color: hsl(0 0% 80%);
800
+ background: color-mix(in srgb, #e4e4e7 90%, transparent);
801
+ }
802
+
803
+ :root:not(.dark) .fd-ai-code-lang {
804
+ color: #52525b;
805
+ }
806
+
807
+ :root:not(.dark) .fd-ai-code-copy {
808
+ color: #52525b;
809
+ border-color: hsl(0 0% 75%);
810
+ }
811
+
812
+ :root:not(.dark) .fd-ai-code-copy:hover {
813
+ color: #18181b;
814
+ background: color-mix(in srgb, #d4d4d8 70%, transparent);
815
+ }
816
+
817
+ :root:not(.dark) .fd-ai-code-block code {
818
+ color: #1f2937;
819
+ }
820
+
690
821
  /* ─── Omni Command Palette (pixel-border theme) ─────────────────── */
691
822
 
692
823
  .omni-content {