@arcote.tech/arc-ds 0.7.11 → 0.7.13
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 +2 -2
- package/src/ds/chat/chat-message.tsx +14 -9
- package/src/ds/markdown/markdown.tsx +216 -0
- package/src/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-ds",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.13",
|
|
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",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"tailwind-merge": "^3.5.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@arcote.tech/arc": "^0.7.
|
|
33
|
+
"@arcote.tech/arc": "^0.7.13",
|
|
34
34
|
"framer-motion": "^12.0.0",
|
|
35
35
|
"lucide-react": ">=0.400.0",
|
|
36
36
|
"radix-ui": "^1.0.0",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import remarkGfm from "remark-gfm";
|
|
1
|
+
import { Markdown } from "../markdown/markdown";
|
|
3
2
|
import { Button } from "../button/button";
|
|
4
|
-
import { Bot, User, MessageSquare } from "lucide-react";
|
|
3
|
+
import { Bot, User, MessageSquare, Loader2 } from "lucide-react";
|
|
5
4
|
import type { ChatMessageData } from "./types";
|
|
6
5
|
import { ToolUseBlock } from "./tool-use-block";
|
|
7
6
|
import { useChatLabels } from "./chat-labels";
|
|
@@ -21,6 +20,12 @@ export function ChatMessage({
|
|
|
21
20
|
const isUser = message.role === "user";
|
|
22
21
|
const hasUnansweredQuestions =
|
|
23
22
|
message.questions && message.questions.length > 0;
|
|
23
|
+
// Empty assistant bubble in streaming state = LLM still thinking / waiting
|
|
24
|
+
// for first chunk. Show a spinner so the UI doesn't look frozen — the
|
|
25
|
+
// pulsing caret on `data-streaming` only works once there's a `last-child`
|
|
26
|
+
// to attach to.
|
|
27
|
+
const showStreamingPlaceholder =
|
|
28
|
+
!isUser && message.isStreaming && !message.content;
|
|
24
29
|
|
|
25
30
|
return (
|
|
26
31
|
<div className={`flex gap-3 ${isUser ? "flex-row-reverse" : ""}`}>
|
|
@@ -48,13 +53,13 @@ export function ChatMessage({
|
|
|
48
53
|
: "bg-card border border-border rounded-tl-sm"
|
|
49
54
|
}`}
|
|
50
55
|
>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
56
|
+
{showStreamingPlaceholder ? (
|
|
57
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
58
|
+
) : (
|
|
59
|
+
<Markdown isStreaming={message.isStreaming}>
|
|
55
60
|
{message.content}
|
|
56
|
-
</
|
|
57
|
-
|
|
61
|
+
</Markdown>
|
|
62
|
+
)}
|
|
58
63
|
</div>
|
|
59
64
|
|
|
60
65
|
{/* Tool uses */}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import ReactMarkdown, { type Components } from "react-markdown";
|
|
2
|
+
import remarkGfm from "remark-gfm";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface MarkdownProps {
|
|
6
|
+
/** Surowy markdown (GFM) do wyrenderowania. */
|
|
7
|
+
children: string;
|
|
8
|
+
/** Dodatkowe klasy na wrapperze (np. typografia z kontekstu konsumenta). */
|
|
9
|
+
className?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Pulsujący caret na końcu ostatniego bloku — dla treści strumieniowanej.
|
|
12
|
+
* Steruje `data-streaming` na wrapperze.
|
|
13
|
+
*/
|
|
14
|
+
isStreaming?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// WAŻNE: wszystkie className to STATYCZNE literały — Tailwind v4 skanuje źródła
|
|
18
|
+
// DS przez `@source` i generuje tylko klasy obecne dosłownie w kodzie. Żadnej
|
|
19
|
+
// dynamicznej konkatenacji nazw klas. `node` (hast) jest odfiltrowywany z każdego
|
|
20
|
+
// mapowania, żeby nie trafił na DOM. `className` z react-markdown (np.
|
|
21
|
+
// `language-ts` na code, `task-list-item` na li) mergujemy przez `cn` —
|
|
22
|
+
// spread `{...props}` PO `className="…"` nadpisałby nasze klasy.
|
|
23
|
+
const components: Components = {
|
|
24
|
+
p: ({ node, className, ...props }) => (
|
|
25
|
+
<p className={cn("my-2 leading-relaxed", className)} {...props} />
|
|
26
|
+
),
|
|
27
|
+
ul: ({ node, className, ...props }) => (
|
|
28
|
+
<ul
|
|
29
|
+
className={cn(
|
|
30
|
+
"my-2 ml-5 list-disc space-y-1 marker:text-muted-foreground",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
),
|
|
36
|
+
ol: ({ node, className, ...props }) => (
|
|
37
|
+
<ol
|
|
38
|
+
className={cn(
|
|
39
|
+
"my-2 ml-5 list-decimal space-y-1 marker:text-muted-foreground",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
45
|
+
li: ({ node, className, ...props }) => {
|
|
46
|
+
// GFM oznacza pozycje task-listy klasą `task-list-item` i wstawia checkbox
|
|
47
|
+
// jako pierwsze dziecko — wtedy bez punktora, w jednej linii z checkboxem.
|
|
48
|
+
const isTask =
|
|
49
|
+
typeof className === "string" && className.includes("task-list-item");
|
|
50
|
+
return (
|
|
51
|
+
<li
|
|
52
|
+
className={cn(
|
|
53
|
+
isTask
|
|
54
|
+
? "-ml-5 flex list-none items-start gap-2"
|
|
55
|
+
: "pl-1 [&>ol]:mt-1 [&>ol]:mb-0 [&>ul]:mt-1 [&>ul]:mb-0",
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
input: ({ node, className, ...props }) => (
|
|
63
|
+
// GFM renderuje `<input type=checkbox disabled checked?>` — read-only,
|
|
64
|
+
// żeby React nie ostrzegał o kontrolce bez `onChange`.
|
|
65
|
+
<input
|
|
66
|
+
className={cn("mt-1 accent-primary", className)}
|
|
67
|
+
readOnly
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
),
|
|
71
|
+
h1: ({ node, className, ...props }) => (
|
|
72
|
+
<h1 className={cn("mt-4 mb-2 text-lg font-semibold", className)} {...props} />
|
|
73
|
+
),
|
|
74
|
+
h2: ({ node, className, ...props }) => (
|
|
75
|
+
<h2
|
|
76
|
+
className={cn("mt-4 mb-2 text-base font-semibold", className)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
),
|
|
80
|
+
h3: ({ node, className, ...props }) => (
|
|
81
|
+
<h3
|
|
82
|
+
className={cn("mt-3 mb-1.5 text-sm font-semibold", className)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
),
|
|
86
|
+
h4: ({ node, className, ...props }) => (
|
|
87
|
+
<h4 className={cn("mt-3 mb-1 text-sm font-medium", className)} {...props} />
|
|
88
|
+
),
|
|
89
|
+
h5: ({ node, className, ...props }) => (
|
|
90
|
+
<h5
|
|
91
|
+
className={cn(
|
|
92
|
+
"mt-2 mb-1 text-xs font-semibold tracking-wide text-muted-foreground uppercase",
|
|
93
|
+
className,
|
|
94
|
+
)}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
),
|
|
98
|
+
h6: ({ node, className, ...props }) => (
|
|
99
|
+
<h6
|
|
100
|
+
className={cn(
|
|
101
|
+
"mt-2 mb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase",
|
|
102
|
+
className,
|
|
103
|
+
)}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
),
|
|
107
|
+
a: ({ node, className, ...props }) => (
|
|
108
|
+
<a
|
|
109
|
+
className={cn(
|
|
110
|
+
"text-primary underline underline-offset-2 break-words hover:opacity-80",
|
|
111
|
+
className,
|
|
112
|
+
)}
|
|
113
|
+
target="_blank"
|
|
114
|
+
rel="noopener noreferrer"
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
),
|
|
118
|
+
code: ({ node, className, ...props }) => (
|
|
119
|
+
// Inline code. Wewnątrz `pre` tło/padding/rozmiar resetuje override `pre`
|
|
120
|
+
// (selektor `pre > code` ma wyższą specyficzność) — brak podwójnego tła.
|
|
121
|
+
<code
|
|
122
|
+
className={cn(
|
|
123
|
+
"rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]",
|
|
124
|
+
className,
|
|
125
|
+
)}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
),
|
|
129
|
+
pre: ({ node, className, ...props }) => (
|
|
130
|
+
<pre
|
|
131
|
+
className={cn(
|
|
132
|
+
"my-2 overflow-x-auto rounded-md bg-muted p-3 text-xs [&>code]:bg-transparent [&>code]:p-0 [&>code]:font-mono [&>code]:text-inherit",
|
|
133
|
+
className,
|
|
134
|
+
)}
|
|
135
|
+
{...props}
|
|
136
|
+
/>
|
|
137
|
+
),
|
|
138
|
+
blockquote: ({ node, className, ...props }) => (
|
|
139
|
+
<blockquote
|
|
140
|
+
className={cn(
|
|
141
|
+
"my-2 border-l-2 border-primary/30 pl-3 text-muted-foreground italic",
|
|
142
|
+
className,
|
|
143
|
+
)}
|
|
144
|
+
{...props}
|
|
145
|
+
/>
|
|
146
|
+
),
|
|
147
|
+
hr: ({ node, className, ...props }) => (
|
|
148
|
+
<hr className={cn("my-4 border-t border-border", className)} {...props} />
|
|
149
|
+
),
|
|
150
|
+
table: ({ node, className, ...props }) => (
|
|
151
|
+
<div className="my-2 overflow-x-auto">
|
|
152
|
+
<table
|
|
153
|
+
className={cn("w-full border-collapse text-sm", className)}
|
|
154
|
+
{...props}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
),
|
|
158
|
+
thead: ({ node, className, ...props }) => (
|
|
159
|
+
<thead className={cn("bg-muted", className)} {...props} />
|
|
160
|
+
),
|
|
161
|
+
th: ({ node, className, ...props }) => (
|
|
162
|
+
<th
|
|
163
|
+
className={cn(
|
|
164
|
+
"border border-border px-3 py-1.5 text-left font-semibold",
|
|
165
|
+
className,
|
|
166
|
+
)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
),
|
|
170
|
+
td: ({ node, className, ...props }) => (
|
|
171
|
+
<td className={cn("border border-border px-3 py-1.5", className)} {...props} />
|
|
172
|
+
),
|
|
173
|
+
del: ({ node, className, ...props }) => (
|
|
174
|
+
<del className={cn("line-through opacity-70", className)} {...props} />
|
|
175
|
+
),
|
|
176
|
+
img: ({ node, className, ...props }) => (
|
|
177
|
+
<img
|
|
178
|
+
className={cn("my-2 max-w-full rounded-md", className)}
|
|
179
|
+
loading="lazy"
|
|
180
|
+
{...props}
|
|
181
|
+
/>
|
|
182
|
+
),
|
|
183
|
+
strong: ({ node, className, ...props }) => (
|
|
184
|
+
<strong className={cn("font-semibold", className)} {...props} />
|
|
185
|
+
),
|
|
186
|
+
em: ({ node, className, ...props }) => (
|
|
187
|
+
<em className={cn("italic", className)} {...props} />
|
|
188
|
+
),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Renderuje markdown (GFM) spójnie z theme DS (zmienne CSS, dark mode). Pokrywa
|
|
193
|
+
* pełny zakres GFM: listy (z punktorami/numerami + zagnieżdżone), task-listy,
|
|
194
|
+
* tabele (z poziomym scrollem), nagłówki h1–h6, code inline/blok, blockquote,
|
|
195
|
+
* hr, linki (external w nowej karcie), strikethrough, obrazy.
|
|
196
|
+
*
|
|
197
|
+
* `isStreaming` dokleja pulsujący caret na końcu ostatniego bloku — wrapper
|
|
198
|
+
* ustawia `data-streaming`, a caret celuje w `[&>*:last-child]::after`
|
|
199
|
+
* (bezpośrednie ostatnie dziecko, nie głębiej np. w `<li>`).
|
|
200
|
+
*/
|
|
201
|
+
export function Markdown({ children, className, isStreaming }: MarkdownProps) {
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
data-streaming={isStreaming || undefined}
|
|
205
|
+
className={cn(
|
|
206
|
+
"text-left break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
|
207
|
+
"data-[streaming]:[&>*:last-child]:after:ml-0.5 data-[streaming]:[&>*:last-child]:after:inline-block data-[streaming]:[&>*:last-child]:after:h-[1em] data-[streaming]:[&>*:last-child]:after:w-[0.4em] data-[streaming]:[&>*:last-child]:after:animate-pulse data-[streaming]:[&>*:last-child]:after:bg-foreground/60 data-[streaming]:[&>*:last-child]:after:align-text-bottom data-[streaming]:[&>*:last-child]:after:content-['']",
|
|
208
|
+
className,
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
|
212
|
+
{children}
|
|
213
|
+
</ReactMarkdown>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -105,6 +105,10 @@ export { ExpandablePanel } from "./layout/expandable-panel";
|
|
|
105
105
|
export { useExpandable } from "./layout/use-expandable";
|
|
106
106
|
export type { UseExpandableReturn } from "./layout/use-expandable";
|
|
107
107
|
|
|
108
|
+
// Markdown
|
|
109
|
+
export { Markdown } from "./ds/markdown/markdown";
|
|
110
|
+
export type { MarkdownProps } from "./ds/markdown/markdown";
|
|
111
|
+
|
|
108
112
|
// Chat
|
|
109
113
|
export { Chat } from "./ds/chat/chat";
|
|
110
114
|
export type { ChatProps } from "./ds/chat/chat";
|