@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 +4 -2
- package/src/ds/bento-card/bento-card.tsx +27 -35
- package/src/ds/bento-grid/bento-grid.tsx +23 -36
- package/src/ds/button/button.tsx +2 -0
- package/src/ds/chat/chat-input.tsx +61 -36
- package/src/ds/chat/chat-labels.tsx +89 -0
- package/src/ds/chat/chat-message.tsx +9 -18
- package/src/ds/chat/chat-tool-log.tsx +23 -4
- package/src/ds/chat/chat-tool-question.tsx +9 -9
- package/src/ds/chat/chat.tsx +18 -18
- package/src/ds/chat/question-tabs.tsx +194 -76
- package/src/ds/editable-text/editable-text.tsx +107 -0
- package/src/ds/form/fields/search-select-field.tsx +13 -5
- package/src/ds/search-select/search-select.tsx +305 -161
- package/src/ds/sidebar/sidebar.tsx +60 -0
- package/src/ds/types.ts +1 -0
- package/src/index.ts +12 -2
- package/src/layout/dynamic-slot.tsx +12 -2
- package/src/layout/layout.tsx +79 -8
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.
|
|
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.
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
35
|
-
*
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
45
|
-
{items
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
);
|
package/src/ds/button/button.tsx
CHANGED
|
@@ -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
|
-
/**
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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-
|
|
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 ?
|
|
67
|
+
placeholder={disabled ? labels.placeholderGenerating : labels.placeholder}
|
|
53
68
|
rows={1}
|
|
54
69
|
/>
|
|
55
70
|
</div>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
<div className="
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
54
|
-
<
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
<
|
|
45
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
<
|
|
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>
|
package/src/ds/chat/chat.tsx
CHANGED
|
@@ -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
|
-
|
|
135
|
-
|
|
133
|
+
showModelSelector={showModelSelector}
|
|
134
|
+
showWebSearch={showWebSearch}
|
|
135
|
+
renderSendButton={renderSendButton}
|
|
136
136
|
disabled={disabled}
|
|
137
137
|
/>
|
|
138
138
|
)}
|