@cascivo/ai 0.2.3
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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/index.d.mts +72 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +222 -0
- package/dist/index.mjs.map +1 -0
- package/dist/style.css +217 -0
- package/package.json +66 -0
- package/readme.body.md +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) cascivo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<!-- generated by scripts/readme/generate.ts — edit readme.body.md, not this file -->
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="72" height="72" alt="cascivo logo"></a>
|
|
5
|
+
<h1>@cascivo/ai</h1>
|
|
6
|
+
<p><strong>AI-native components for cascivo — StreamingText, AiLabel, Terminal, AiChat</strong></p>
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@cascivo/ai)
|
|
9
|
+
[](https://www.npmjs.com/package/@cascivo/ai)
|
|
10
|
+
[](https://github.com/cascivo/cascivo/blob/main/LICENSE)
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
[npm](https://www.npmjs.com/package/@cascivo/ai) · [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
AI-native components for cascivo — the presentation layer for AI features: streaming text, generation status, animated terminals, and a full chat surface. Signal-driven like the rest of the design system (no `useState`, no `useEffect`), typed, and localized via `@cascivo/i18n` built-in catalogs.
|
|
20
|
+
|
|
21
|
+
> Newly published — first release on npm as of `0.1.0`.
|
|
22
|
+
|
|
23
|
+
**Peer dependencies:** `react >=18` and `@preact/signals-react >=2` (the components are signal-driven). Styles ship as CSS modules bundled with the components — no stylesheet import needed. Pair with `@cascivo/themes` for tokens.
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pnpm add @cascivo/ai @preact/signals-react
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## `StreamingText`
|
|
30
|
+
|
|
31
|
+
Types text out character-by-character with a blinking cursor, driven by `requestAnimationFrame`. Feed it a growing string (e.g. an accumulating LLM response) or a static one:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { StreamingText } from '@cascivo/ai'
|
|
35
|
+
;<StreamingText
|
|
36
|
+
text={response} // target text; typing catches up as it grows
|
|
37
|
+
speed={2} // characters per frame (default 2)
|
|
38
|
+
onComplete={() => setDone(true)}
|
|
39
|
+
/>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The cursor renders only while the displayed text lags the target, and the animation resets when `text` is replaced with a shorter string.
|
|
43
|
+
|
|
44
|
+
## `AiLabel`
|
|
45
|
+
|
|
46
|
+
A `role="status"` badge for AI-generated content with three variants — `generating` (default), `done`, `error`. Labels come from the `@cascivo/i18n` built-in catalog, so they localize automatically:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { AiLabel } from '@cascivo/ai'
|
|
50
|
+
|
|
51
|
+
<AiLabel variant="generating" />
|
|
52
|
+
<AiLabel variant="done" />
|
|
53
|
+
<AiLabel variant="error" />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
It extends `HTMLAttributes<HTMLSpanElement>`, so `className`, `title`, etc. pass through.
|
|
57
|
+
|
|
58
|
+
## `Terminal`
|
|
59
|
+
|
|
60
|
+
An animated terminal window (`role="log"`, `aria-live="polite"`) that types out a script of lines. Each line has a `type` (`command` | `output` | `error` | `comment`) for per-line styling and an optional `prefix` (e.g. `$`):
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { Terminal } from '@cascivo/ai'
|
|
64
|
+
;<Terminal
|
|
65
|
+
lines={[
|
|
66
|
+
{ text: 'npx cascivo add button', prefix: '$', type: 'command' },
|
|
67
|
+
{ text: 'Added button to src/components.', type: 'output' },
|
|
68
|
+
{ text: '# done in 1.2s', type: 'comment' },
|
|
69
|
+
]}
|
|
70
|
+
speed={3} // characters per frame (default 3)
|
|
71
|
+
loop // restart from the top after the last line
|
|
72
|
+
onComplete={() => {}}
|
|
73
|
+
/>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## `AiChat`
|
|
77
|
+
|
|
78
|
+
A complete chat surface: message list (`role="log"`), streaming assistant bubble, and a textarea composer with Enter-to-send (Shift+Enter for a newline). It is fully controlled — you own the message array and the send handler:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { AiChat, type ChatMessage } from '@cascivo/ai'
|
|
82
|
+
|
|
83
|
+
const messages: ChatMessage[] = [
|
|
84
|
+
{ id: '1', role: 'user', content: 'What is cascivo?' },
|
|
85
|
+
{ id: '2', role: 'assistant', content: 'A CSS-native design system.' },
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
<AiChat
|
|
89
|
+
messages={messages} // system messages are accepted but not rendered
|
|
90
|
+
onSend={(text) => append({ role: 'user', content: text })}
|
|
91
|
+
isStreaming={streaming} // disables the send button
|
|
92
|
+
streamingText={partial} // rendered as a live assistant bubble via StreamingText
|
|
93
|
+
/>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Role labels ("You" / "Assistant"), the input placeholder, and the send button label all default from the `@cascivo/i18n` built-in catalog.
|
|
97
|
+
|
|
98
|
+
## Install
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
pnpm add @cascivo/ai
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
[cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo) · AI agents: use [`@cascivo/mcp`](https://github.com/cascivo/cascivo/tree/main/packages/mcp) and [`registry.json`](https://github.com/cascivo/cascivo/blob/main/registry.json) · MIT
|
|
107
|
+
|
|
108
|
+
<div align="center"><a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="28" height="28" alt="cascivo"></a></div>
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/streaming-text.d.ts
|
|
4
|
+
interface StreamingTextProps {
|
|
5
|
+
text: string;
|
|
6
|
+
speed?: number;
|
|
7
|
+
onComplete?: () => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
declare function StreamingText({
|
|
11
|
+
text,
|
|
12
|
+
speed,
|
|
13
|
+
onComplete,
|
|
14
|
+
className
|
|
15
|
+
}: StreamingTextProps): import("react").JSX.Element;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/ai-label.d.ts
|
|
18
|
+
type AiLabelVariant = 'generating' | 'done' | 'error';
|
|
19
|
+
interface AiLabelProps extends HTMLAttributes<HTMLSpanElement> {
|
|
20
|
+
variant?: AiLabelVariant;
|
|
21
|
+
}
|
|
22
|
+
declare function AiLabel({
|
|
23
|
+
variant,
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: AiLabelProps): import("react").JSX.Element;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/terminal.d.ts
|
|
29
|
+
type TerminalLineType = 'command' | 'output' | 'error' | 'comment';
|
|
30
|
+
interface TerminalLine {
|
|
31
|
+
text: string;
|
|
32
|
+
prefix?: string;
|
|
33
|
+
type?: TerminalLineType;
|
|
34
|
+
}
|
|
35
|
+
interface TerminalProps {
|
|
36
|
+
lines: TerminalLine[];
|
|
37
|
+
speed?: number;
|
|
38
|
+
loop?: boolean;
|
|
39
|
+
onComplete?: () => void;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
declare function Terminal({
|
|
43
|
+
lines,
|
|
44
|
+
speed,
|
|
45
|
+
loop,
|
|
46
|
+
onComplete,
|
|
47
|
+
className
|
|
48
|
+
}: TerminalProps): import("react").JSX.Element;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/ai-chat.d.ts
|
|
51
|
+
interface ChatMessage {
|
|
52
|
+
id: string;
|
|
53
|
+
role: 'user' | 'assistant' | 'system';
|
|
54
|
+
content: string;
|
|
55
|
+
}
|
|
56
|
+
interface AiChatProps {
|
|
57
|
+
messages: ChatMessage[];
|
|
58
|
+
onSend: (text: string) => void;
|
|
59
|
+
isStreaming?: boolean;
|
|
60
|
+
streamingText?: string;
|
|
61
|
+
className?: string;
|
|
62
|
+
}
|
|
63
|
+
declare function AiChat({
|
|
64
|
+
messages,
|
|
65
|
+
onSend,
|
|
66
|
+
isStreaming,
|
|
67
|
+
streamingText,
|
|
68
|
+
className
|
|
69
|
+
}: AiChatProps): import("react").JSX.Element;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { AiChat, type AiChatProps, AiLabel, type AiLabelProps, type AiLabelVariant, type ChatMessage, StreamingText, type StreamingTextProps, Terminal, type TerminalLine, type TerminalLineType, type TerminalProps };
|
|
72
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/streaming-text.tsx","../src/ai-label.tsx","../src/terminal.tsx","../src/ai-chat.tsx"],"mappings":";;;UAKiB,kBAAA;EACf,IAAA;EACA,KAAA;EACA,UAAA;EACA,SAAA;AAAA;AAAA,iBAGc,aAAA;EAAgB,IAAA;EAAM,KAAA;EAAW,UAAA;EAAY;AAAA,GAAa,kBAAA,mBAAkB,GAAA,CAAA,OAAA;;;KCNhF,cAAA;AAAA,UAEK,YAAA,SAAqB,cAAA,CAAe,eAAA;EACnD,OAAA,GAAU,cAAA;AAAA;AAAA,iBAGI,OAAA;EAAU,OAAA;EAAwB,SAAA;EAAA,GAAc;AAAA,GAAS,YAAA,mBAAY,GAAA,CAAA,OAAA;;;KCPzE,gBAAA;AAAA,UAEK,YAAA;EACf,IAAA;EACA,MAAA;EACA,IAAA,GAAO,gBAAgB;AAAA;AAAA,UAGR,aAAA;EACf,KAAA,EAAO,YAAY;EACnB,KAAA;EACA,IAAA;EACA,UAAA;EACA,SAAA;AAAA;AAAA,iBAGc,QAAA;EAAW,KAAA;EAAO,KAAA;EAAW,IAAA;EAAc,UAAA;EAAY;AAAA,GAAa,aAAA,mBAAa,GAAA,CAAA,OAAA;;;UCdhF,WAAA;EACf,EAAA;EACA,IAAA;EACA,OAAA;AAAA;AAAA,UAGe,WAAA;EACf,QAAA,EAAU,WAAW;EACrB,MAAA,GAAS,IAAA;EACT,WAAA;EACA,aAAA;EACA,SAAA;AAAA;AAAA,iBAGc,MAAA;EACd,QAAA;EACA,MAAA;EACA,WAAA;EACA,aAAA;EACA;AAAA,GACC,WAAA,mBAAW,GAAA,CAAA,OAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { useSignal, useSignalEffect, useSignals } from "@cascivo/core";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { builtin, t } from "@cascivo/i18n";
|
|
5
|
+
//#region src/streaming-text.module.css
|
|
6
|
+
var streaming_text_module_default = {
|
|
7
|
+
"cascade-cursor-blink": "dgjyMq_cascade-cursor-blink",
|
|
8
|
+
"cursor": "dgjyMq_cursor",
|
|
9
|
+
"root": "dgjyMq_root"
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/streaming-text.tsx
|
|
13
|
+
function StreamingText({ text, speed = 2, onComplete, className }) {
|
|
14
|
+
useSignals();
|
|
15
|
+
const displayed = useSignal("");
|
|
16
|
+
const onCompleteRef = useRef(onComplete);
|
|
17
|
+
onCompleteRef.current = onComplete;
|
|
18
|
+
displayed.value = displayed.value.length > text.length ? "" : displayed.value;
|
|
19
|
+
useSignalEffect(() => {
|
|
20
|
+
const target = text;
|
|
21
|
+
if (displayed.value === target) {
|
|
22
|
+
onCompleteRef.current?.();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let rafId;
|
|
26
|
+
function tick() {
|
|
27
|
+
const current = displayed.value;
|
|
28
|
+
if (current.length >= target.length) {
|
|
29
|
+
onCompleteRef.current?.();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
displayed.value = target.slice(0, current.length + speed);
|
|
33
|
+
rafId = requestAnimationFrame(tick);
|
|
34
|
+
}
|
|
35
|
+
rafId = requestAnimationFrame(tick);
|
|
36
|
+
return () => cancelAnimationFrame(rafId);
|
|
37
|
+
});
|
|
38
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
39
|
+
className: [streaming_text_module_default.root, className].filter(Boolean).join(" "),
|
|
40
|
+
children: [displayed.value, displayed.value !== text && /* @__PURE__ */ jsx("span", {
|
|
41
|
+
className: streaming_text_module_default.cursor,
|
|
42
|
+
"aria-hidden": "true"
|
|
43
|
+
})]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/ai-label.module.css
|
|
48
|
+
var ai_label_module_default = {
|
|
49
|
+
"cascade-ai-pulse": "iirJiG_cascade-ai-pulse",
|
|
50
|
+
"root": "iirJiG_root"
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/ai-label.tsx
|
|
54
|
+
function AiLabel({ variant = "generating", className, ...props }) {
|
|
55
|
+
useSignals();
|
|
56
|
+
const label = variant === "generating" ? t(builtin.ai.generating) : variant === "done" ? t(builtin.ai.done) : t(builtin.ai.error);
|
|
57
|
+
return /* @__PURE__ */ jsx("span", {
|
|
58
|
+
role: "status",
|
|
59
|
+
"aria-label": label,
|
|
60
|
+
"data-variant": variant,
|
|
61
|
+
className: [ai_label_module_default.root, className].filter(Boolean).join(" "),
|
|
62
|
+
...props,
|
|
63
|
+
children: label
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/terminal.module.css
|
|
68
|
+
var terminal_module_default = {
|
|
69
|
+
"cascade-cursor-blink": "GFOf3W_cascade-cursor-blink",
|
|
70
|
+
"cursor": "GFOf3W_cursor",
|
|
71
|
+
"line": "GFOf3W_line",
|
|
72
|
+
"prefix": "GFOf3W_prefix",
|
|
73
|
+
"root": "GFOf3W_root",
|
|
74
|
+
"text": "GFOf3W_text"
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/terminal.tsx
|
|
78
|
+
function Terminal({ lines, speed = 3, loop = false, onComplete, className }) {
|
|
79
|
+
useSignals();
|
|
80
|
+
const lineIndex = useSignal(0);
|
|
81
|
+
const charIndex = useSignal(0);
|
|
82
|
+
const onCompleteRef = useRef(onComplete);
|
|
83
|
+
onCompleteRef.current = onComplete;
|
|
84
|
+
useSignalEffect(() => {
|
|
85
|
+
if (lines.length === 0) return;
|
|
86
|
+
let rafId;
|
|
87
|
+
function tick() {
|
|
88
|
+
const li = lineIndex.value;
|
|
89
|
+
const ci = charIndex.value;
|
|
90
|
+
if (li >= lines.length) {
|
|
91
|
+
onCompleteRef.current?.();
|
|
92
|
+
if (loop) {
|
|
93
|
+
lineIndex.value = 0;
|
|
94
|
+
charIndex.value = 0;
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (ci < lines[li].text.length) charIndex.value = ci + speed;
|
|
99
|
+
else {
|
|
100
|
+
lineIndex.value = li + 1;
|
|
101
|
+
charIndex.value = 0;
|
|
102
|
+
}
|
|
103
|
+
rafId = requestAnimationFrame(tick);
|
|
104
|
+
}
|
|
105
|
+
rafId = requestAnimationFrame(tick);
|
|
106
|
+
return () => cancelAnimationFrame(rafId);
|
|
107
|
+
});
|
|
108
|
+
const renderedLines = lines.slice(0, lineIndex.value + 1).map((line, i) => {
|
|
109
|
+
const isCurrentLine = i === lineIndex.value;
|
|
110
|
+
const text = isCurrentLine ? line.text.slice(0, charIndex.value) : line.text;
|
|
111
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
112
|
+
className: terminal_module_default.line,
|
|
113
|
+
"data-type": line.type ?? "output",
|
|
114
|
+
children: [
|
|
115
|
+
line.prefix && /* @__PURE__ */ jsx("span", {
|
|
116
|
+
className: terminal_module_default.prefix,
|
|
117
|
+
children: line.prefix
|
|
118
|
+
}),
|
|
119
|
+
/* @__PURE__ */ jsx("span", {
|
|
120
|
+
className: terminal_module_default.text,
|
|
121
|
+
children: text
|
|
122
|
+
}),
|
|
123
|
+
isCurrentLine && lineIndex.value < lines.length && /* @__PURE__ */ jsx("span", {
|
|
124
|
+
className: terminal_module_default.cursor,
|
|
125
|
+
"aria-hidden": "true"
|
|
126
|
+
})
|
|
127
|
+
]
|
|
128
|
+
}, i);
|
|
129
|
+
});
|
|
130
|
+
return /* @__PURE__ */ jsx("div", {
|
|
131
|
+
role: "log",
|
|
132
|
+
"aria-live": "polite",
|
|
133
|
+
"aria-label": "Terminal output",
|
|
134
|
+
className: [terminal_module_default.root, className].filter(Boolean).join(" "),
|
|
135
|
+
children: renderedLines
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/ai-chat.module.css
|
|
140
|
+
var ai_chat_module_default = {
|
|
141
|
+
"content": "oL95jG_content",
|
|
142
|
+
"inputArea": "oL95jG_inputArea",
|
|
143
|
+
"message": "oL95jG_message",
|
|
144
|
+
"messages": "oL95jG_messages",
|
|
145
|
+
"roleLabel": "oL95jG_roleLabel",
|
|
146
|
+
"root": "oL95jG_root",
|
|
147
|
+
"sendButton": "oL95jG_sendButton",
|
|
148
|
+
"textarea": "oL95jG_textarea"
|
|
149
|
+
};
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/ai-chat.tsx
|
|
152
|
+
function AiChat({ messages, onSend, isStreaming = false, streamingText, className }) {
|
|
153
|
+
useSignals();
|
|
154
|
+
const inputValue = useSignal("");
|
|
155
|
+
const listRef = useRef(null);
|
|
156
|
+
function handleSend() {
|
|
157
|
+
const text = inputValue.value.trim();
|
|
158
|
+
if (!text) return;
|
|
159
|
+
onSend(text);
|
|
160
|
+
inputValue.value = "";
|
|
161
|
+
}
|
|
162
|
+
function handleKeyDown(e) {
|
|
163
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
handleSend();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
169
|
+
className: [ai_chat_module_default.root, className].filter(Boolean).join(" "),
|
|
170
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
171
|
+
ref: listRef,
|
|
172
|
+
className: ai_chat_module_default.messages,
|
|
173
|
+
role: "log",
|
|
174
|
+
"aria-live": "polite",
|
|
175
|
+
children: [messages.filter((m) => m.role !== "system").map((msg) => /* @__PURE__ */ jsxs("div", {
|
|
176
|
+
className: ai_chat_module_default.message,
|
|
177
|
+
"data-role": msg.role,
|
|
178
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
179
|
+
className: ai_chat_module_default.roleLabel,
|
|
180
|
+
children: msg.role === "user" ? t(builtin.ai.you) : t(builtin.ai.assistant)
|
|
181
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
182
|
+
className: ai_chat_module_default.content,
|
|
183
|
+
children: msg.content
|
|
184
|
+
})]
|
|
185
|
+
}, msg.id)), isStreaming && streamingText !== void 0 && /* @__PURE__ */ jsxs("div", {
|
|
186
|
+
className: ai_chat_module_default.message,
|
|
187
|
+
"data-role": "assistant",
|
|
188
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
189
|
+
className: ai_chat_module_default.roleLabel,
|
|
190
|
+
children: t(builtin.ai.assistant)
|
|
191
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
192
|
+
className: ai_chat_module_default.content,
|
|
193
|
+
children: /* @__PURE__ */ jsx(StreamingText, { text: streamingText })
|
|
194
|
+
})]
|
|
195
|
+
})]
|
|
196
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
197
|
+
className: ai_chat_module_default.inputArea,
|
|
198
|
+
children: [/* @__PURE__ */ jsx("textarea", {
|
|
199
|
+
className: ai_chat_module_default.textarea,
|
|
200
|
+
placeholder: t(builtin.ai.placeholder),
|
|
201
|
+
value: inputValue.value,
|
|
202
|
+
onChange: (e) => {
|
|
203
|
+
inputValue.value = e.target.value;
|
|
204
|
+
},
|
|
205
|
+
onKeyDown: handleKeyDown,
|
|
206
|
+
rows: 1,
|
|
207
|
+
"aria-label": t(builtin.ai.placeholder)
|
|
208
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
209
|
+
type: "button",
|
|
210
|
+
className: ai_chat_module_default.sendButton,
|
|
211
|
+
onClick: handleSend,
|
|
212
|
+
disabled: isStreaming,
|
|
213
|
+
"aria-label": t(builtin.ai.send),
|
|
214
|
+
children: t(builtin.ai.send)
|
|
215
|
+
})]
|
|
216
|
+
})]
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
export { AiChat, AiLabel, StreamingText, Terminal };
|
|
221
|
+
|
|
222
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["styles","styles","styles","styles"],"sources":["../src/streaming-text.tsx","../src/ai-label.tsx","../src/terminal.tsx","../src/ai-chat.tsx"],"sourcesContent":["'use client'\nimport { useRef } from 'react'\nimport { useSignal, useSignalEffect, useSignals } from '@cascivo/core'\nimport styles from './streaming-text.module.css'\n\nexport interface StreamingTextProps {\n text: string\n speed?: number\n onComplete?: () => void\n className?: string\n}\n\nexport function StreamingText({ text, speed = 2, onComplete, className }: StreamingTextProps) {\n useSignals()\n const displayed = useSignal('')\n const onCompleteRef = useRef(onComplete)\n onCompleteRef.current = onComplete\n\n // Reset when text prop changes\n displayed.value = displayed.value.length > text.length ? '' : displayed.value\n\n useSignalEffect(() => {\n const target = text\n if (displayed.value === target) {\n onCompleteRef.current?.()\n return\n }\n let rafId: number\n function tick() {\n const current = displayed.value\n if (current.length >= target.length) {\n onCompleteRef.current?.()\n return\n }\n displayed.value = target.slice(0, current.length + speed)\n rafId = requestAnimationFrame(tick)\n }\n rafId = requestAnimationFrame(tick)\n return () => cancelAnimationFrame(rafId)\n })\n\n return (\n <span className={[styles.root, className].filter(Boolean).join(' ')}>\n {displayed.value}\n {displayed.value !== text && <span className={styles.cursor} aria-hidden=\"true\" />}\n </span>\n )\n}\n","'use client'\nimport { useSignals } from '@cascivo/core'\nimport { builtin, t } from '@cascivo/i18n'\nimport styles from './ai-label.module.css'\nimport type { HTMLAttributes } from 'react'\n\nexport type AiLabelVariant = 'generating' | 'done' | 'error'\n\nexport interface AiLabelProps extends HTMLAttributes<HTMLSpanElement> {\n variant?: AiLabelVariant\n}\n\nexport function AiLabel({ variant = 'generating', className, ...props }: AiLabelProps) {\n useSignals()\n const label =\n variant === 'generating'\n ? t(builtin.ai.generating)\n : variant === 'done'\n ? t(builtin.ai.done)\n : t(builtin.ai.error)\n\n return (\n <span\n role=\"status\"\n aria-label={label}\n data-variant={variant}\n className={[styles.root, className].filter(Boolean).join(' ')}\n {...props}\n >\n {label}\n </span>\n )\n}\n","'use client'\nimport { useRef } from 'react'\nimport { useSignal, useSignalEffect, useSignals } from '@cascivo/core'\nimport styles from './terminal.module.css'\n\nexport type TerminalLineType = 'command' | 'output' | 'error' | 'comment'\n\nexport interface TerminalLine {\n text: string\n prefix?: string\n type?: TerminalLineType\n}\n\nexport interface TerminalProps {\n lines: TerminalLine[]\n speed?: number\n loop?: boolean\n onComplete?: () => void\n className?: string\n}\n\nexport function Terminal({ lines, speed = 3, loop = false, onComplete, className }: TerminalProps) {\n useSignals()\n const lineIndex = useSignal(0)\n const charIndex = useSignal(0)\n const onCompleteRef = useRef(onComplete)\n onCompleteRef.current = onComplete\n\n useSignalEffect(() => {\n if (lines.length === 0) return\n let rafId: number\n function tick() {\n const li = lineIndex.value\n const ci = charIndex.value\n if (li >= lines.length) {\n onCompleteRef.current?.()\n if (loop) {\n lineIndex.value = 0\n charIndex.value = 0\n }\n return\n }\n const line = lines[li]!\n const target = line.text\n if (ci < target.length) {\n charIndex.value = ci + speed\n } else {\n lineIndex.value = li + 1\n charIndex.value = 0\n }\n rafId = requestAnimationFrame(tick)\n }\n rafId = requestAnimationFrame(tick)\n return () => cancelAnimationFrame(rafId)\n })\n\n const renderedLines = lines.slice(0, lineIndex.value + 1).map((line, i) => {\n const isCurrentLine = i === lineIndex.value\n const text = isCurrentLine ? line.text.slice(0, charIndex.value) : line.text\n return (\n <div key={i} className={styles.line} data-type={line.type ?? 'output'}>\n {line.prefix && <span className={styles.prefix}>{line.prefix}</span>}\n <span className={styles.text}>{text}</span>\n {isCurrentLine && lineIndex.value < lines.length && (\n <span className={styles.cursor} aria-hidden=\"true\" />\n )}\n </div>\n )\n })\n\n return (\n <div\n role=\"log\"\n aria-live=\"polite\"\n aria-label=\"Terminal output\"\n className={[styles.root, className].filter(Boolean).join(' ')}\n >\n {renderedLines}\n </div>\n )\n}\n","'use client'\nimport { useRef } from 'react'\nimport { useSignal, useSignals } from '@cascivo/core'\nimport { builtin, t } from '@cascivo/i18n'\nimport { StreamingText } from './streaming-text'\nimport styles from './ai-chat.module.css'\n\nexport interface ChatMessage {\n id: string\n role: 'user' | 'assistant' | 'system'\n content: string\n}\n\nexport interface AiChatProps {\n messages: ChatMessage[]\n onSend: (text: string) => void\n isStreaming?: boolean\n streamingText?: string\n className?: string\n}\n\nexport function AiChat({\n messages,\n onSend,\n isStreaming = false,\n streamingText,\n className,\n}: AiChatProps) {\n useSignals()\n const inputValue = useSignal('')\n const listRef = useRef<HTMLDivElement>(null)\n\n function handleSend() {\n const text = inputValue.value.trim()\n if (!text) return\n onSend(text)\n inputValue.value = ''\n }\n\n function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n handleSend()\n }\n }\n\n return (\n <div className={[styles.root, className].filter(Boolean).join(' ')}>\n <div ref={listRef} className={styles.messages} role=\"log\" aria-live=\"polite\">\n {messages\n .filter((m) => m.role !== 'system')\n .map((msg) => (\n <div key={msg.id} className={styles.message} data-role={msg.role}>\n <span className={styles.roleLabel}>\n {msg.role === 'user' ? t(builtin.ai.you) : t(builtin.ai.assistant)}\n </span>\n <div className={styles.content}>{msg.content}</div>\n </div>\n ))}\n {isStreaming && streamingText !== undefined && (\n <div className={styles.message} data-role=\"assistant\">\n <span className={styles.roleLabel}>{t(builtin.ai.assistant)}</span>\n <div className={styles.content}>\n <StreamingText text={streamingText} />\n </div>\n </div>\n )}\n </div>\n <div className={styles.inputArea}>\n <textarea\n className={styles.textarea}\n placeholder={t(builtin.ai.placeholder)}\n value={inputValue.value}\n onChange={(e) => {\n inputValue.value = e.target.value\n }}\n onKeyDown={handleKeyDown}\n rows={1}\n aria-label={t(builtin.ai.placeholder)}\n />\n <button\n type=\"button\"\n className={styles.sendButton}\n onClick={handleSend}\n disabled={isStreaming}\n aria-label={t(builtin.ai.send)}\n >\n {t(builtin.ai.send)}\n </button>\n </div>\n </div>\n )\n}\n"],"mappings":"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"}
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
@layer cascivo.component {
|
|
2
|
+
.dgjyMq_root {
|
|
3
|
+
display: inline;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.dgjyMq_cursor {
|
|
7
|
+
vertical-align: text-bottom;
|
|
8
|
+
background: currentColor;
|
|
9
|
+
width: 2px;
|
|
10
|
+
height: 1em;
|
|
11
|
+
margin-inline-start: 1px;
|
|
12
|
+
animation: 1s step-end infinite dgjyMq_cascade-cursor-blink;
|
|
13
|
+
display: inline-block;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@keyframes dgjyMq_cascade-cursor-blink {
|
|
18
|
+
0%, 100% {
|
|
19
|
+
opacity: 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
50% {
|
|
23
|
+
opacity: 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
@layer cascivo.component {
|
|
27
|
+
.iirJiG_root {
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: var(--cascivo-space-1);
|
|
30
|
+
padding: var(--cascivo-space-1) var(--cascivo-space-2);
|
|
31
|
+
border-radius: var(--cascivo-radius-indicator);
|
|
32
|
+
font-size: var(--cascivo-font-size-xs);
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
line-height: 1;
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.iirJiG_root[data-variant="generating"] {
|
|
39
|
+
background: var(--cascivo-color-surface-2);
|
|
40
|
+
color: var(--cascivo-color-foreground-muted);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.iirJiG_root[data-variant="generating"]:before {
|
|
44
|
+
content: "";
|
|
45
|
+
border-radius: var(--cascivo-radius-full);
|
|
46
|
+
background: currentColor;
|
|
47
|
+
block-size: .375rem;
|
|
48
|
+
inline-size: .375rem;
|
|
49
|
+
animation: 1.5s ease-in-out infinite iirJiG_cascade-ai-pulse;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.iirJiG_root[data-variant="done"] {
|
|
53
|
+
background: var(--cascivo-color-success-subtle);
|
|
54
|
+
color: var(--cascivo-color-success-foreground);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.iirJiG_root[data-variant="error"] {
|
|
58
|
+
background: var(--cascivo-color-destructive-subtle);
|
|
59
|
+
color: var(--cascivo-color-destructive-foreground);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@keyframes iirJiG_cascade-ai-pulse {
|
|
64
|
+
0%, 100% {
|
|
65
|
+
opacity: 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
50% {
|
|
69
|
+
opacity: .6;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
@layer cascivo.component {
|
|
73
|
+
.GFOf3W_root {
|
|
74
|
+
color: oklch(90% .02 145);
|
|
75
|
+
font-family: ui-monospace, Cascadia Code, Source Code Pro, monospace;
|
|
76
|
+
font-size: var(--cascivo-font-size-sm, .875rem);
|
|
77
|
+
padding: var(--cascivo-space-4);
|
|
78
|
+
border-radius: var(--cascivo-radius-surface);
|
|
79
|
+
background: oklch(12% .01 250);
|
|
80
|
+
line-height: 1.6;
|
|
81
|
+
overflow-x: auto;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.GFOf3W_line {
|
|
85
|
+
gap: var(--cascivo-space-2);
|
|
86
|
+
display: flex;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.GFOf3W_prefix {
|
|
90
|
+
color: oklch(70% .15 145);
|
|
91
|
+
user-select: none;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.GFOf3W_text {
|
|
95
|
+
flex: 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.GFOf3W_cursor {
|
|
99
|
+
background: oklch(90% .02 145);
|
|
100
|
+
width: 8px;
|
|
101
|
+
height: 1em;
|
|
102
|
+
animation: 1s step-end infinite GFOf3W_cascade-cursor-blink;
|
|
103
|
+
display: inline-block;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.GFOf3W_line[data-type="command"] .GFOf3W_text {
|
|
107
|
+
color: oklch(90% .1 250);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.GFOf3W_line[data-type="error"] .GFOf3W_text {
|
|
111
|
+
color: oklch(75% .15 22);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.GFOf3W_line[data-type="comment"] .GFOf3W_text {
|
|
115
|
+
color: oklch(60% .02 145);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@keyframes GFOf3W_cascade-cursor-blink {
|
|
120
|
+
0%, 100% {
|
|
121
|
+
opacity: 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
50% {
|
|
125
|
+
opacity: 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
@layer cascivo.component {
|
|
129
|
+
.oL95jG_root {
|
|
130
|
+
background: var(--cascivo-color-background);
|
|
131
|
+
border: 1px solid var(--cascivo-border-default);
|
|
132
|
+
border-radius: var(--cascivo-radius-surface);
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
height: 100%;
|
|
135
|
+
min-height: 400px;
|
|
136
|
+
display: flex;
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.oL95jG_messages {
|
|
141
|
+
padding: var(--cascivo-space-4);
|
|
142
|
+
gap: var(--cascivo-space-3);
|
|
143
|
+
flex-direction: column;
|
|
144
|
+
flex: 1;
|
|
145
|
+
display: flex;
|
|
146
|
+
overflow-y: auto;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.oL95jG_message {
|
|
150
|
+
gap: var(--cascivo-space-1);
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
max-width: 80%;
|
|
153
|
+
display: flex;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.oL95jG_message[data-role="user"] {
|
|
157
|
+
align-self: flex-end;
|
|
158
|
+
align-items: flex-end;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.oL95jG_message[data-role="assistant"] {
|
|
162
|
+
align-self: flex-start;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.oL95jG_roleLabel {
|
|
166
|
+
font-size: var(--cascivo-font-size-xs, .75rem);
|
|
167
|
+
color: var(--cascivo-color-foreground-muted);
|
|
168
|
+
font-weight: 500;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.oL95jG_content {
|
|
172
|
+
padding: var(--cascivo-space-2) var(--cascivo-space-3);
|
|
173
|
+
border-radius: var(--cascivo-radius-control);
|
|
174
|
+
background: var(--cascivo-color-surface);
|
|
175
|
+
line-height: 1.5;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.oL95jG_message[data-role="user"] .oL95jG_content {
|
|
179
|
+
background: var(--cascivo-color-accent);
|
|
180
|
+
color: var(--cascivo-color-accent-foreground);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.oL95jG_inputArea {
|
|
184
|
+
gap: var(--cascivo-space-2);
|
|
185
|
+
padding: var(--cascivo-space-3);
|
|
186
|
+
border-block-start: 1px solid var(--cascivo-border-subtle);
|
|
187
|
+
background: var(--cascivo-color-surface);
|
|
188
|
+
display: flex;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.oL95jG_textarea {
|
|
192
|
+
resize: none;
|
|
193
|
+
border: 1px solid var(--cascivo-border-default);
|
|
194
|
+
border-radius: var(--cascivo-radius-control);
|
|
195
|
+
padding: var(--cascivo-space-2) var(--cascivo-space-3);
|
|
196
|
+
font: inherit;
|
|
197
|
+
background: var(--cascivo-color-background);
|
|
198
|
+
color: var(--cascivo-color-foreground);
|
|
199
|
+
flex: 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.oL95jG_sendButton {
|
|
203
|
+
padding: var(--cascivo-space-2) var(--cascivo-space-4);
|
|
204
|
+
background: var(--cascivo-color-accent);
|
|
205
|
+
color: var(--cascivo-color-accent-foreground);
|
|
206
|
+
border-radius: var(--cascivo-radius-control);
|
|
207
|
+
font: inherit;
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
border: none;
|
|
210
|
+
font-weight: 500;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.oL95jG_sendButton:disabled {
|
|
214
|
+
opacity: .5;
|
|
215
|
+
cursor: not-allowed;
|
|
216
|
+
}
|
|
217
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cascivo/ai",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "AI-native components for cascivo — StreamingText, AiLabel, Terminal, AiChat",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"cascivo",
|
|
8
|
+
"chat",
|
|
9
|
+
"css",
|
|
10
|
+
"design-system",
|
|
11
|
+
"react",
|
|
12
|
+
"signals",
|
|
13
|
+
"streaming"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/cascivo/cascivo/tree/main/packages/ai#readme",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "urbanisierung",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/cascivo/cascivo.git",
|
|
21
|
+
"directory": "packages/ai"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": [
|
|
28
|
+
"**/*.css"
|
|
29
|
+
],
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": "./dist/index.mjs",
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"default": "./dist/index.mjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"provenance": true
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@cascivo/core": "^0.2.6",
|
|
44
|
+
"@cascivo/tokens": "^0.3.8",
|
|
45
|
+
"@cascivo/i18n": "^0.2.3"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
49
|
+
"@testing-library/react": "^16.3.2",
|
|
50
|
+
"@testing-library/user-event": "^14",
|
|
51
|
+
"@types/react": "^19.2.17",
|
|
52
|
+
"react": "^19.2.7",
|
|
53
|
+
"typescript": "^5",
|
|
54
|
+
"vite-plus": "^0.2.1"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@preact/signals-react": ">=2.0.0",
|
|
58
|
+
"react": ">=18.0.0"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "vp pack",
|
|
62
|
+
"check": "tsc --noEmit",
|
|
63
|
+
"test": "vp test",
|
|
64
|
+
"dev": "vp pack --watch"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/readme.body.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
AI-native components for cascivo — the presentation layer for AI features: streaming text, generation status, animated terminals, and a full chat surface. Signal-driven like the rest of the design system (no `useState`, no `useEffect`), typed, and localized via `@cascivo/i18n` built-in catalogs.
|
|
2
|
+
|
|
3
|
+
> Newly published — first release on npm as of `0.1.0`.
|
|
4
|
+
|
|
5
|
+
**Peer dependencies:** `react >=18` and `@preact/signals-react >=2` (the components are signal-driven). Styles ship as CSS modules bundled with the components — no stylesheet import needed. Pair with `@cascivo/themes` for tokens.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @cascivo/ai @preact/signals-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## `StreamingText`
|
|
12
|
+
|
|
13
|
+
Types text out character-by-character with a blinking cursor, driven by `requestAnimationFrame`. Feed it a growing string (e.g. an accumulating LLM response) or a static one:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { StreamingText } from '@cascivo/ai'
|
|
17
|
+
;<StreamingText
|
|
18
|
+
text={response} // target text; typing catches up as it grows
|
|
19
|
+
speed={2} // characters per frame (default 2)
|
|
20
|
+
onComplete={() => setDone(true)}
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The cursor renders only while the displayed text lags the target, and the animation resets when `text` is replaced with a shorter string.
|
|
25
|
+
|
|
26
|
+
## `AiLabel`
|
|
27
|
+
|
|
28
|
+
A `role="status"` badge for AI-generated content with three variants — `generating` (default), `done`, `error`. Labels come from the `@cascivo/i18n` built-in catalog, so they localize automatically:
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { AiLabel } from '@cascivo/ai'
|
|
32
|
+
|
|
33
|
+
<AiLabel variant="generating" />
|
|
34
|
+
<AiLabel variant="done" />
|
|
35
|
+
<AiLabel variant="error" />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It extends `HTMLAttributes<HTMLSpanElement>`, so `className`, `title`, etc. pass through.
|
|
39
|
+
|
|
40
|
+
## `Terminal`
|
|
41
|
+
|
|
42
|
+
An animated terminal window (`role="log"`, `aria-live="polite"`) that types out a script of lines. Each line has a `type` (`command` | `output` | `error` | `comment`) for per-line styling and an optional `prefix` (e.g. `$`):
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { Terminal } from '@cascivo/ai'
|
|
46
|
+
;<Terminal
|
|
47
|
+
lines={[
|
|
48
|
+
{ text: 'npx cascivo add button', prefix: '$', type: 'command' },
|
|
49
|
+
{ text: 'Added button to src/components.', type: 'output' },
|
|
50
|
+
{ text: '# done in 1.2s', type: 'comment' },
|
|
51
|
+
]}
|
|
52
|
+
speed={3} // characters per frame (default 3)
|
|
53
|
+
loop // restart from the top after the last line
|
|
54
|
+
onComplete={() => {}}
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## `AiChat`
|
|
59
|
+
|
|
60
|
+
A complete chat surface: message list (`role="log"`), streaming assistant bubble, and a textarea composer with Enter-to-send (Shift+Enter for a newline). It is fully controlled — you own the message array and the send handler:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { AiChat, type ChatMessage } from '@cascivo/ai'
|
|
64
|
+
|
|
65
|
+
const messages: ChatMessage[] = [
|
|
66
|
+
{ id: '1', role: 'user', content: 'What is cascivo?' },
|
|
67
|
+
{ id: '2', role: 'assistant', content: 'A CSS-native design system.' },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
<AiChat
|
|
71
|
+
messages={messages} // system messages are accepted but not rendered
|
|
72
|
+
onSend={(text) => append({ role: 'user', content: text })}
|
|
73
|
+
isStreaming={streaming} // disables the send button
|
|
74
|
+
streamingText={partial} // rendered as a live assistant bubble via StreamingText
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Role labels ("You" / "Assistant"), the input placeholder, and the send button label all default from the `@cascivo/i18n` built-in catalog.
|