@arcote.tech/arc-ds 0.5.2 → 0.5.6

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-ds",
3
3
  "type": "module",
4
- "version": "0.5.2",
4
+ "version": "0.5.6",
5
5
  "private": false,
6
6
  "author": "Przemysław Krasiński [arcote.tech]",
7
7
  "description": "Design System for Arc framework — CVA-based components with display modes and variant overrides",
@@ -25,10 +25,12 @@
25
25
  "dependencies": {
26
26
  "class-variance-authority": "^0.7.1",
27
27
  "clsx": "^2.1.1",
28
+ "react-markdown": "^10.1.0",
29
+ "remark-gfm": "^4.0.1",
28
30
  "tailwind-merge": "^3.5.0"
29
31
  },
30
32
  "peerDependencies": {
31
- "@arcote.tech/arc": "^0.5.2",
33
+ "@arcote.tech/arc": "^0.5.6",
32
34
  "framer-motion": "^12.0.0",
33
35
  "lucide-react": ">=0.400.0",
34
36
  "radix-ui": "^1.0.0",
@@ -1,5 +1,4 @@
1
1
  import { Box } from "../box/box";
2
- import { useBentoSpan } from "../bento-grid/bento-grid";
3
2
  import { motion } from "framer-motion";
4
3
  import { Plus } from "lucide-react";
5
4
  import type { ComponentType, ReactNode } from "react";
@@ -13,7 +12,6 @@ export interface BentoCardProps {
13
12
  onEdit: () => void;
14
13
  emptyLabel?: ReactNode;
15
14
  className?: string;
16
- span?: string;
17
15
  children?: ReactNode;
18
16
  }
19
17
 
@@ -26,45 +24,39 @@ export function BentoCard({
26
24
  onEdit,
27
25
  emptyLabel,
28
26
  className,
29
- span,
30
27
  children,
31
28
  }: BentoCardProps) {
32
- const autoSpan = useBentoSpan();
33
- const resolvedSpan = span ?? autoSpan;
34
-
35
29
  return (
36
- <div className={`h-full ${resolvedSpan} ${className ?? ""}`.trim()}>
37
- <motion.div
38
- layoutId={layoutId}
39
- className="h-full"
40
- style={{ opacity: isModalOpen ? 0 : 1 }}
30
+ <motion.div
31
+ layoutId={layoutId}
32
+ className={className}
33
+ style={{ opacity: isModalOpen ? 0 : 1 }}
34
+ >
35
+ <Box
36
+ layout={false}
37
+ className="group relative cursor-pointer p-5 hover:shadow-lg transition-all duration-300 hover:scale-[1.01]"
38
+ onClick={onEdit}
41
39
  >
42
- <Box
43
- layout={false}
44
- className="group relative cursor-pointer p-5 hover:shadow-lg transition-all duration-300 h-full min-h-[140px] hover:scale-[1.01]"
45
- onClick={onEdit}
46
- >
47
- <div className="flex items-center gap-2.5 mb-3">
48
- <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10">
49
- <Icon className="h-4 w-4 text-primary" />
50
- </div>
51
- <span className="text-sm font-semibold">{title}</span>
40
+ <div className="flex items-center gap-2.5 mb-3">
41
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10">
42
+ <Icon className="h-4 w-4 text-primary" />
52
43
  </div>
44
+ <span className="text-sm font-semibold">{title}</span>
45
+ </div>
53
46
 
54
- {isEmpty ? (
55
- <div className="flex flex-col items-center justify-center gap-2 py-4 border-2 border-dashed border-muted-foreground/15 rounded-xl transition-colors group-hover:border-primary/30">
56
- <div className="flex h-9 w-9 items-center justify-center rounded-full bg-muted transition-colors group-hover:bg-primary/10">
57
- <Plus className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-primary" />
58
- </div>
59
- {emptyLabel && (
60
- <span className="text-xs text-muted-foreground">{emptyLabel}</span>
61
- )}
47
+ {isEmpty ? (
48
+ <div className="flex flex-col items-center justify-center gap-2 py-4 border-2 border-dashed border-muted-foreground/15 rounded-xl transition-colors group-hover:border-primary/30">
49
+ <div className="flex h-9 w-9 items-center justify-center rounded-full bg-muted transition-colors group-hover:bg-primary/10">
50
+ <Plus className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-primary" />
62
51
  </div>
63
- ) : (
64
- <div className="flex flex-col gap-2">{children}</div>
65
- )}
66
- </Box>
67
- </motion.div>
68
- </div>
52
+ {emptyLabel && (
53
+ <span className="text-xs text-muted-foreground">{emptyLabel}</span>
54
+ )}
55
+ </div>
56
+ ) : (
57
+ <div className="flex flex-col gap-2">{children}</div>
58
+ )}
59
+ </Box>
60
+ </motion.div>
69
61
  );
70
62
  }
@@ -1,51 +1,38 @@
1
- import { Children, createContext, useContext, type ReactNode } from "react";
1
+ import { Children, useMemo, type ReactNode } from "react";
2
2
 
3
3
  export interface BentoGridProps {
4
4
  children: ReactNode;
5
5
  className?: string;
6
6
  }
7
7
 
8
- type BentoGridItemContext = {
9
- index: number;
10
- count: number;
11
- };
12
-
13
- const BentoItemContext = createContext<BentoGridItemContext | null>(null);
14
-
15
- /**
16
- * Hook for children to read their grid span class.
17
- * Returns the appropriate Tailwind class (e.g., "md:row-span-2", "md:col-span-3")
18
- * or empty string for normal cells.
19
- */
20
- export function useBentoSpan(): string {
21
- const ctx = useContext(BentoItemContext);
22
- if (!ctx) return "";
23
- const { index, count } = ctx;
24
-
25
- if (count < 5) return "";
26
- if (index === 0) return "md:row-span-2";
27
- if (index === count - 1 && (count === 6 || count === 8)) return "md:col-span-3";
28
- return "";
29
- }
30
-
31
8
  /**
32
- * Responsive bento grid with auto-layout based on child count.
9
+ * Responsive multi-column layout. Children are distributed round-robin into
10
+ * 3 columns; each column is a flex-col so items take their natural height.
11
+ * No row-spanning, no fixed row heights, no auto-layout magic — every item
12
+ * controls its own size and renders exactly as tall as its content.
33
13
  *
34
- * Children call useBentoSpan() to get their grid span class.
35
- * No wrapper divs children control their own layout.
14
+ * Breakpoints: 1 column (mobile), 2 (md), 3 (lg+). Children are always
15
+ * distributed across 3 buckets; on narrower viewports the buckets stack via
16
+ * the outer `grid-cols-*` utilities.
36
17
  */
37
18
  export function BentoGrid({ children, className }: BentoGridProps) {
38
- const items = Children.toArray(children);
39
- const count = items.length;
40
-
41
- if (count === 0) return null;
19
+ const columns = useMemo(() => {
20
+ const items = Children.toArray(children);
21
+ const cols: ReactNode[][] = [[], [], []];
22
+ items.forEach((item, i) => {
23
+ cols[i % 3].push(item);
24
+ });
25
+ return cols;
26
+ }, [children]);
42
27
 
43
28
  return (
44
- <div className={`grid grid-cols-1 md:grid-cols-3 gap-3 ${className ?? ""}`}>
45
- {items.map((child, index) => (
46
- <BentoItemContext.Provider key={index} value={{ index, count }}>
47
- {child}
48
- </BentoItemContext.Provider>
29
+ <div
30
+ className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 items-start ${className ?? ""}`}
31
+ >
32
+ {columns.map((col, i) => (
33
+ <div key={i} className="flex flex-col gap-3">
34
+ {col}
35
+ </div>
49
36
  ))}
50
37
  </div>
51
38
  );
@@ -21,6 +21,8 @@ export const buttonVariants = cva(
21
21
  "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22
22
  outline:
23
23
  "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
24
+ "outline-dashed":
25
+ "border border-dashed border-border/60 bg-transparent text-muted-foreground hover:border-border hover:text-foreground",
24
26
  destructive: "bg-destructive text-white hover:bg-destructive/90",
25
27
  link: "text-primary underline-offset-4 hover:underline",
26
28
  },
@@ -5,6 +5,7 @@ import { SearchSelect } from "../search-select/search-select";
5
5
  import { Send, Globe } from "lucide-react";
6
6
  import { useState, type ReactNode } from "react";
7
7
  import type { ChatModel, SendMessageOptions } from "./types";
8
+ import { useChatLabels } from "./chat-labels";
8
9
 
9
10
  interface ChatInputProps {
10
11
  onSend: (message: string, options: SendMessageOptions) => void;
@@ -12,10 +13,18 @@ interface ChatInputProps {
12
13
  defaultModel?: string;
13
14
  /** Extra toolbar content (e.g., attach menu) rendered after web search toggle */
14
15
  toolbar?: ReactNode;
15
- /** Label for web search toggle */
16
- webSearchLabel?: string;
17
- /** Placeholder for message input */
18
- placeholder?: string;
16
+ /** Show the model selector dropdown. Default true. */
17
+ showModelSelector?: boolean;
18
+ /** Show the web search toggle. Default true. */
19
+ showWebSearch?: boolean;
20
+ /**
21
+ * Render slot for the send button. Receives `onClick` and `disabled` —
22
+ * consumer wires them to its own button. Defaults to a `<Button icon={Send}>`.
23
+ */
24
+ renderSendButton?: (props: {
25
+ onClick: () => void;
26
+ disabled: boolean;
27
+ }) => ReactNode;
19
28
  /** Disable input (e.g., during generation) */
20
29
  disabled?: boolean;
21
30
  }
@@ -25,10 +34,12 @@ export function ChatInput({
25
34
  models,
26
35
  defaultModel,
27
36
  toolbar,
28
- webSearchLabel = "Web",
29
- placeholder = "Type a message...",
37
+ showModelSelector = true,
38
+ showWebSearch = true,
39
+ renderSendButton,
30
40
  disabled = false,
31
41
  }: ChatInputProps) {
42
+ const labels = useChatLabels();
32
43
  const [message, setMessage] = useState("");
33
44
  const [model, setModel] = useState(defaultModel ?? models[0]?.value ?? "");
34
45
  const [webSearch, setWebSearch] = useState(false);
@@ -41,51 +52,65 @@ export function ChatInput({
41
52
  setMessage("");
42
53
  };
43
54
 
55
+ // Hide the toolbar row entirely if there's nothing in it
56
+ const hasToolbar = showModelSelector || showWebSearch || toolbar;
57
+ const sendDisabled = disabled || !message.trim();
58
+
44
59
  return (
45
60
  <Box className={`p-3 space-y-2 ${disabled ? "opacity-50 pointer-events-none" : ""}`}>
46
61
  {/* Input + send */}
47
- <div className="flex items-end gap-2">
62
+ <div className="flex items-center gap-2">
48
63
  <div className="flex-1">
49
64
  <TextareaField
50
65
  value={message}
51
66
  onChange={(val) => setMessage(val ?? "")}
52
- placeholder={disabled ? "Generowanie..." : placeholder}
67
+ placeholder={disabled ? labels.placeholderGenerating : labels.placeholder}
53
68
  rows={1}
54
69
  />
55
70
  </div>
56
- <Button
57
- size="sm"
58
- icon={Send}
59
- onClick={handleSend}
60
- disabled={disabled || !message.trim()}
61
- />
71
+ {renderSendButton ? (
72
+ renderSendButton({ onClick: handleSend, disabled: sendDisabled })
73
+ ) : (
74
+ <Button
75
+ size="sm"
76
+ icon={Send}
77
+ onClick={handleSend}
78
+ disabled={sendDisabled}
79
+ />
80
+ )}
62
81
  </div>
63
82
 
64
83
  {/* Toolbar */}
65
- <div className="flex items-center gap-1.5">
66
- <div className="w-[130px]">
67
- <SearchSelect
68
- value={model}
69
- onChange={setModel}
70
- options={models}
71
- placeholder="Model..."
72
- position="absolute"
73
- direction="up"
74
- size="sm"
75
- allowClear={false}
76
- />
77
- </div>
84
+ {hasToolbar && (
85
+ <div className="flex items-center gap-1.5">
86
+ {showModelSelector && (
87
+ <div className="w-[130px]">
88
+ <SearchSelect
89
+ value={model}
90
+ onChange={setModel}
91
+ options={models}
92
+ placeholder={labels.modelPlaceholder}
93
+ position="absolute"
94
+ direction="up"
95
+ size="sm"
96
+ allowClear={false}
97
+ />
98
+ </div>
99
+ )}
78
100
 
79
- <Button
80
- variant={webSearch ? "default" : "ghost"}
81
- size="xs"
82
- icon={Globe}
83
- label={webSearchLabel}
84
- onClick={() => setWebSearch((v) => !v)}
85
- />
101
+ {showWebSearch && (
102
+ <Button
103
+ variant={webSearch ? "default" : "ghost"}
104
+ size="xs"
105
+ icon={Globe}
106
+ label={labels.webSearchLabel}
107
+ onClick={() => setWebSearch((v) => !v)}
108
+ />
109
+ )}
86
110
 
87
- {toolbar}
88
- </div>
111
+ {toolbar}
112
+ </div>
113
+ )}
89
114
  </Box>
90
115
  );
91
116
  }
@@ -0,0 +1,89 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+
3
+ export interface ChatLabels {
4
+ // ChatInput
5
+ placeholder: string;
6
+ placeholderGenerating: string;
7
+ modelPlaceholder: string;
8
+ webSearchLabel: string;
9
+ // QuestionTabs (wizard)
10
+ submitLabel: string;
11
+ customPlaceholder: string;
12
+ nextLabel: string;
13
+ backLabel: string;
14
+ summaryTitle: string;
15
+ editLabel: string;
16
+ noAnswerLabel: string;
17
+ questionOfLabel: (current: number, total: number) => string;
18
+ /** Secondary button in QuestionTabs — "Continue discussion" instead of answering */
19
+ discussLabel: string;
20
+ // ChatMessage
21
+ questionsLabel: string;
22
+ answerLabel: string;
23
+ // Tool execution log (rendered by chat-component)
24
+ toolCallingLabel: string;
25
+ toolDoneLabel: string;
26
+ errorLabel: string;
27
+ // askQuestions tool view
28
+ answerBelowLabel: string;
29
+ // completeStage tool view
30
+ stageCompleteLabel: string;
31
+ advanceStageLabel: string;
32
+ continueStageLabel: string;
33
+ stageAdvancedLabel: string;
34
+ stageContinuedLabel: string;
35
+ /** Label for the "Przejdź do następnego" button on the final stage. */
36
+ finishConsultationLabel: string;
37
+ }
38
+
39
+ export const defaultChatLabels: ChatLabels = {
40
+ placeholder: "Type a message...",
41
+ placeholderGenerating: "Generating...",
42
+ modelPlaceholder: "Model...",
43
+ webSearchLabel: "Web",
44
+ submitLabel: "Submit",
45
+ customPlaceholder: "Custom answer...",
46
+ nextLabel: "Next",
47
+ backLabel: "Back",
48
+ summaryTitle: "Summary",
49
+ editLabel: "Edit",
50
+ noAnswerLabel: "No answer",
51
+ questionOfLabel: (current, total) => `Question ${current} of ${total}`,
52
+ discussLabel: "Continue discussion",
53
+ questionsLabel: "questions to answer",
54
+ answerLabel: "Answer",
55
+ toolCallingLabel: "Executing...",
56
+ toolDoneLabel: "Done",
57
+ errorLabel: "Error",
58
+ answerBelowLabel: "Answer the questions below",
59
+ stageCompleteLabel: "Stage complete",
60
+ advanceStageLabel: "Go to next stage",
61
+ continueStageLabel: "Keep talking",
62
+ stageAdvancedLabel: "Moved to next stage",
63
+ stageContinuedLabel: "Staying on this stage",
64
+ finishConsultationLabel: "Finish consultation",
65
+ };
66
+
67
+ const ChatLabelsContext = createContext<ChatLabels>(defaultChatLabels);
68
+
69
+ export function ChatLabelsProvider({
70
+ labels,
71
+ children,
72
+ }: {
73
+ labels?: Partial<ChatLabels>;
74
+ children: ReactNode;
75
+ }) {
76
+ const merged = useMemo<ChatLabels>(
77
+ () => ({ ...defaultChatLabels, ...labels }),
78
+ [labels],
79
+ );
80
+ return (
81
+ <ChatLabelsContext.Provider value={merged}>
82
+ {children}
83
+ </ChatLabelsContext.Provider>
84
+ );
85
+ }
86
+
87
+ export function useChatLabels(): ChatLabels {
88
+ return useContext(ChatLabelsContext);
89
+ }
@@ -1,25 +1,23 @@
1
+ import ReactMarkdown from "react-markdown";
2
+ import remarkGfm from "remark-gfm";
1
3
  import { Button } from "../button/button";
2
4
  import { Bot, User, MessageSquare } from "lucide-react";
3
5
  import type { ChatMessageData } from "./types";
4
6
  import { ToolUseBlock } from "./tool-use-block";
7
+ import { useChatLabels } from "./chat-labels";
5
8
 
6
9
  interface ChatMessageProps {
7
10
  message: ChatMessageData;
8
11
  onAnswerQuestions?: () => void;
9
12
  onToolUseClick?: (link: string) => void;
10
- /** Label for questions count, e.g. "questions to answer" */
11
- questionsLabel?: string;
12
- /** Label for the answer button */
13
- answerLabel?: string;
14
13
  }
15
14
 
16
15
  export function ChatMessage({
17
16
  message,
18
17
  onAnswerQuestions,
19
18
  onToolUseClick,
20
- questionsLabel = "questions to answer",
21
- answerLabel = "Answer",
22
19
  }: ChatMessageProps) {
20
+ const { questionsLabel, answerLabel } = useChatLabels();
23
21
  const isUser = message.role === "user";
24
22
  const hasUnansweredQuestions =
25
23
  message.questions && message.questions.length > 0;
@@ -50,18 +48,11 @@ export function ChatMessage({
50
48
  : "bg-card border border-border rounded-tl-sm"
51
49
  }`}
52
50
  >
53
- {message.content.split("\n").map((line, i) => (
54
- <p key={i} className={i > 0 ? "mt-1.5" : ""}>
55
- {line.startsWith("• ") ? (
56
- <span className="flex items-start gap-1.5 text-left">
57
- <span className="text-primary mt-px">•</span>
58
- <span>{line.slice(2)}</span>
59
- </span>
60
- ) : (
61
- line
62
- )}
63
- </p>
64
- ))}
51
+ <div className="chat-markdown space-y-2 text-left [&_p]:m-0 [&_ul]:my-1 [&_ul]:pl-5 [&_ol]:my-1 [&_ol]:pl-5 [&_li]:my-0.5 [&_a]:text-primary [&_a]:underline [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto [&_strong]:font-semibold [&_em]:italic [&_h1]:text-base [&_h1]:font-semibold [&_h2]:text-sm [&_h2]:font-semibold [&_h3]:text-sm [&_h3]:font-medium [&_blockquote]:border-l-2 [&_blockquote]:border-primary/30 [&_blockquote]:pl-3 [&_blockquote]:italic">
52
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
53
+ {message.content}
54
+ </ReactMarkdown>
55
+ </div>
65
56
  {message.isStreaming && (
66
57
  <span className="inline-block w-1.5 h-4 bg-foreground/60 animate-pulse ml-0.5 -mb-0.5" />
67
58
  )}
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { CheckCircle2, Loader2, AlertCircle } from "lucide-react";
2
+ import { CheckCircle2, Loader2, AlertCircle, ArrowUpRight } from "lucide-react";
3
3
 
4
4
  export interface ChatToolLogProps {
5
5
  /** Tool label displayed in the header */
@@ -12,6 +12,13 @@ export interface ChatToolLogProps {
12
12
  icon?: string;
13
13
  /** Additional details rendered below the label */
14
14
  children?: ReactNode;
15
+ /**
16
+ * Optional click handler. When provided, the log becomes interactive
17
+ * (button) and shows a navigation indicator. Useful for jumping to
18
+ * the underlying domain object (e.g. the strategy page that the tool
19
+ * just mutated).
20
+ */
21
+ onClick?: () => void;
15
22
  }
16
23
 
17
24
  export function ChatToolLog({
@@ -19,6 +26,7 @@ export function ChatToolLog({
19
26
  calling,
20
27
  error,
21
28
  children,
29
+ onClick,
22
30
  }: ChatToolLogProps) {
23
31
  const hasError = !!error;
24
32
 
@@ -40,9 +48,17 @@ export function ChatToolLog({
40
48
  ? "text-blue-700 dark:text-blue-400"
41
49
  : "text-green-700 dark:text-green-400";
42
50
 
51
+ const isClickable = !!onClick && !calling && !hasError;
52
+ const Component = isClickable ? "button" : "div";
53
+ const interactiveCls = isClickable
54
+ ? "cursor-pointer hover:bg-green-500/10 text-left"
55
+ : "";
56
+
43
57
  return (
44
- <div
45
- className={`flex items-start gap-3 w-full rounded-xl border p-3 transition-colors ${borderColor}`}
58
+ <Component
59
+ type={isClickable ? "button" : undefined}
60
+ onClick={isClickable ? onClick : undefined}
61
+ className={`flex items-start gap-3 w-full rounded-xl border p-3 transition-colors ${borderColor} ${interactiveCls}`}
46
62
  >
47
63
  <div className={`mt-0.5 shrink-0 ${iconColor}`}>
48
64
  {hasError ? (
@@ -66,6 +82,9 @@ export function ChatToolLog({
66
82
  </div>
67
83
  )}
68
84
  </div>
69
- </div>
85
+ {isClickable && (
86
+ <ArrowUpRight className="h-3.5 w-3.5 mt-0.5 shrink-0 text-muted-foreground" />
87
+ )}
88
+ </Component>
70
89
  );
71
90
  }
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { MessageCircleQuestion, CheckCircle2 } from "lucide-react";
2
+ import { MessageCircleQuestion, User } from "lucide-react";
3
3
 
4
4
  export interface ChatToolQuestionProps {
5
5
  /** Whether the tool is waiting for user response */
@@ -12,23 +12,23 @@ export function ChatToolQuestion({
12
12
  calling,
13
13
  children,
14
14
  }: ChatToolQuestionProps) {
15
- const borderColor = calling
16
- ? "border-amber-500/20 bg-amber-500/5"
17
- : "border-green-500/20 bg-green-500/5";
15
+ // Calling = waiting for user → amber accent.
16
+ // Answered → primary (purple) so it visually matches user message bubbles.
17
+ const containerColor = calling
18
+ ? "border border-amber-500/20 bg-amber-500/5"
19
+ : "border border-primary/20 bg-primary/10";
18
20
 
19
- const iconColor = calling
20
- ? "text-amber-500"
21
- : "text-green-500";
21
+ const iconColor = calling ? "text-amber-500" : "text-primary";
22
22
 
23
23
  return (
24
24
  <div
25
- className={`flex items-start gap-3 w-full rounded-xl border p-3 transition-colors ${borderColor}`}
25
+ className={`flex items-start gap-3 w-full rounded-xl p-3 transition-colors ${containerColor}`}
26
26
  >
27
27
  <div className={`mt-0.5 shrink-0 ${iconColor}`}>
28
28
  {calling ? (
29
29
  <MessageCircleQuestion className="h-4 w-4" />
30
30
  ) : (
31
- <CheckCircle2 className="h-4 w-4" />
31
+ <User className="h-4 w-4" />
32
32
  )}
33
33
  </div>
34
34
  <div className="flex-1 min-w-0 space-y-2 text-sm">{children}</div>
@@ -32,20 +32,22 @@ export interface ChatProps {
32
32
  sidebar?: ReactNode;
33
33
  /** Extra toolbar content for ChatInput (attach menu, etc.) */
34
34
  toolbar?: ReactNode;
35
+ /** Show the model selector dropdown in ChatInput. Default true. */
36
+ showModelSelector?: boolean;
37
+ /** Show the web search toggle in ChatInput. Default true. */
38
+ showWebSearch?: boolean;
39
+ /**
40
+ * Render slot for ChatInput's send button. Receives `onClick` and
41
+ * `disabled` — caller renders its own button (e.g. branded with a logo).
42
+ */
43
+ renderSendButton?: (props: {
44
+ onClick: () => void;
45
+ disabled: boolean;
46
+ }) => ReactNode;
35
47
  /** Max width class for the message area */
36
48
  maxWidth?: string;
37
49
  /** Disable input (during generation) */
38
50
  disabled?: boolean;
39
- /** Labels for i18n */
40
- labels?: {
41
- questionsLabel?: string;
42
- answerLabel?: string;
43
- webSearchLabel?: string;
44
- placeholder?: string;
45
- submitLabel?: string;
46
- completedLabel?: string;
47
- customPlaceholder?: string;
48
- };
49
51
  }
50
52
 
51
53
  export function Chat({
@@ -58,9 +60,11 @@ export function Chat({
58
60
  header,
59
61
  sidebar,
60
62
  toolbar,
63
+ showModelSelector = true,
64
+ showWebSearch = true,
65
+ renderSendButton,
61
66
  maxWidth = "max-w-3xl",
62
67
  disabled = false,
63
- labels,
64
68
  }: ChatProps) {
65
69
  const { inputOverride } = useChatInput();
66
70
  const [answeringMessageId, setAnsweringMessageId] = useState<string | null>(
@@ -103,8 +107,6 @@ export function Chat({
103
107
  : undefined
104
108
  }
105
109
  onToolUseClick={onToolUseClick}
106
- questionsLabel={labels?.questionsLabel}
107
- answerLabel={labels?.answerLabel}
108
110
  />
109
111
  ))}
110
112
  <div ref={messagesEndRef} />
@@ -121,9 +123,6 @@ export function Chat({
121
123
  onAnswerQuestions?.(answeringMessageId!, answers);
122
124
  setAnsweringMessageId(null);
123
125
  }}
124
- submitLabel={labels?.submitLabel}
125
- completedLabel={labels?.completedLabel}
126
- customPlaceholder={labels?.customPlaceholder}
127
126
  />
128
127
  ) : (
129
128
  <ChatInput
@@ -131,8 +130,9 @@ export function Chat({
131
130
  models={models}
132
131
  defaultModel={defaultModel}
133
132
  toolbar={toolbar}
134
- webSearchLabel={labels?.webSearchLabel}
135
- placeholder={labels?.placeholder}
133
+ showModelSelector={showModelSelector}
134
+ showWebSearch={showWebSearch}
135
+ renderSendButton={renderSendButton}
136
136
  disabled={disabled}
137
137
  />
138
138
  )}