tekivex-ui 2.5.9 → 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.
- package/README.md +1 -1
- package/dist/index.cjs +46 -23
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4189 -3806
- package/dist/src/components/TkxAIChatBubble.d.ts +24 -0
- package/dist/src/components/TkxAIChatBubble.d.ts.map +1 -0
- package/dist/src/components/TkxAIConfidenceBar.d.ts +17 -0
- package/dist/src/components/TkxAIConfidenceBar.d.ts.map +1 -0
- package/dist/src/components/TkxAIThinking.d.ts +17 -0
- package/dist/src/components/TkxAIThinking.d.ts.map +1 -0
- package/dist/src/components/index.d.ts +3 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/TkxAIChatBubble.tsx +209 -0
- package/src/components/TkxAIConfidenceBar.tsx +131 -0
- package/src/components/TkxAIThinking.tsx +191 -0
- package/src/components/index.ts +5 -0
|
@@ -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.
|
|
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
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -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';
|