@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 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
+ [![npm](https://img.shields.io/npm/v/%40cascivo%2Fai?style=flat-square&color=0079bf)](https://www.npmjs.com/package/@cascivo/ai)
9
+ [![downloads](https://img.shields.io/npm/dm/%40cascivo%2Fai?style=flat-square&color=0079bf)](https://www.npmjs.com/package/@cascivo/ai)
10
+ [![license](https://img.shields.io/npm/l/%40cascivo%2Fai?style=flat-square&color=0079bf)](https://github.com/cascivo/cascivo/blob/main/LICENSE)
11
+ ![types](https://img.shields.io/badge/types-included-0079bf?style=flat-square&logo=typescript&logoColor=white)
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>
@@ -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.