@rebasepro/plugin-ai 0.12.1-canary.gf5f1d39 → 0.13.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 +62 -18
- package/dist/api.d.ts +58 -30
- package/dist/components/AutofillReviewDialog.d.ts +16 -0
- package/dist/components/DataEnhancementControllerProvider.d.ts +2 -3
- package/dist/components/FormEnhanceAction.d.ts +1 -1
- package/dist/editor/useEditorAIController.d.ts +11 -2
- package/dist/index.es.js +601 -382
- package/dist/index.es.js.map +1 -1
- package/dist/types/data_enhancement_controller.d.ts +84 -28
- package/dist/useDataEnhancementPlugin.d.ts +10 -5
- package/package.json +24 -19
- package/src/api.ts +241 -174
- package/src/components/AutofillReviewDialog.tsx +209 -0
- package/src/components/DataEnhancementControllerProvider.tsx +203 -262
- package/src/components/FormEnhanceAction.tsx +154 -128
- package/src/editor/useEditorAIController.tsx +20 -33
- package/src/tests/AutofillReviewDialog.test.tsx +340 -0
- package/src/tests/api.test.ts +283 -0
- package/src/tests/properties.test.ts +420 -0
- package/src/tests/review.test.tsx +393 -0
- package/src/tests/useDataEnhancementPlugin.test.tsx +36 -15
- package/src/tests/useEditorAIController.test.ts +98 -0
- package/src/types/data_enhancement_controller.tsx +98 -31
- package/src/useDataEnhancementPlugin.tsx +13 -12
- package/dist/utils/diffStrings.d.ts +0 -7
- package/dist/utils/strings_counter.d.ts +0 -2
- package/dist/utils/suggestions.d.ts +0 -1
- package/src/tests/diffStrings.test.ts +0 -128
- package/src/tests/strings_counter.test.ts +0 -117
- package/src/tests/suggestions.test.ts +0 -53
- package/src/utils/diffStrings.ts +0 -70
- package/src/utils/strings_counter.ts +0 -22
- package/src/utils/suggestions.ts +0 -6
|
@@ -1,64 +1,131 @@
|
|
|
1
1
|
import { EntityValues } from "@rebasepro/types";
|
|
2
2
|
import { EditorAIController } from "@rebasepro/admin";
|
|
3
3
|
|
|
4
|
-
export type
|
|
5
|
-
|
|
4
|
+
export type GenerateParams<M extends Record<string, unknown>> = {
|
|
5
|
+
values: EntityValues<M>;
|
|
6
|
+
/** Free-text instruction from the operator, if they gave one. */
|
|
7
|
+
instructions?: string;
|
|
8
|
+
/** Restrict the run to one field. */
|
|
6
9
|
propertyKey?: string;
|
|
7
10
|
propertyInstructions?: string;
|
|
8
|
-
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One field the model wants to write, awaiting the operator's decision.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here has touched the form. That is the entire point of the type: the
|
|
17
|
+
* previous design streamed generated text straight into the live fields, which
|
|
18
|
+
* meant a half-written sentence was indistinguishable from a bug, the
|
|
19
|
+
* operator's own words were overwritten by heuristics that tried to guess
|
|
20
|
+
* whether to append or replace, and there was no way back other than retyping.
|
|
21
|
+
*/
|
|
22
|
+
export type ProposedField = {
|
|
23
|
+
/** Dotted property path, e.g. `seo.title`. */
|
|
24
|
+
key: string;
|
|
25
|
+
/** The property's display name, falling back to its key. */
|
|
26
|
+
label: string;
|
|
27
|
+
/** What is in the form right now — shown so an overwrite is visible. */
|
|
28
|
+
currentValue: unknown;
|
|
29
|
+
/** What the model proposes. Grows while `pending`. */
|
|
30
|
+
proposed: unknown;
|
|
31
|
+
/** Still streaming. */
|
|
32
|
+
pending: boolean;
|
|
33
|
+
/** Whether Apply will write this one. */
|
|
34
|
+
selected: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type AutofillReview = {
|
|
38
|
+
status: "generating" | "ready" | "failed";
|
|
39
|
+
/** Set when `status` is `failed`. */
|
|
40
|
+
error?: string;
|
|
41
|
+
/** In arrival order, so the list reads as the model works. */
|
|
42
|
+
fields: ProposedField[];
|
|
43
|
+
/** What was asked for, shown back to the operator while they review. */
|
|
9
44
|
instructions?: string;
|
|
10
|
-
replaceValues: boolean;
|
|
11
45
|
};
|
|
12
46
|
|
|
13
47
|
export type DataEnhancementController = {
|
|
14
48
|
/**
|
|
15
|
-
* Whether
|
|
49
|
+
* Whether autofill can actually be used right now.
|
|
50
|
+
*
|
|
51
|
+
* The conjunction of two separate things: the host app allows it for this
|
|
52
|
+
* collection ({@link DataEnhancementPluginProps.getConfigForPath}), *and*
|
|
53
|
+
* the service reported itself available. The second half is what the
|
|
54
|
+
* FireCMS-era plugin lacked — it rendered its button unconditionally
|
|
55
|
+
* against a host that no longer existed, so every click 404'd.
|
|
16
56
|
*/
|
|
17
57
|
enabled: boolean;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
58
|
+
|
|
59
|
+
/** The run in flight or awaiting review; `null` when there is neither. */
|
|
60
|
+
review: AutofillReview | null;
|
|
61
|
+
|
|
62
|
+
/** Start a run. Opens {@link review}; never writes to the form. */
|
|
63
|
+
generate: <M extends Record<string, unknown>>(params: GenerateParams<M>) => Promise<void>;
|
|
64
|
+
|
|
65
|
+
/** Include or exclude one field from what Apply will write. */
|
|
66
|
+
toggleField: (key: string) => void;
|
|
67
|
+
|
|
68
|
+
/** Select or deselect every field at once. */
|
|
69
|
+
toggleAll: (selected: boolean) => void;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Write the selected fields to the form and close the review.
|
|
73
|
+
*
|
|
74
|
+
* The only path by which this plugin mutates a record, and it runs once per
|
|
75
|
+
* run rather than once per token — so it is a single undo step and a single
|
|
76
|
+
* dirty transition, not hundreds.
|
|
77
|
+
*/
|
|
78
|
+
applyReview: () => void;
|
|
79
|
+
|
|
80
|
+
/** Close the review, writing nothing. The record is untouched. */
|
|
81
|
+
dismissReview: () => void;
|
|
82
|
+
|
|
23
83
|
getSamplePrompts: (entityName: string, input?: string) => Promise<SamplePromptsResult>;
|
|
24
|
-
|
|
84
|
+
|
|
25
85
|
editorAIController?: EditorAIController;
|
|
26
|
-
}
|
|
86
|
+
};
|
|
27
87
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
88
|
+
/** What `GET /status` answers. Everything else is gated on `available`. */
|
|
89
|
+
export type AiStatus = {
|
|
90
|
+
available: boolean;
|
|
91
|
+
model?: string;
|
|
92
|
+
features?: string[];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type AutofillResult = {
|
|
96
|
+
/** Every field the service completed, keyed by property path. */
|
|
97
|
+
suggestions: Record<string, unknown>;
|
|
98
|
+
usage?: { inputTokens?: number; outputTokens?: number };
|
|
99
|
+
};
|
|
36
100
|
|
|
37
101
|
export type SamplePrompt = {
|
|
38
102
|
prompt: string;
|
|
39
103
|
type: "recent" | "sample";
|
|
40
|
-
}
|
|
104
|
+
};
|
|
41
105
|
|
|
42
106
|
export type SamplePromptsResult = {
|
|
43
107
|
prompts: SamplePrompt[];
|
|
44
|
-
host?: string;
|
|
45
108
|
};
|
|
46
109
|
|
|
47
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The autofill request body.
|
|
112
|
+
*
|
|
113
|
+
* The property schema travels with every request because the service has no
|
|
114
|
+
* access to the caller's collections — it is a hosted endpoint reachable from
|
|
115
|
+
* any self-hosted admin panel. That is the cost of not putting an LLM
|
|
116
|
+
* dependency in `@rebasepro/server`; had the route lived in the backend it
|
|
117
|
+
* could have read the collection registry and this would be three fields.
|
|
118
|
+
*/
|
|
119
|
+
export type AutofillRequest = {
|
|
48
120
|
entityName: string;
|
|
49
121
|
entityDescription?: string;
|
|
50
|
-
|
|
122
|
+
values: Record<string, unknown>;
|
|
51
123
|
properties: Record<string, InputProperty>;
|
|
52
124
|
propertyKey?: string;
|
|
53
|
-
propertyInstructions?: string
|
|
125
|
+
propertyInstructions?: string;
|
|
54
126
|
instructions?: string;
|
|
55
127
|
};
|
|
56
128
|
|
|
57
|
-
export type InputEntity = {
|
|
58
|
-
entityId?: string | number;
|
|
59
|
-
values: Record<string, any>;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
129
|
export type InputProperty = {
|
|
63
130
|
name?: string;
|
|
64
131
|
description?: string;
|
|
@@ -72,4 +139,4 @@ export type InputProperty = {
|
|
|
72
139
|
typeField?: string;
|
|
73
140
|
valueField?: string;
|
|
74
141
|
};
|
|
75
|
-
}
|
|
142
|
+
};
|
|
@@ -5,17 +5,12 @@ import { RebasePlugin } from "@rebasepro/admin-types";
|
|
|
5
5
|
import { DataEnhancementControllerProvider } from "./components/DataEnhancementControllerProvider";
|
|
6
6
|
import { FormEnhanceAction } from "./components/FormEnhanceAction";
|
|
7
7
|
|
|
8
|
-
const DEFAULT_API_KEY = "fcms-U9jdDii0xXWSDC34asfrf54lbkFJBfKfRWcEDEwdc4V5wDWEDF";
|
|
9
|
-
|
|
10
8
|
export interface DataEnhancementPluginProps {
|
|
11
9
|
|
|
12
|
-
apiKey?: string;
|
|
13
|
-
|
|
14
10
|
/**
|
|
15
11
|
* Use this function to determine if the data enhancement plugin should be enabled for a given path.
|
|
16
12
|
* If this function is not provided, the plugin will be enabled for all paths.
|
|
17
13
|
* If the function returns false, the plugin will be disabled for the given path.
|
|
18
|
-
* You can also return a configuration object to override the default configuration.
|
|
19
14
|
*
|
|
20
15
|
* @param path
|
|
21
16
|
* @param collection
|
|
@@ -27,10 +22,17 @@ export interface DataEnhancementPluginProps {
|
|
|
27
22
|
}) => boolean;
|
|
28
23
|
|
|
29
24
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
25
|
+
* Base URL of the AI service.
|
|
26
|
+
*
|
|
27
|
+
* Defaults to the one Rebase hosts, which is free to use and needs no
|
|
28
|
+
* configuration. Point it at your own deployment to keep generation inside
|
|
29
|
+
* your infrastructure — the wire format is documented in `src/api.ts`, and
|
|
30
|
+
* the reference implementation is `saas/backend/functions/ai.ts`.
|
|
31
|
+
*
|
|
32
|
+
* Whatever it points at, the plugin renders nothing until that host's
|
|
33
|
+
* `GET /status` reports itself available.
|
|
32
34
|
*/
|
|
33
|
-
|
|
35
|
+
endpoint?: string;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -40,8 +42,8 @@ export interface DataEnhancementPluginProps {
|
|
|
40
42
|
*/
|
|
41
43
|
export function useDataEnhancementPlugin(props?: DataEnhancementPluginProps): RebasePlugin {
|
|
42
44
|
|
|
43
|
-
const apiKey = props?.apiKey ?? DEFAULT_API_KEY;
|
|
44
45
|
const getConfigForPath = props?.getConfigForPath;
|
|
46
|
+
const endpoint = props?.endpoint;
|
|
45
47
|
|
|
46
48
|
return React.useMemo(() => ({
|
|
47
49
|
key: "data_enhancement",
|
|
@@ -57,11 +59,10 @@ export function useDataEnhancementPlugin(props?: DataEnhancementPluginProps): Re
|
|
|
57
59
|
scope: "form" as const,
|
|
58
60
|
Component: DataEnhancementControllerProvider as React.ComponentType<any>,
|
|
59
61
|
props: {
|
|
60
|
-
apiKey,
|
|
61
62
|
getConfigForPath,
|
|
62
|
-
|
|
63
|
+
endpoint
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
]
|
|
66
|
-
}), [
|
|
67
|
+
}), [getConfigForPath, endpoint]);
|
|
67
68
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getAppendableSuggestion(suggestion: string | number | undefined, value: unknown): string | undefined;
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// Import the diffStrings function from your module
|
|
2
|
-
// import { diffStrings } from './your-module';
|
|
3
|
-
|
|
4
|
-
import { Change, diffStrings } from "../utils/diffStrings";
|
|
5
|
-
|
|
6
|
-
describe("diffStrings", () => {
|
|
7
|
-
test("equal strings", () => {
|
|
8
|
-
const oldStr = "This is a test string";
|
|
9
|
-
const newStr = "This is a test string";
|
|
10
|
-
const expected: Change[] = [
|
|
11
|
-
{
|
|
12
|
-
type: "equal",
|
|
13
|
-
value: "This is a test string"
|
|
14
|
-
}
|
|
15
|
-
];
|
|
16
|
-
expect(diffStrings(oldStr, newStr)).toEqual(expected);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("insertions only", () => {
|
|
20
|
-
const oldStr = "This is a test string";
|
|
21
|
-
const newStr = "This is a new test string";
|
|
22
|
-
const expected: Change[] = [
|
|
23
|
-
{
|
|
24
|
-
type: "equal",
|
|
25
|
-
value: "This is a"
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
type: "insert",
|
|
29
|
-
value: " new"
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
type: "equal",
|
|
33
|
-
value: " test string"
|
|
34
|
-
}
|
|
35
|
-
];
|
|
36
|
-
expect(diffStrings(oldStr, newStr)).toEqual(expected);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("deletions only", () => {
|
|
40
|
-
const oldStr = "This is an old test string";
|
|
41
|
-
const newStr = "This is a test string";
|
|
42
|
-
const expected: Change[] = [
|
|
43
|
-
{
|
|
44
|
-
type: "equal",
|
|
45
|
-
value: "This is a"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: "delete",
|
|
49
|
-
value: "n old"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
type: "equal",
|
|
53
|
-
value: " test string"
|
|
54
|
-
}
|
|
55
|
-
];
|
|
56
|
-
expect(diffStrings(oldStr, newStr)).toEqual(expected);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("insertions and deletions", () => {
|
|
60
|
-
const oldStr = "This is an old test string";
|
|
61
|
-
const newStr = "This is a new modified test string";
|
|
62
|
-
// The LCS algorithm finds the longest common substrings correctly
|
|
63
|
-
// Even though the output is more granular, it correctly represents the diff
|
|
64
|
-
const expected: Change[] = [
|
|
65
|
-
{
|
|
66
|
-
type: "equal",
|
|
67
|
-
value: "This is a"
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
type: "insert",
|
|
71
|
-
value: " "
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
type: "equal",
|
|
75
|
-
value: "n"
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
type: "insert",
|
|
79
|
-
value: "ew"
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
type: "equal",
|
|
83
|
-
value: " "
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
type: "insert",
|
|
87
|
-
value: "m"
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
type: "equal",
|
|
91
|
-
value: "o"
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
type: "delete",
|
|
95
|
-
value: "l"
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
type: "insert",
|
|
99
|
-
value: "difie"
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
type: "equal",
|
|
103
|
-
value: "d test string"
|
|
104
|
-
}
|
|
105
|
-
];
|
|
106
|
-
expect(diffStrings(oldStr, newStr)).toEqual(expected);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("completely different strings", () => {
|
|
110
|
-
const oldStr = "Old string";
|
|
111
|
-
const newStr = "New string";
|
|
112
|
-
const expected: Change[] = [
|
|
113
|
-
{
|
|
114
|
-
type: "delete",
|
|
115
|
-
value: "Old"
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
type: "insert",
|
|
119
|
-
value: "New"
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
type: "equal",
|
|
123
|
-
value: " string"
|
|
124
|
-
}
|
|
125
|
-
];
|
|
126
|
-
expect(diffStrings(oldStr, newStr)).toEqual(expected);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { countStringCharacters } from "../utils/strings_counter";
|
|
2
|
-
import type { Properties } from "@rebasepro/types";
|
|
3
|
-
|
|
4
|
-
describe("countStringCharacters", () => {
|
|
5
|
-
it("counts characters in a string property", () => {
|
|
6
|
-
const values = { title: "Hello World" };
|
|
7
|
-
const properties: Properties = {
|
|
8
|
-
title: { name: "title",
|
|
9
|
-
type: "string" }
|
|
10
|
-
};
|
|
11
|
-
expect(countStringCharacters(values, properties)).toBe(11);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("counts characters in a number property (stringified)", () => {
|
|
15
|
-
const values = { price: 12345 };
|
|
16
|
-
const properties: Properties = {
|
|
17
|
-
price: { name: "price",
|
|
18
|
-
type: "number" }
|
|
19
|
-
};
|
|
20
|
-
expect(countStringCharacters(values, properties)).toBe(5);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("skips disabled properties", () => {
|
|
24
|
-
const values = { title: "Hello",
|
|
25
|
-
hidden: "Secret" };
|
|
26
|
-
const properties: Properties = {
|
|
27
|
-
title: { name: "title",
|
|
28
|
-
type: "string" },
|
|
29
|
-
hidden: { name: "hidden",
|
|
30
|
-
type: "string",
|
|
31
|
-
admin: { disabled: true } }
|
|
32
|
-
};
|
|
33
|
-
expect(countStringCharacters(values, properties)).toBe(5);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("counts array of strings", () => {
|
|
37
|
-
const values = { tags: ["hello", "world", "test"] };
|
|
38
|
-
const properties: Properties = {
|
|
39
|
-
tags: {
|
|
40
|
-
name: "tags",
|
|
41
|
-
type: "array",
|
|
42
|
-
of: { name: "tag",
|
|
43
|
-
type: "string" }
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
// "hello" (5) + "world" (5) + "test" (4) = 14
|
|
47
|
-
expect(countStringCharacters(values, properties)).toBe(14);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("recurses into map properties", () => {
|
|
51
|
-
const values = {
|
|
52
|
-
address: { city: "New York",
|
|
53
|
-
country: "USA" }
|
|
54
|
-
};
|
|
55
|
-
const properties: Properties = {
|
|
56
|
-
address: {
|
|
57
|
-
name: "address",
|
|
58
|
-
type: "map",
|
|
59
|
-
properties: {
|
|
60
|
-
city: { name: "city",
|
|
61
|
-
type: "string" },
|
|
62
|
-
country: { name: "country",
|
|
63
|
-
type: "string" }
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
// "New York" (8) + "USA" (3) = 11
|
|
68
|
-
expect(countStringCharacters(values, properties)).toBe(11);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("returns 0 for empty values", () => {
|
|
72
|
-
const properties: Properties = {
|
|
73
|
-
title: { name: "title",
|
|
74
|
-
type: "string" }
|
|
75
|
-
};
|
|
76
|
-
expect(countStringCharacters({}, properties)).toBe(0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("handles mixed property types", () => {
|
|
80
|
-
const values = {
|
|
81
|
-
title: "Hello",
|
|
82
|
-
count: 42,
|
|
83
|
-
active: true,
|
|
84
|
-
tags: ["a", "bb"]
|
|
85
|
-
};
|
|
86
|
-
const properties: Properties = {
|
|
87
|
-
title: { name: "title",
|
|
88
|
-
type: "string" },
|
|
89
|
-
count: { name: "count",
|
|
90
|
-
type: "number" },
|
|
91
|
-
active: { name: "active",
|
|
92
|
-
type: "boolean" },
|
|
93
|
-
tags: {
|
|
94
|
-
name: "tags",
|
|
95
|
-
type: "array",
|
|
96
|
-
of: { name: "tag",
|
|
97
|
-
type: "string" }
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
// "Hello" (5) + "42" (2) + "a" (1) + "bb" (2) = 10
|
|
101
|
-
expect(countStringCharacters(values, properties)).toBe(10);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("handles null values in array gracefully", () => {
|
|
105
|
-
const values = { tags: [null, "hello", null] };
|
|
106
|
-
const properties: Properties = {
|
|
107
|
-
tags: {
|
|
108
|
-
name: "tags",
|
|
109
|
-
type: "array",
|
|
110
|
-
of: { name: "tag",
|
|
111
|
-
type: "string" }
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
// null?.length = 0, "hello" = 5, null?.length = 0
|
|
115
|
-
expect(countStringCharacters(values, properties)).toBe(5);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { getAppendableSuggestion } from "../utils/suggestions";
|
|
2
|
-
|
|
3
|
-
describe("getAppendableSuggestion", () => {
|
|
4
|
-
it("returns the remaining part when suggestion starts with value", () => {
|
|
5
|
-
const result = getAppendableSuggestion("Hello World", "Hello");
|
|
6
|
-
expect(result).toBe(" World");
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("returns undefined when suggestion does not start with value", () => {
|
|
10
|
-
const result = getAppendableSuggestion("Goodbye World", "Hello");
|
|
11
|
-
expect(result).toBeUndefined();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("performs case-insensitive matching", () => {
|
|
15
|
-
const result = getAppendableSuggestion("Hello World", "hello");
|
|
16
|
-
expect(result).toBe(" World");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("returns undefined for number suggestion", () => {
|
|
20
|
-
const result = getAppendableSuggestion(42, "4");
|
|
21
|
-
expect(result).toBeUndefined();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("returns undefined for undefined suggestion", () => {
|
|
25
|
-
const result = getAppendableSuggestion(undefined, "hello");
|
|
26
|
-
expect(result).toBeUndefined();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("returns undefined for non-string value", () => {
|
|
30
|
-
const result = getAppendableSuggestion("Hello", 42);
|
|
31
|
-
expect(result).toBeUndefined();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("returns full suggestion when value is empty string", () => {
|
|
35
|
-
const result = getAppendableSuggestion("Hello World", "");
|
|
36
|
-
expect(result).toBe("Hello World");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("handles whitespace trimming", () => {
|
|
40
|
-
const result = getAppendableSuggestion("Hello World", "Hello");
|
|
41
|
-
expect(result).toBe(" World");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("returns empty string when suggestion equals value", () => {
|
|
45
|
-
const result = getAppendableSuggestion("Hello", "Hello");
|
|
46
|
-
expect(result).toBe("");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("returns undefined when value is longer than suggestion", () => {
|
|
50
|
-
const result = getAppendableSuggestion("Hi", "Hello World");
|
|
51
|
-
expect(result).toBeUndefined();
|
|
52
|
-
});
|
|
53
|
-
});
|
package/src/utils/diffStrings.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
type ChangeType = "equal" | "delete" | "insert";
|
|
2
|
-
|
|
3
|
-
export interface Change {
|
|
4
|
-
type: ChangeType;
|
|
5
|
-
value: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function diffStrings(oldStr: string, newStr: string): Change[] {
|
|
9
|
-
// Find the longest common substring
|
|
10
|
-
function longestCommonSubstring(s1: string, s2: string): string {
|
|
11
|
-
const longest = { start: 0,
|
|
12
|
-
length: 0 };
|
|
13
|
-
const matrix = new Array(s1.length + 1).fill(null).map(() => new Array(s2.length + 1).fill(0));
|
|
14
|
-
|
|
15
|
-
for (let i = 1; i <= s1.length; i++) {
|
|
16
|
-
for (let j = 1; j <= s2.length; j++) {
|
|
17
|
-
if (s1[i - 1] === s2[j - 1]) {
|
|
18
|
-
matrix[i][j] = matrix[i - 1][j - 1] + 1;
|
|
19
|
-
if (matrix[i][j] > longest.length) {
|
|
20
|
-
longest.start = i - matrix[i][j];
|
|
21
|
-
longest.length = matrix[i][j];
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return s1.slice(longest.start, longest.start + longest.length);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Recursively find changes and create Change objects
|
|
31
|
-
function findChanges(s1: string, s2: string): Change[] {
|
|
32
|
-
if (s1 === s2) return s1.length > 0 ? [{
|
|
33
|
-
type: "equal",
|
|
34
|
-
value: s1
|
|
35
|
-
}] : [];
|
|
36
|
-
|
|
37
|
-
const common = longestCommonSubstring(s1, s2);
|
|
38
|
-
|
|
39
|
-
if (common.length === 0) {
|
|
40
|
-
const changes: Change[] = [];
|
|
41
|
-
if (s1.length > 0) {
|
|
42
|
-
changes.push({ type: "delete",
|
|
43
|
-
value: s1 });
|
|
44
|
-
}
|
|
45
|
-
if (s2.length > 0) {
|
|
46
|
-
changes.push({ type: "insert",
|
|
47
|
-
value: s2 });
|
|
48
|
-
}
|
|
49
|
-
return changes;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const s1Before = s1.slice(0, s1.indexOf(common));
|
|
53
|
-
const s1After = s1.slice(s1.indexOf(common) + common.length);
|
|
54
|
-
const s2Before = s2.slice(0, s2.indexOf(common));
|
|
55
|
-
const s2After = s2.slice(s2.indexOf(common) + common.length);
|
|
56
|
-
|
|
57
|
-
const changesBefore = findChanges(s1Before, s2Before);
|
|
58
|
-
const changesAfter = findChanges(s1After, s2After);
|
|
59
|
-
|
|
60
|
-
return [
|
|
61
|
-
...changesBefore,
|
|
62
|
-
{ type: "equal",
|
|
63
|
-
value: common },
|
|
64
|
-
...changesAfter
|
|
65
|
-
];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return findChanges(oldStr, newStr);
|
|
69
|
-
}
|
|
70
|
-
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { EntityValues, Properties, Property } from "@rebasepro/types";
|
|
2
|
-
|
|
3
|
-
export function countStringCharacters(values: EntityValues<any>, properties: Properties) {
|
|
4
|
-
let count = 0;
|
|
5
|
-
|
|
6
|
-
for (const key in values) {
|
|
7
|
-
const value = values[key];
|
|
8
|
-
const property: Property = properties[key];
|
|
9
|
-
|
|
10
|
-
if (property && !property.admin?.disabled) {
|
|
11
|
-
if (property.type === "string" || property.type === "number") {
|
|
12
|
-
count += String(value).length;
|
|
13
|
-
} else if (property.type === "array" && Array.isArray(value) && property.of && !Array.isArray(property.of) && property.of.type === "string") {
|
|
14
|
-
count += (value as string[]).reduce((acc, curr) => acc + (curr?.length ?? 0), 0);
|
|
15
|
-
} else if (property.type === "map" && property.properties && typeof value === "object") {
|
|
16
|
-
count += countStringCharacters(value, property.properties);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return count;
|
|
22
|
-
}
|
package/src/utils/suggestions.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export function getAppendableSuggestion(suggestion: string | number | undefined, value: unknown): string | undefined {
|
|
2
|
-
const suggestionIncludesValue = typeof suggestion === "string" && typeof value === "string" && suggestion.toLowerCase().trim().startsWith(value.toLowerCase().trim());
|
|
3
|
-
return (typeof value === "string" && suggestionIncludesValue)
|
|
4
|
-
? suggestion.substring(suggestion.toLowerCase().trim().indexOf(value.toLowerCase().trim()) + value.trim().length)
|
|
5
|
-
: undefined;
|
|
6
|
-
}
|