tekivex-ui 2.5.10 → 2.5.11

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.
@@ -0,0 +1,24 @@
1
+ import { type CSSProperties, type ReactNode } from 'react';
2
+ export type AIRole = 'user' | 'assistant' | 'system';
3
+ export interface TkxAIChatBubbleProps {
4
+ /** Who sent this message */
5
+ role: AIRole;
6
+ /** Message text (will be sanitized) or arbitrary ReactNode */
7
+ content: string | ReactNode;
8
+ /** Optional avatar URL or initials */
9
+ avatar?: string;
10
+ /** Optional display name */
11
+ name?: string;
12
+ /** ISO timestamp string */
13
+ timestamp?: string;
14
+ /** Animate text in character-by-character (streaming effect) */
15
+ streaming?: boolean;
16
+ /** AI confidence 0–100 — renders a mini confidence bar when provided */
17
+ confidence?: number;
18
+ /** Show copy-to-clipboard button */
19
+ copyable?: boolean;
20
+ className?: string;
21
+ style?: CSSProperties;
22
+ }
23
+ export declare function TkxAIChatBubble({ role, content, avatar, name, timestamp, streaming, confidence, copyable, className, style, }: TkxAIChatBubbleProps): import("react/jsx-runtime").JSX.Element;
24
+ //# sourceMappingURL=TkxAIChatBubble.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TkxAIChatBubble.d.ts","sourceRoot":"","sources":["../../../src/components/TkxAIChatBubble.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAKxF,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AAErD,MAAM,WAAW,oBAAoB;IACnC,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAiCD,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,OAAO,EACP,MAAM,EACN,IAAI,EACJ,SAAS,EACT,SAAiB,EACjB,UAAU,EACV,QAAgB,EAChB,SAAS,EACT,KAAK,GACN,EAAE,oBAAoB,2CA0ItB"}
@@ -0,0 +1,17 @@
1
+ import { type CSSProperties } from 'react';
2
+ export interface TkxAIConfidenceBarProps {
3
+ /** 0–100 confidence value */
4
+ value: number;
5
+ /** Optional label for the metric being shown */
6
+ label?: string;
7
+ /** Show numeric percentage text */
8
+ showLabel?: boolean;
9
+ /** Size of the bar */
10
+ size?: 'sm' | 'md' | 'lg';
11
+ /** Animate on mount */
12
+ animate?: boolean;
13
+ className?: string;
14
+ style?: CSSProperties;
15
+ }
16
+ export declare function TkxAIConfidenceBar({ value, label, showLabel, size, animate, className, style, }: TkxAIConfidenceBarProps): import("react/jsx-runtime").JSX.Element;
17
+ //# sourceMappingURL=TkxAIConfidenceBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TkxAIConfidenceBar.d.ts","sourceRoot":"","sources":["../../../src/components/TkxAIConfidenceBar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAI9D,MAAM,WAAW,uBAAuB;IACtC,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sBAAsB;IACtB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,uBAAuB;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAaD,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,KAAK,EACL,SAAgB,EAChB,IAAW,EACX,OAAc,EACd,SAAS,EACT,KAAK,GACN,EAAE,uBAAuB,2CA4FzB"}
@@ -0,0 +1,17 @@
1
+ import { type CSSProperties } from 'react';
2
+ export interface TkxAIThinkingProps {
3
+ /** Text shown while thinking */
4
+ label?: string;
5
+ /** What steps the AI is working through (optional animated list) */
6
+ steps?: string[];
7
+ /** Size variant */
8
+ size?: 'sm' | 'md' | 'lg';
9
+ /** Visual style */
10
+ variant?: 'dots' | 'pulse' | 'wave' | 'orbit';
11
+ /** Whether actively thinking (false = done) */
12
+ active?: boolean;
13
+ className?: string;
14
+ style?: CSSProperties;
15
+ }
16
+ export declare function TkxAIThinking({ label, steps, size, variant, active, className, style, }: TkxAIThinkingProps): import("react/jsx-runtime").JSX.Element;
17
+ //# sourceMappingURL=TkxAIThinking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TkxAIThinking.d.ts","sourceRoot":"","sources":["../../../src/components/TkxAIThinking.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAIhE,MAAM,WAAW,kBAAkB;IACjC,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,mBAAmB;IACnB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,mBAAmB;IACnB,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAC9C,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AA4ED,wBAAgB,aAAa,CAAC,EAC5B,KAAmB,EACnB,KAAK,EACL,IAAW,EACX,OAAgB,EAChB,MAAa,EACb,SAAS,EACT,KAAK,GACN,EAAE,kBAAkB,2CAyFpB"}
@@ -69,4 +69,7 @@ export * from './TkxAnchor';
69
69
  export * from './TkxCascader';
70
70
  export * from './TkxList';
71
71
  export * from './TkxDropdown';
72
+ export * from './TkxAIConfidenceBar';
73
+ export * from './TkxAIChatBubble';
74
+ export * from './TkxAIThinking';
72
75
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAI1B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAI1B,cAAc,eAAe,CAAC;AAG9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tekivex-ui",
3
- "version": "2.5.10",
3
+ "version": "2.5.11",
4
4
  "description": "Production-grade React component library — 70+ components, WCAG 2.1 AAA, WAI-ARIA 1.2, built-in charts, headless primitives, zero-runtime CSS engine, TypeScript-first.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -0,0 +1,209 @@
1
+ import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { cx, tkx } from '../engine/tkx';
4
+ import { sanitizeString } from '../engine/security';
5
+
6
+ export type AIRole = 'user' | 'assistant' | 'system';
7
+
8
+ export interface TkxAIChatBubbleProps {
9
+ /** Who sent this message */
10
+ role: AIRole;
11
+ /** Message text (will be sanitized) or arbitrary ReactNode */
12
+ content: string | ReactNode;
13
+ /** Optional avatar URL or initials */
14
+ avatar?: string;
15
+ /** Optional display name */
16
+ name?: string;
17
+ /** ISO timestamp string */
18
+ timestamp?: string;
19
+ /** Animate text in character-by-character (streaming effect) */
20
+ streaming?: boolean;
21
+ /** AI confidence 0–100 — renders a mini confidence bar when provided */
22
+ confidence?: number;
23
+ /** Show copy-to-clipboard button */
24
+ copyable?: boolean;
25
+ className?: string;
26
+ style?: CSSProperties;
27
+ }
28
+
29
+ function confidenceColor(v: number, primary: string) {
30
+ if (v >= 80) return '#10b981';
31
+ if (v >= 55) return primary;
32
+ if (v >= 30) return '#f59e0b';
33
+ return '#ef4444';
34
+ }
35
+
36
+ function Avatar({ src, name, size = 32 }: { src?: string; name?: string; size?: number }) {
37
+ const theme = useTheme();
38
+ const initials = name
39
+ ? name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
40
+ : '?';
41
+
42
+ if (src) {
43
+ return (
44
+ <img src={src} alt={name ?? 'avatar'} width={size} height={size}
45
+ style={{ borderRadius: '50%', objectFit: 'cover', flexShrink: 0 }} />
46
+ );
47
+ }
48
+ return (
49
+ <div style={{
50
+ width: size, height: size, borderRadius: '50%', flexShrink: 0,
51
+ background: `${theme.primary}22`, border: `1px solid ${theme.primary}44`,
52
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
53
+ fontSize: size * 0.35, fontWeight: 700, color: theme.primary,
54
+ }}>
55
+ {initials}
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export function TkxAIChatBubble({
61
+ role,
62
+ content,
63
+ avatar,
64
+ name,
65
+ timestamp,
66
+ streaming = false,
67
+ confidence,
68
+ copyable = false,
69
+ className,
70
+ style,
71
+ }: TkxAIChatBubbleProps) {
72
+ const theme = useTheme();
73
+ const isUser = role === 'user';
74
+ const isSystem = role === 'system';
75
+
76
+ // Streaming typewriter effect
77
+ const fullText = typeof content === 'string' ? sanitizeString(content) : '';
78
+ const [displayed, setDisplayed] = useState(streaming ? '' : fullText);
79
+ const [copied, setCopied] = useState(false);
80
+ const idxRef = useRef(0);
81
+
82
+ useEffect(() => {
83
+ if (!streaming || typeof content !== 'string') return;
84
+ idxRef.current = 0;
85
+ setDisplayed('');
86
+ const interval = setInterval(() => {
87
+ idxRef.current++;
88
+ setDisplayed(fullText.slice(0, idxRef.current));
89
+ if (idxRef.current >= fullText.length) clearInterval(interval);
90
+ }, 18);
91
+ return () => clearInterval(interval);
92
+ }, [fullText, streaming, content]);
93
+
94
+ const handleCopy = () => {
95
+ navigator.clipboard?.writeText(fullText);
96
+ setCopied(true);
97
+ setTimeout(() => setCopied(false), 2000);
98
+ };
99
+
100
+ // System message style
101
+ if (isSystem) {
102
+ return (
103
+ <div style={{
104
+ display: 'flex', justifyContent: 'center', padding: '8px 0',
105
+ }}>
106
+ <div style={{
107
+ padding: '6px 16px', borderRadius: 999, fontSize: 12,
108
+ background: `${theme.border}`, color: theme.textMuted,
109
+ fontStyle: 'italic',
110
+ }}>
111
+ {typeof content === 'string' ? sanitizeString(content) : content}
112
+ </div>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ const bubbleStyle: CSSProperties = {
118
+ maxWidth: '75%',
119
+ padding: '12px 16px',
120
+ borderRadius: isUser ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
121
+ background: isUser
122
+ ? `linear-gradient(135deg, ${theme.primary}, ${theme.primary}cc)`
123
+ : `${theme.surface}`,
124
+ border: isUser ? 'none' : `1px solid ${theme.border}`,
125
+ color: isUser ? '#fff' : theme.text,
126
+ fontSize: 14,
127
+ lineHeight: 1.65,
128
+ boxShadow: isUser
129
+ ? `0 4px 16px -4px ${theme.primary}55`
130
+ : `0 2px 8px -2px rgba(0,0,0,0.2)`,
131
+ position: 'relative',
132
+ wordBreak: 'break-word',
133
+ };
134
+
135
+ return (
136
+ <div
137
+ className={cx(tkx('flex gap-2'), className)}
138
+ style={{
139
+ flexDirection: isUser ? 'row-reverse' : 'row',
140
+ alignItems: 'flex-end',
141
+ ...style,
142
+ }}
143
+ >
144
+ {/* Avatar */}
145
+ <Avatar src={avatar} name={name ?? (isUser ? 'You' : 'AI')} />
146
+
147
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4, alignItems: isUser ? 'flex-end' : 'flex-start' }}>
148
+ {/* Name + timestamp */}
149
+ {(name || timestamp) && (
150
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center', paddingInline: 4 }}>
151
+ {name && <span style={{ fontSize: 11, fontWeight: 700, color: theme.textMuted }}>{name}</span>}
152
+ {timestamp && <span style={{ fontSize: 10, color: theme.textMuted }}>{timestamp}</span>}
153
+ </div>
154
+ )}
155
+
156
+ {/* Bubble */}
157
+ <div style={bubbleStyle}>
158
+ {typeof content === 'string' ? (
159
+ <>
160
+ {streaming ? displayed : sanitizeString(content)}
161
+ {streaming && displayed.length < fullText.length && (
162
+ <span style={{ display: 'inline-block', width: 2, height: 14, background: isUser ? '#fff' : theme.primary, marginLeft: 2, animation: 'tkx-blink 1s step-end infinite', verticalAlign: 'text-bottom' }} />
163
+ )}
164
+ </>
165
+ ) : content}
166
+
167
+ {/* Copy button */}
168
+ {copyable && !isUser && (
169
+ <button
170
+ onClick={handleCopy}
171
+ title="Copy message"
172
+ style={{
173
+ position: 'absolute', top: 8, right: 8,
174
+ background: 'none', border: 'none', cursor: 'pointer',
175
+ fontSize: 13, color: copied ? '#10b981' : theme.textMuted,
176
+ padding: '2px 4px', borderRadius: 4,
177
+ transition: 'color 0.2s',
178
+ }}
179
+ >
180
+ {copied ? '✓' : '⎘'}
181
+ </button>
182
+ )}
183
+ </div>
184
+
185
+ {/* Confidence bar (AI only) */}
186
+ {!isUser && confidence !== undefined && (
187
+ <div style={{ paddingInline: 4, width: '100%', maxWidth: 240 }}>
188
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
189
+ <div style={{ flex: 1, height: 3, borderRadius: 3, background: theme.border, overflow: 'hidden' }}>
190
+ <div style={{
191
+ height: '100%', width: `${confidence}%`,
192
+ background: confidenceColor(confidence, theme.primary),
193
+ borderRadius: 3, transition: 'width 0.8s ease',
194
+ }} />
195
+ </div>
196
+ <span style={{ fontSize: 10, color: theme.textMuted, whiteSpace: 'nowrap' }}>
197
+ {confidence}% confident
198
+ </span>
199
+ </div>
200
+ </div>
201
+ )}
202
+ </div>
203
+
204
+ <style>{`
205
+ @keyframes tkx-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
206
+ `}</style>
207
+ </div>
208
+ );
209
+ }
@@ -0,0 +1,131 @@
1
+ import { useEffect, useRef, type CSSProperties } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { cx, tkx } from '../engine/tkx';
4
+
5
+ export interface TkxAIConfidenceBarProps {
6
+ /** 0–100 confidence value */
7
+ value: number;
8
+ /** Optional label for the metric being shown */
9
+ label?: string;
10
+ /** Show numeric percentage text */
11
+ showLabel?: boolean;
12
+ /** Size of the bar */
13
+ size?: 'sm' | 'md' | 'lg';
14
+ /** Animate on mount */
15
+ animate?: boolean;
16
+ className?: string;
17
+ style?: CSSProperties;
18
+ }
19
+
20
+ const HEIGHT: Record<NonNullable<TkxAIConfidenceBarProps['size']>, number> = {
21
+ sm: 4, md: 8, lg: 12,
22
+ };
23
+
24
+ function confidenceColor(value: number, primary: string): string {
25
+ if (value >= 80) return '#10b981'; // green
26
+ if (value >= 55) return primary; // brand
27
+ if (value >= 30) return '#f59e0b'; // amber
28
+ return '#ef4444'; // red
29
+ }
30
+
31
+ export function TkxAIConfidenceBar({
32
+ value,
33
+ label,
34
+ showLabel = true,
35
+ size = 'md',
36
+ animate = true,
37
+ className,
38
+ style,
39
+ }: TkxAIConfidenceBarProps) {
40
+ const theme = useTheme();
41
+ const fillRef = useRef<HTMLDivElement>(null);
42
+ const clamped = Math.max(0, Math.min(100, value));
43
+ const color = confidenceColor(clamped, theme.primary);
44
+ const h = HEIGHT[size];
45
+
46
+ useEffect(() => {
47
+ if (!animate || !fillRef.current) return;
48
+ const el = fillRef.current;
49
+ el.style.width = '0%';
50
+ const raf = requestAnimationFrame(() => {
51
+ el.style.transition = 'width 0.9s cubic-bezier(0.4, 0, 0.2, 1)';
52
+ el.style.width = `${clamped}%`;
53
+ });
54
+ return () => cancelAnimationFrame(raf);
55
+ }, [clamped, animate]);
56
+
57
+ const label_ = typeof clamped === 'number'
58
+ ? `${clamped.toFixed(0)}% AI confidence${label ? ` for ${label}` : ''}`
59
+ : label;
60
+
61
+ return (
62
+ <div
63
+ className={cx(tkx('flex flex-col gap-1'), className)}
64
+ style={style}
65
+ role="meter"
66
+ aria-valuenow={clamped}
67
+ aria-valuemin={0}
68
+ aria-valuemax={100}
69
+ aria-label={label_}
70
+ >
71
+ {(label || showLabel) && (
72
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
73
+ {label && (
74
+ <span style={{ fontSize: 12, fontWeight: 600, color: theme.textMuted }}>
75
+ {label}
76
+ </span>
77
+ )}
78
+ {showLabel && (
79
+ <span style={{ fontSize: 12, fontWeight: 700, color, marginLeft: 'auto' }}>
80
+ {clamped.toFixed(0)}%
81
+ </span>
82
+ )}
83
+ </div>
84
+ )}
85
+
86
+ {/* Track */}
87
+ <div style={{
88
+ height: h,
89
+ borderRadius: h,
90
+ background: `${theme.border}`,
91
+ overflow: 'hidden',
92
+ position: 'relative',
93
+ }}>
94
+ {/* Shimmer */}
95
+ <div style={{
96
+ position: 'absolute', inset: 0,
97
+ background: `linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.12) 50%, transparent 100%)`,
98
+ backgroundSize: '200% 100%',
99
+ animation: 'tkx-ai-shimmer 2s linear infinite',
100
+ pointerEvents: 'none',
101
+ zIndex: 1,
102
+ }} />
103
+ {/* Fill */}
104
+ <div
105
+ ref={fillRef}
106
+ style={{
107
+ height: '100%',
108
+ width: animate ? '0%' : `${clamped}%`,
109
+ background: `linear-gradient(90deg, ${color}cc, ${color})`,
110
+ borderRadius: h,
111
+ boxShadow: `0 0 8px ${color}66`,
112
+ transition: animate ? undefined : 'width 0.3s ease',
113
+ }}
114
+ />
115
+ </div>
116
+
117
+ {/* Confidence label */}
118
+ <div style={{ fontSize: 10, color: theme.textMuted, marginTop: 2, display: 'flex', gap: 4, alignItems: 'center' }}>
119
+ <span style={{ width: 6, height: 6, borderRadius: '50%', background: color, display: 'inline-block', flexShrink: 0 }} />
120
+ {clamped >= 80 ? 'High confidence' : clamped >= 55 ? 'Moderate confidence' : clamped >= 30 ? 'Low confidence' : 'Very low — review manually'}
121
+ </div>
122
+
123
+ <style>{`
124
+ @keyframes tkx-ai-shimmer {
125
+ 0% { background-position: -200% 0; }
126
+ 100% { background-position: 200% 0; }
127
+ }
128
+ `}</style>
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,191 @@
1
+ import { useState, useEffect, type CSSProperties } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { cx, tkx } from '../engine/tkx';
4
+
5
+ export interface TkxAIThinkingProps {
6
+ /** Text shown while thinking */
7
+ label?: string;
8
+ /** What steps the AI is working through (optional animated list) */
9
+ steps?: string[];
10
+ /** Size variant */
11
+ size?: 'sm' | 'md' | 'lg';
12
+ /** Visual style */
13
+ variant?: 'dots' | 'pulse' | 'wave' | 'orbit';
14
+ /** Whether actively thinking (false = done) */
15
+ active?: boolean;
16
+ className?: string;
17
+ style?: CSSProperties;
18
+ }
19
+
20
+ function DotsIndicator({ color }: { color: string }) {
21
+ return (
22
+ <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
23
+ {[0, 1, 2].map(i => (
24
+ <div key={i} style={{
25
+ width: 7, height: 7, borderRadius: '50%',
26
+ background: color,
27
+ animation: `tkx-thinking-dot 1.2s ease-in-out ${i * 0.18}s infinite`,
28
+ }} />
29
+ ))}
30
+ </div>
31
+ );
32
+ }
33
+
34
+ function PulseIndicator({ color }: { color: string }) {
35
+ return (
36
+ <div style={{ position: 'relative', width: 24, height: 24 }}>
37
+ <div style={{
38
+ position: 'absolute', inset: 0, borderRadius: '50%',
39
+ background: color, opacity: 0.15,
40
+ animation: 'tkx-thinking-pulse 1.5s ease-out infinite',
41
+ }} />
42
+ <div style={{
43
+ position: 'absolute', inset: 4, borderRadius: '50%',
44
+ background: color, opacity: 0.6,
45
+ animation: 'tkx-thinking-pulse 1.5s ease-out 0.3s infinite',
46
+ }} />
47
+ <div style={{
48
+ position: 'absolute', inset: 8, borderRadius: '50%',
49
+ background: color,
50
+ }} />
51
+ </div>
52
+ );
53
+ }
54
+
55
+ function WaveIndicator({ color }: { color: string }) {
56
+ return (
57
+ <div style={{ display: 'flex', gap: 3, alignItems: 'center', height: 20 }}>
58
+ {[0, 1, 2, 3, 4].map(i => (
59
+ <div key={i} style={{
60
+ width: 3, borderRadius: 3,
61
+ background: color,
62
+ animation: `tkx-thinking-wave 1s ease-in-out ${i * 0.1}s infinite`,
63
+ }} />
64
+ ))}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function OrbitIndicator({ color }: { color: string }) {
70
+ return (
71
+ <div style={{ position: 'relative', width: 28, height: 28 }}>
72
+ <div style={{
73
+ position: 'absolute', inset: 0, borderRadius: '50%',
74
+ border: `2px solid ${color}33`,
75
+ }} />
76
+ <div style={{
77
+ position: 'absolute', inset: 0, borderRadius: '50%',
78
+ border: `2px solid transparent`,
79
+ borderTopColor: color,
80
+ animation: 'tkx-thinking-orbit 0.8s linear infinite',
81
+ }} />
82
+ <div style={{
83
+ position: 'absolute', inset: '38%', borderRadius: '50%',
84
+ background: color, opacity: 0.8,
85
+ }} />
86
+ </div>
87
+ );
88
+ }
89
+
90
+ const FONT: Record<NonNullable<TkxAIThinkingProps['size']>, number> = {
91
+ sm: 12, md: 14, lg: 16,
92
+ };
93
+
94
+ export function TkxAIThinking({
95
+ label = 'Thinking…',
96
+ steps,
97
+ size = 'md',
98
+ variant = 'dots',
99
+ active = true,
100
+ className,
101
+ style,
102
+ }: TkxAIThinkingProps) {
103
+ const theme = useTheme();
104
+ const [stepIdx, setStepIdx] = useState(0);
105
+
106
+ useEffect(() => {
107
+ if (!steps?.length || !active) return;
108
+ const id = setInterval(() => {
109
+ setStepIdx(i => (i + 1) % steps.length);
110
+ }, 1800);
111
+ return () => clearInterval(id);
112
+ }, [steps, active]);
113
+
114
+ const color = theme.primary;
115
+ const fs = FONT[size];
116
+
117
+ const indicator = {
118
+ dots: <DotsIndicator color={color} />,
119
+ pulse: <PulseIndicator color={color} />,
120
+ wave: <WaveIndicator color={color} />,
121
+ orbit: <OrbitIndicator color={color} />,
122
+ }[variant];
123
+
124
+ return (
125
+ <div
126
+ className={cx(tkx('inline-flex flex-col gap-2'), className)}
127
+ style={style}
128
+ role="status"
129
+ aria-live="polite"
130
+ aria-label={active ? label : 'Done thinking'}
131
+ >
132
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
133
+ {/* Indicator */}
134
+ <div style={{ opacity: active ? 1 : 0.35, transition: 'opacity 0.3s' }}>
135
+ {active ? indicator : (
136
+ <div style={{ width: 20, height: 20, borderRadius: '50%', background: '#10b981', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
137
+ <span style={{ color: '#fff', fontSize: 11, fontWeight: 900 }}>✓</span>
138
+ </div>
139
+ )}
140
+ </div>
141
+
142
+ {/* Label / step text */}
143
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
144
+ <span style={{
145
+ fontSize: fs, fontWeight: 600,
146
+ color: active ? theme.text : theme.textMuted,
147
+ transition: 'color 0.3s',
148
+ }}>
149
+ {active ? (steps ? steps[stepIdx] : label) : 'Done'}
150
+ </span>
151
+ {active && (
152
+ <span style={{ fontSize: fs - 2, color: theme.textMuted }}>
153
+ ⚛ Quantum AI · Amplitude Amplification
154
+ </span>
155
+ )}
156
+ </div>
157
+ </div>
158
+
159
+ {/* Step progress dots */}
160
+ {steps && steps.length > 1 && (
161
+ <div style={{ display: 'flex', gap: 4, paddingLeft: 2 }}>
162
+ {steps.map((_, i) => (
163
+ <div key={i} style={{
164
+ width: i === stepIdx ? 16 : 6, height: 4, borderRadius: 4,
165
+ background: i === stepIdx ? color : `${color}44`,
166
+ transition: 'all 0.3s ease',
167
+ }} />
168
+ ))}
169
+ </div>
170
+ )}
171
+
172
+ <style>{`
173
+ @keyframes tkx-thinking-dot {
174
+ 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
175
+ 40% { transform: scale(1); opacity: 1; }
176
+ }
177
+ @keyframes tkx-thinking-pulse {
178
+ 0% { transform: scale(0.6); opacity: 0.8; }
179
+ 100% { transform: scale(2); opacity: 0; }
180
+ }
181
+ @keyframes tkx-thinking-wave {
182
+ 0%, 100% { height: 6px; }
183
+ 50% { height: 18px; }
184
+ }
185
+ @keyframes tkx-thinking-orbit {
186
+ to { transform: rotate(360deg); }
187
+ }
188
+ `}</style>
189
+ </div>
190
+ );
191
+ }
@@ -72,3 +72,8 @@ export * from './TkxList';
72
72
  // TkxQuantumForm, TkxThemeBuilder, TkxPlayground → '@tekivex/ui/quantum'
73
73
  // TkxLiveFeed, TkxLiveMetrics, TkxRealTimeChart, TkxLiveLog → '@tekivex/ui/realtime'
74
74
  export * from './TkxDropdown';
75
+
76
+ // ── AI-native components ──────────────────────────────────────────────────────
77
+ export * from './TkxAIConfidenceBar';
78
+ export * from './TkxAIChatBubble';
79
+ export * from './TkxAIThinking';