inconvo 1.3.0 → 1.5.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.
package/README.md CHANGED
@@ -20,6 +20,8 @@ Options:
20
20
 
21
21
  By default the installer writes everything under `src/`, including the tool components and their supporting `src/lib/inconvo/types.ts`. It also installs external dependencies such as `recharts` and `@tanstack/react-table` unless you opt out with `--skip-install`.
22
22
 
23
+ After the files are copied, the CLI attempts to update `src/components/assistant-ui/thread.tsx` by importing `InconvoTools` and rendering it inside `ThreadPrimitive.Root`. If the file is missing or has been customized heavily, you'll see a warning and can complete the insertion manually.
24
+
23
25
  ## Development
24
26
 
25
27
  ```bash
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
4
4
  import { logger } from "../lib/logger.js";
5
5
  import { confirmPrompt } from "../lib/prompt.js";
6
6
  import { installDependencies } from "../lib/install-dependencies.js";
7
+ import { ensureThreadHasTools } from "../lib/thread-injector.js";
7
8
  import { componentPacks, getPack } from "../templates/packs.js";
8
9
  export const addCommand = new Command()
9
10
  .name("add")
@@ -70,6 +71,7 @@ export const addCommand = new Command()
70
71
  if (replaced.size) {
71
72
  logger.info(`Overwrote ${replaced.size} file(s).`);
72
73
  }
74
+ await ensureThreadHasTools(cwd);
73
75
  if (pack.dependencies) {
74
76
  const depList = Object.entries(pack.dependencies).map(([name, version]) => `${name}@${version}`);
75
77
  if (!depList.length)
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { logger } from "./logger.js";
4
+ const TOOLS_IMPORT = `import { InconvoTools } from "./tools/inconvo-tools";`;
5
+ const THREAD_PATH = ["src", "components", "assistant-ui", "thread.tsx"];
6
+ export async function ensureThreadHasTools(projectRoot) {
7
+ const threadPath = path.join(projectRoot, ...THREAD_PATH);
8
+ if (!(await fileExists(threadPath))) {
9
+ logger.warn(`Could not find ${path.relative(projectRoot, threadPath)}. Skipping thread update.`);
10
+ return;
11
+ }
12
+ let content = await fs.readFile(threadPath, "utf8");
13
+ let updated = false;
14
+ if (!hasToolsImport(content)) {
15
+ content = injectImport(content);
16
+ updated = true;
17
+ }
18
+ if (!content.includes("<InconvoTools")) {
19
+ const nextContent = injectComponentUsage(content);
20
+ if (nextContent) {
21
+ content = nextContent;
22
+ updated = true;
23
+ }
24
+ else {
25
+ logger.warn("Could not locate </ThreadPrimitive.Root> in thread.tsx. Please insert <InconvoTools /> manually.");
26
+ }
27
+ }
28
+ if (updated) {
29
+ await fs.writeFile(threadPath, content, "utf8");
30
+ logger.success("Updated thread component to render <InconvoTools />.");
31
+ }
32
+ else {
33
+ logger.info("Thread component already renders <InconvoTools />.");
34
+ }
35
+ }
36
+ function hasToolsImport(content) {
37
+ const namedImportRegex = /import\s+{[^}]*InconvoTools[^}]*}\s+from\s+["'][^"']+["']/;
38
+ const defaultImportRegex = /import\s+InconvoTools\s+from\s+["'][^"']+["']/;
39
+ return (namedImportRegex.test(content) || defaultImportRegex.test(content));
40
+ }
41
+ function injectImport(content) {
42
+ const lines = content.split("\n");
43
+ let lastImportIndex = -1;
44
+ for (let i = 0; i < lines.length; i++) {
45
+ if (lines[i].startsWith("import ")) {
46
+ lastImportIndex = i;
47
+ }
48
+ }
49
+ if (lastImportIndex >= 0) {
50
+ lines.splice(lastImportIndex + 1, 0, TOOLS_IMPORT);
51
+ }
52
+ else {
53
+ lines.unshift(TOOLS_IMPORT);
54
+ }
55
+ return lines.join("\n");
56
+ }
57
+ function injectComponentUsage(content) {
58
+ const lines = content.split("\n");
59
+ const rootIndex = lines.findIndex((line) => line.includes("<ThreadPrimitive.Root"));
60
+ if (rootIndex === -1) {
61
+ return null;
62
+ }
63
+ let insertIndex = rootIndex + 1;
64
+ for (let i = rootIndex; i < lines.length; i++) {
65
+ if (lines[i].includes(">")) {
66
+ insertIndex = i + 1;
67
+ break;
68
+ }
69
+ }
70
+ const baseIndent = lines[rootIndex].match(/^\s*/)?.[0] ?? "";
71
+ const componentLine = `${baseIndent} <InconvoTools />`;
72
+ lines.splice(insertIndex, 0, componentLine);
73
+ return lines.join("\n");
74
+ }
75
+ async function fileExists(target) {
76
+ try {
77
+ await fs.stat(target);
78
+ return true;
79
+ }
80
+ catch (error) {
81
+ if (error && error.code === "ENOENT") {
82
+ return false;
83
+ }
84
+ throw error;
85
+ }
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inconvo",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "CLI for installing Inconvo assistant-ui tool components into any project.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,188 +1,75 @@
1
1
  "use client";
2
2
 
3
- import { useMemo, type ReactNode } from "react";
4
- import {
5
- ResponsiveContainer,
6
- LineChart as RechartsLineChart,
7
- Line,
8
- BarChart as RechartsBarChart,
9
- Bar,
10
- CartesianGrid,
11
- Tooltip,
12
- XAxis,
13
- YAxis,
14
- Label,
15
- Legend,
16
- } from "recharts";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { VegaEmbed } from "react-vega";
5
+ import type { VisualizationSpec } from "vega-embed";
17
6
 
18
- import type { InconvoChartData, InconvoChartType } from "~/lib/inconvo/types";
19
- import { buildChartPalette } from "~/components/assistant-ui/tools/inconvo-chart-colors";
7
+ import type {
8
+ InconvoChartData,
9
+ InconvoChartType,
10
+ InconvoChartSpec,
11
+ } from "~/lib/inconvo/types";
20
12
 
21
13
  interface InconvoChartProps {
22
- data: InconvoChartData;
23
- variant: InconvoChartType;
14
+ data?: InconvoChartData;
15
+ spec?: InconvoChartSpec;
16
+ variant?: InconvoChartType;
24
17
  xLabel?: string;
25
18
  yLabel?: string;
26
19
  title?: string;
27
20
  }
28
21
 
29
- const ChartScaffold = ({
30
- children,
31
- axisColor,
32
- textColor,
33
- xLabel,
34
- yLabel,
35
- labelCount,
36
- }: {
37
- children: ReactNode;
38
- axisColor: string;
39
- textColor: string;
40
- xLabel?: string;
41
- yLabel?: string;
42
- labelCount: number;
43
- }) => (
44
- <>
45
- <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
46
- <XAxis
47
- dataKey="name"
48
- stroke={axisColor}
49
- tick={{ fill: axisColor, fontSize: 12 }}
50
- angle={-30}
51
- textAnchor="end"
52
- interval={labelCount > 12 ? "preserveStartEnd" : 0}
53
- >
54
- {xLabel ? (
55
- <Label
56
- position="bottom"
57
- offset={24}
58
- style={{
59
- fill: axisColor,
60
- textAnchor: "middle",
61
- }}
62
- value={xLabel}
63
- />
64
- ) : null}
65
- </XAxis>
66
- <YAxis width={80} stroke={axisColor} tick={{ fill: axisColor, fontSize: 12 }}>
67
- {yLabel ? (
68
- <Label
69
- angle={-90}
70
- position="insideLeft"
71
- style={{
72
- fill: axisColor,
73
- textAnchor: "middle",
74
- }}
75
- value={yLabel}
76
- />
77
- ) : null}
78
- </YAxis>
79
- <Tooltip
80
- contentStyle={{
81
- backgroundColor: "var(--card)",
82
- border: "1px solid var(--border)",
83
- borderRadius: 8,
84
- color: textColor,
85
- }}
86
- labelStyle={{
87
- color: textColor,
88
- fontWeight: 600,
89
- }}
90
- />
91
- <Legend
92
- verticalAlign="top"
93
- align="right"
94
- wrapperStyle={{
95
- color: axisColor,
96
- paddingBottom: "4px",
97
- }}
98
- />
99
- {children}
100
- </>
101
- );
22
+ export const InconvoChart = ({ spec: providedSpec }: InconvoChartProps) => {
23
+ const [error, setError] = useState<string | null>(null);
102
24
 
103
- export const InconvoChart = ({
104
- data,
105
- variant,
106
- xLabel,
107
- yLabel,
108
- }: InconvoChartProps) => {
109
- const chartData = useMemo(() => {
110
- return data.labels.map((label, index) => {
111
- const row: { name: string; [key: string]: string | number } = {
112
- name: label,
113
- };
114
- data.datasets.forEach((dataset) => {
115
- row[dataset.name] = dataset.values[index] ?? 0;
116
- });
117
- return row;
118
- });
119
- }, [data]);
25
+ const resolvedSpec = useMemo<VisualizationSpec | null>(() => {
26
+ if (providedSpec) {
27
+ return {
28
+ $schema: "https://vega.github.io/schema/vega-lite/v5.json",
29
+ background: "transparent",
30
+ autosize: { type: "fit", contains: "padding" },
31
+ width: "container",
32
+ ...providedSpec,
33
+ } as VisualizationSpec;
34
+ }
120
35
 
121
- const palette = useMemo(
122
- () => buildChartPalette(data.datasets.length),
123
- [data.datasets.length],
124
- );
36
+ return null;
37
+ }, [providedSpec]);
125
38
 
126
- const axisColor = "var(--muted-foreground)";
127
- const textColor = "var(--foreground)";
128
- const margins = { top: 20, right: 30, bottom: xLabel ? 80 : 40, left: 20 };
39
+ useEffect(() => {
40
+ setError(null);
41
+ }, [resolvedSpec]);
129
42
 
130
- const renderLines = () =>
131
- data.datasets.map((dataset, index) => {
132
- const stroke = palette[index] ?? "var(--chart-series-primary)";
133
- return (
134
- <Line
135
- key={dataset.name}
136
- type="monotone"
137
- dataKey={dataset.name}
138
- stroke={stroke}
139
- strokeWidth={2}
140
- dot={{ r: 3, strokeWidth: 2, stroke, fill: "var(--card)" }}
141
- activeDot={{ r: 5, strokeWidth: 2, stroke, fill: stroke }}
142
- />
143
- );
144
- });
43
+ const handleError = (err: unknown) => {
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ console.error("Vega-Lite render error:", err);
46
+ setError(message);
47
+ };
145
48
 
146
- const renderBars = () =>
147
- data.datasets.map((dataset, index) => (
148
- <Bar
149
- key={dataset.name}
150
- dataKey={dataset.name}
151
- fill={palette[index] ?? "var(--chart-series-primary)"}
152
- radius={[4, 4, 0, 0]}
153
- maxBarSize={48}
154
- />
155
- ));
49
+ if (!resolvedSpec) {
50
+ return (
51
+ <div className="text-sm text-muted-foreground">
52
+ No chart data provided.
53
+ </div>
54
+ );
55
+ }
56
+
57
+ if (error) {
58
+ return (
59
+ <div className="text-sm text-red-500">
60
+ Failed to render chart: {error}
61
+ </div>
62
+ );
63
+ }
156
64
 
157
65
  return (
158
66
  <div className="flex w-full flex-col gap-4 text-foreground">
159
- <ResponsiveContainer width="100%" height={400}>
160
- {variant === "line" ? (
161
- <RechartsLineChart data={chartData} margin={margins}>
162
- <ChartScaffold
163
- axisColor={axisColor}
164
- textColor={textColor}
165
- xLabel={xLabel}
166
- yLabel={yLabel}
167
- labelCount={data.labels.length}
168
- >
169
- {renderLines()}
170
- </ChartScaffold>
171
- </RechartsLineChart>
172
- ) : (
173
- <RechartsBarChart data={chartData} margin={margins}>
174
- <ChartScaffold
175
- axisColor={axisColor}
176
- textColor={textColor}
177
- xLabel={xLabel}
178
- yLabel={yLabel}
179
- labelCount={data.labels.length}
180
- >
181
- {renderBars()}
182
- </ChartScaffold>
183
- </RechartsBarChart>
184
- )}
185
- </ResponsiveContainer>
67
+ <VegaEmbed
68
+ spec={resolvedSpec}
69
+ options={{ actions: false }}
70
+ onError={handleError}
71
+ style={{ width: "100%" }}
72
+ />
186
73
  </div>
187
74
  );
188
75
  };
@@ -1,15 +0,0 @@
1
- const chartColorVars = [
2
- "var(--chart-1)",
3
- "var(--chart-2)",
4
- "var(--chart-3)",
5
- "var(--chart-4)",
6
- "var(--chart-5)",
7
- ];
8
-
9
- export const buildChartPalette = (seriesCount: number) => {
10
- if (seriesCount <= 0) return [];
11
- return Array.from({ length: seriesCount }, (_, index) => {
12
- const colorIndex = index % chartColorVars.length;
13
- return chartColorVars[colorIndex] ?? "var(--chart-series-primary)";
14
- });
15
- };