vanilla-agent 0.1.0

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,11 @@
1
+ import { ChatWidgetSessionStatus } from "../session";
2
+
3
+ export const statusCopy: Record<ChatWidgetSessionStatus, string> = {
4
+ idle: "Online",
5
+ connecting: "Connecting…",
6
+ connected: "Streaming…",
7
+ error: "Offline"
8
+ };
9
+
10
+
11
+
@@ -0,0 +1,20 @@
1
+ /**
2
+ * DOM utility functions
3
+ */
4
+ export const createElement = <K extends keyof HTMLElementTagNameMap>(
5
+ tag: K,
6
+ className?: string
7
+ ): HTMLElementTagNameMap[K] => {
8
+ const element = document.createElement(tag);
9
+ if (className) {
10
+ element.className = className;
11
+ }
12
+ return element;
13
+ };
14
+
15
+ export const createFragment = (): DocumentFragment => {
16
+ return document.createDocumentFragment();
17
+ };
18
+
19
+
20
+
@@ -0,0 +1,77 @@
1
+ import { ChatWidgetReasoning, ChatWidgetToolCall } from "../types";
2
+
3
+ export const formatUnknownValue = (value: unknown): string => {
4
+ if (value === null) return "null";
5
+ if (value === undefined) return "";
6
+ if (typeof value === "string") return value;
7
+ if (typeof value === "number" || typeof value === "boolean") {
8
+ return String(value);
9
+ }
10
+ try {
11
+ return JSON.stringify(value, null, 2);
12
+ } catch (error) {
13
+ return String(value);
14
+ }
15
+ };
16
+
17
+ export const formatReasoningDuration = (reasoning: ChatWidgetReasoning) => {
18
+ const end = reasoning.completedAt ?? Date.now();
19
+ const start = reasoning.startedAt ?? end;
20
+ const durationMs =
21
+ reasoning.durationMs !== undefined
22
+ ? reasoning.durationMs
23
+ : Math.max(0, end - start);
24
+ const seconds = durationMs / 1000;
25
+ if (seconds < 0.1) {
26
+ return "Thought for <0.1 seconds";
27
+ }
28
+ const formatted =
29
+ seconds >= 10
30
+ ? Math.round(seconds).toString()
31
+ : seconds.toFixed(1).replace(/\.0$/, "");
32
+ return `Thought for ${formatted} seconds`;
33
+ };
34
+
35
+ export const describeReasonStatus = (reasoning: ChatWidgetReasoning) => {
36
+ if (reasoning.status === "complete") return formatReasoningDuration(reasoning);
37
+ if (reasoning.status === "pending") return "Waiting";
38
+ return "";
39
+ };
40
+
41
+ export const formatToolDuration = (tool: ChatWidgetToolCall) => {
42
+ const durationMs =
43
+ typeof tool.duration === "number"
44
+ ? tool.duration
45
+ : typeof tool.durationMs === "number"
46
+ ? tool.durationMs
47
+ : Math.max(
48
+ 0,
49
+ (tool.completedAt ?? Date.now()) -
50
+ (tool.startedAt ?? tool.completedAt ?? Date.now())
51
+ );
52
+ const seconds = durationMs / 1000;
53
+ if (seconds < 0.1) {
54
+ return "Used tool for <0.1 seconds";
55
+ }
56
+ const formatted =
57
+ seconds >= 10
58
+ ? Math.round(seconds).toString()
59
+ : seconds.toFixed(1).replace(/\.0$/, "");
60
+ return `Used tool for ${formatted} seconds`;
61
+ };
62
+
63
+ export const describeToolStatus = (status: ChatWidgetToolCall["status"]) => {
64
+ if (status === "complete") return "";
65
+ if (status === "pending") return "Starting";
66
+ return "Running";
67
+ };
68
+
69
+ export const describeToolTitle = (tool: ChatWidgetToolCall) => {
70
+ if (tool.status === "complete") {
71
+ return formatToolDuration(tool);
72
+ }
73
+ return "Using tool...";
74
+ };
75
+
76
+
77
+
@@ -0,0 +1,92 @@
1
+ import { icons } from "lucide";
2
+ import type { IconNode } from "lucide";
3
+
4
+ /**
5
+ * Renders a Lucide icon as an inline SVG element
6
+ * This approach requires no CSS and works on any page
7
+ *
8
+ * @param iconName - The Lucide icon name in kebab-case (e.g., "arrow-up")
9
+ * @param size - The size of the icon (default: 24)
10
+ * @param color - The stroke color (default: "currentColor")
11
+ * @param strokeWidth - The stroke width (default: 2)
12
+ * @returns SVGElement or null if icon not found
13
+ */
14
+ export const renderLucideIcon = (
15
+ iconName: string,
16
+ size: number | string = 24,
17
+ color: string = "currentColor",
18
+ strokeWidth: number = 2
19
+ ): SVGElement | null => {
20
+ try {
21
+ // Convert kebab-case to PascalCase (e.g., "arrow-up" -> "ArrowUp")
22
+ const pascalName = iconName
23
+ .split("-")
24
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
25
+ .join("");
26
+
27
+ // Lucide's icons object contains IconNode data directly, not functions
28
+ const iconData = (icons as Record<string, IconNode>)[pascalName] as IconNode;
29
+
30
+ if (!iconData) {
31
+ console.warn(`Lucide icon "${iconName}" not found (tried "${pascalName}"). Available icons: https://lucide.dev/icons`);
32
+ return null;
33
+ }
34
+
35
+ return createSvgFromIconData(iconData, size, color, strokeWidth);
36
+ } catch (error) {
37
+ console.warn(`Failed to render Lucide icon "${iconName}":`, error);
38
+ return null;
39
+ }
40
+ };
41
+
42
+ /**
43
+ * Helper function to create SVG from IconNode data
44
+ */
45
+ function createSvgFromIconData(
46
+ iconData: IconNode,
47
+ size: number | string,
48
+ color: string,
49
+ strokeWidth: number
50
+ ): SVGElement | null {
51
+ if (!iconData || !Array.isArray(iconData)) {
52
+ return null;
53
+ }
54
+
55
+ // Create SVG element
56
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
57
+ svg.setAttribute("width", String(size));
58
+ svg.setAttribute("height", String(size));
59
+ svg.setAttribute("viewBox", "0 0 24 24");
60
+ svg.setAttribute("fill", "none");
61
+ svg.setAttribute("stroke", color);
62
+ svg.setAttribute("stroke-width", String(strokeWidth));
63
+ svg.setAttribute("stroke-linecap", "round");
64
+ svg.setAttribute("stroke-linejoin", "round");
65
+ svg.setAttribute("aria-hidden", "true");
66
+
67
+ // Render elements from icon data
68
+ // IconNode format: [["path", {"d": "..."}], ["rect", {"x": "...", "y": "..."}], ...]
69
+ iconData.forEach((elementData) => {
70
+ if (Array.isArray(elementData) && elementData.length >= 2) {
71
+ const tagName = elementData[0] as string;
72
+ const attrs = elementData[1] as Record<string, string>;
73
+
74
+ if (attrs) {
75
+ // Create the appropriate SVG element (path, rect, circle, ellipse, line, etc.)
76
+ const element = document.createElementNS("http://www.w3.org/2000/svg", tagName);
77
+
78
+ // Apply all attributes, but skip 'stroke' (we want to use the parent SVG's stroke for consistent coloring)
79
+ Object.entries(attrs).forEach(([key, value]) => {
80
+ if (key !== "stroke") {
81
+ element.setAttribute(key, String(value));
82
+ }
83
+ });
84
+
85
+ svg.appendChild(element);
86
+ }
87
+ }
88
+ });
89
+
90
+ return svg;
91
+ }
92
+
@@ -0,0 +1,12 @@
1
+ export const positionMap: Record<
2
+ "bottom-right" | "bottom-left" | "top-right" | "top-left",
3
+ string
4
+ > = {
5
+ "bottom-right": "tvw-bottom-6 tvw-right-6",
6
+ "bottom-left": "tvw-bottom-6 tvw-left-6",
7
+ "top-right": "tvw-top-6 tvw-right-6",
8
+ "top-left": "tvw-top-6 tvw-left-6"
9
+ };
10
+
11
+
12
+
@@ -0,0 +1,20 @@
1
+ import { ChatWidgetConfig } from "../types";
2
+
3
+ export const applyThemeVariables = (
4
+ element: HTMLElement,
5
+ config?: ChatWidgetConfig
6
+ ) => {
7
+ const theme = config?.theme ?? {};
8
+ Object.entries(theme).forEach(([key, value]) => {
9
+ // Skip undefined or empty values
10
+ if (value === undefined || value === null || value === "") {
11
+ return;
12
+ }
13
+ // Convert camelCase to kebab-case (e.g., radiusSm → radius-sm)
14
+ const kebabKey = key.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
15
+ element.style.setProperty(`--cw-${kebabKey}`, String(value));
16
+ });
17
+ };
18
+
19
+
20
+
package/src/widget.css ADDED
@@ -0,0 +1 @@
1
+ @import "./styles/widget.css";
package/widget.css ADDED
@@ -0,0 +1 @@
1
+ @import "./src/widget.css";