@clone-ai/prompt-prediction 0.7.0-bootstrap.0 → 0.7.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/CONTRIBUTING.md +52 -0
  3. package/LICENSE +21 -0
  4. package/README.md +119 -1
  5. package/SECURITY.md +9 -0
  6. package/dist/assistant-ui.d.ts +5 -0
  7. package/dist/assistant-ui.js +26 -0
  8. package/dist/clone-mode.d.ts +64 -0
  9. package/dist/clone-mode.js +156 -0
  10. package/dist/controller.d.ts +61 -0
  11. package/dist/controller.js +155 -0
  12. package/dist/deadline.d.ts +2 -0
  13. package/dist/deadline.js +25 -0
  14. package/dist/feedback.d.ts +50 -0
  15. package/dist/feedback.js +118 -0
  16. package/dist/generated/api-types.d.ts +1663 -0
  17. package/dist/generated/api-types.js +5 -0
  18. package/dist/http.d.ts +8 -0
  19. package/dist/http.js +35 -0
  20. package/dist/index.d.ts +9 -0
  21. package/dist/index.js +4 -0
  22. package/dist/react.d.ts +50 -0
  23. package/dist/react.js +147 -0
  24. package/dist/server.d.ts +33 -0
  25. package/dist/server.js +93 -0
  26. package/dist/transport.d.ts +15 -0
  27. package/dist/transport.js +42 -0
  28. package/dist/types.d.ts +8 -0
  29. package/dist/types.js +1 -0
  30. package/docs/agent-integration.md +122 -0
  31. package/docs/api.md +69 -0
  32. package/docs/billing.md +53 -0
  33. package/docs/browser-onboarding.md +69 -0
  34. package/docs/clone-mode.md +40 -0
  35. package/docs/context-mapping.md +28 -0
  36. package/docs/data-and-service.md +25 -0
  37. package/docs/developer-apps.md +87 -0
  38. package/docs/feedback.md +70 -0
  39. package/docs/pilot-validation.md +35 -0
  40. package/docs/releases.md +88 -0
  41. package/docs/reliability.md +33 -0
  42. package/docs/start.md +91 -0
  43. package/examples/react/composer-events.ts +3 -0
  44. package/examples/react/demo.tsx +195 -0
  45. package/examples/react/index.html +12 -0
  46. package/examples/react/mode-demo.tsx +95 -0
  47. package/examples/react/pilot-observations.ts +57 -0
  48. package/examples/react/vite.config.ts +123 -0
  49. package/openapi.json +2268 -0
  50. package/package.json +99 -4
  51. package/playwright.config.ts +13 -0
  52. package/release-manifest.json +63 -0
  53. package/scripts/create-example.mjs +31 -0
  54. package/scripts/pilot-metrics.mjs +91 -0
  55. package/tests/browser/clone-mode-options.tsx +38 -0
  56. package/tests/browser/clone-mode.spec.ts +68 -0
  57. package/tests/browser/composer.spec.ts +238 -0
@@ -0,0 +1,238 @@
1
+ import { expect, test } from '@playwright/test';
2
+ import type { CompletionRequest } from '@clone-ai/prompt-prediction';
3
+
4
+ for (const assistant of [false, true]) {
5
+ test.describe(assistant ? 'assistant-ui' : 'React textarea', () => {
6
+ test.beforeEach(async ({ page }) => {
7
+ await page.goto('/?fixture=1' + (assistant ? '&assistant=1' : ''));
8
+ });
9
+ test('Tab inserts, Undo restores, explicit Enter sends once', async ({ page }) => {
10
+ const input = page.getByRole('textbox', { name: 'Instruction' });
11
+ await input.fill('이 장면을');
12
+ await expect(page.locator('[data-clone-suggestion] span')).toHaveText(' 더 짧게 편집해줘.');
13
+ await input.press('Tab');
14
+ await expect(input).toHaveValue('이 장면을 더 짧게 편집해줘.');
15
+ await expect(page.getByTestId('receipt-count')).toHaveText('0');
16
+ await input.press('ControlOrMeta+z');
17
+ await expect(input).toHaveValue('이 장면을');
18
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
19
+ await input.press('Tab');
20
+ await input.press('Enter');
21
+ await expect(page.getByTestId('receipt-count')).toHaveText('1');
22
+ await expect(input).toHaveValue('');
23
+ });
24
+ test('empty next prompt, Escape, focus traversal and repeated Tab', async ({ page }) => {
25
+ const input = page.getByRole('textbox', { name: 'Instruction' });
26
+ await input.focus();
27
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
28
+ await input.press('Escape');
29
+ await expect(page.locator('[data-clone-suggestion] span')).toBeEmpty();
30
+ await input.press('Tab');
31
+ await expect(input).not.toBeFocused();
32
+ await input.fill('다음');
33
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
34
+ await input.press('Tab');
35
+ await input.press('Tab');
36
+ await expect(input).toHaveValue('다음 더 짧게 편집해줘.');
37
+ await expect(page.getByTestId('receipt-count')).toHaveText('0');
38
+ });
39
+ test('composition, selected text and context change invalidate suggestions', async ({ page }) => {
40
+ const input = page.getByRole('textbox', { name: 'Instruction' });
41
+ await input.fill('편집');
42
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
43
+ await input.dispatchEvent('compositionstart', { data: '한' });
44
+ await expect(page.locator('[data-clone-suggestion] span')).toBeEmpty();
45
+ await input.dispatchEvent('keydown', { key: 'Tab', isComposing: true, keyCode: 229 });
46
+ await expect(input).toHaveValue('편집');
47
+ await input.dispatchEvent('compositionend', { data: '한' });
48
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
49
+ await input.press('Shift+ArrowLeft');
50
+ await expect(page.locator('[data-clone-suggestion] span')).toBeEmpty();
51
+ await page.getByRole('combobox', { name: 'Context' }).selectOption('slides');
52
+ await input.focus(); await input.press('ArrowRight');
53
+ await expect(page.locator('[data-clone-suggestion] span')).toHaveText(' 결론부터 정리해줘.');
54
+ await input.press('Tab');
55
+ await expect(input).toHaveValue('편집 결론부터 정리해줘.');
56
+ await page.screenshot({ path: `test-results/${assistant ? 'assistant-ui' : 'react'}-slides.png`, fullPage: true });
57
+ });
58
+ test('records acceptance, human edit and explicit submission separately', async ({ page }) => {
59
+ const input = page.getByRole('textbox', { name: 'Instruction' });
60
+ const readEvents = async () => JSON.parse(await page.getByTestId('observation-events').textContent() ?? '[]');
61
+ await input.fill('초안');
62
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
63
+ await input.press('Tab');
64
+ expect((await readEvents()).map((event: { kind: string }) => event.kind)).toEqual(['presented', 'accepted']);
65
+ await input.pressSequentially('!');
66
+ await expect(input).toHaveValue('초안 더 짧게 편집해줘.!');
67
+ await input.press('Enter');
68
+ await expect(page.getByTestId('receipt-count')).toHaveText('1');
69
+ const events = await readEvents();
70
+ expect(events.map((event: { kind: string }) => event.kind)).toEqual(['presented', 'accepted', 'edited', 'submitted']);
71
+ expect(new Set(events.map((event: { request_id: string }) => event.request_id)).size).toBe(1);
72
+ expect(events.every((event: { delivery: string }) => event.delivery === 'fixture')).toBe(true);
73
+ expect(JSON.stringify(events)).not.toContain('초안');
74
+ });
75
+ test('opted-in edited submission retains attribution and final text', async ({ page }) => {
76
+ await page.getByText('Feedback loop · test fixture', { exact: true }).click();
77
+ await page.getByRole('checkbox', { name: 'Share edited submission text (optional)' }).check();
78
+ const input = page.getByRole('textbox', { name: 'Instruction' });
79
+ await input.fill('초안');
80
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
81
+ await input.press('Tab'); await input.pressSequentially('!'); await input.press('Enter');
82
+ await expect(page.getByTestId('receipt-count')).toHaveText('1');
83
+ const events = JSON.parse(await page.getByTestId('observation-events').textContent() ?? '[]');
84
+ expect(events.find((event: { kind: string }) => event.kind === 'submitted')).toMatchObject({
85
+ submission_origin: 'edited_prediction', content_opt_in: true, final_text: '초안 더 짧게 편집해줘.!',
86
+ });
87
+ });
88
+ test('explicit rejection and evaluation do not send the draft', async ({ page }) => {
89
+ await page.getByText('Feedback loop · test fixture', { exact: true }).click();
90
+ const input = page.getByRole('textbox', { name: 'Instruction' });
91
+ await input.fill('초안');
92
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
93
+ await page.getByRole('button', { name: 'Reject: too long' }).click();
94
+ await expect(page.getByTestId('receipt-count')).toHaveText('0');
95
+ await page.getByRole('checkbox', { name: 'Share edited submission text (optional)' }).check();
96
+ await page.getByRole('textbox', { name: 'Feedback guidance' }).fill('Use one sentence.');
97
+ await page.getByRole('button', { name: 'Send feedback', exact: true }).click();
98
+ const events = JSON.parse(await page.getByTestId('observation-events').textContent() ?? '[]');
99
+ expect(events.find((event: { kind: string }) => event.kind === 'rejected')).toMatchObject({ reason: 'too_long' });
100
+ expect(events.find((event: { kind: string }) => event.kind === 'feedback')).toMatchObject({
101
+ guidance: 'Use one sentence.', content_opt_in: true,
102
+ });
103
+ await expect(page.getByTestId('receipt-count')).toHaveText('0');
104
+ });
105
+ test('does not attribute manual submission after Undo removes the completion', async ({ page }) => {
106
+ const input = page.getByRole('textbox', { name: 'Instruction' });
107
+ await input.fill('초안');
108
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
109
+ await input.press('Tab');
110
+ await input.press('ControlOrMeta+z');
111
+ await expect(input).toHaveValue('초안');
112
+ await input.press('Enter');
113
+ await expect(page.getByTestId('receipt-count')).toHaveText('1');
114
+ const events = JSON.parse(await page.getByTestId('observation-events').textContent() ?? '[]');
115
+ expect(events.some((event: { kind: string }) => event.kind === 'submitted')).toBe(false);
116
+ });
117
+ test('failed telemetry delivery does not block explicit host submission', async ({ page }) => {
118
+ await page.route('**/api/clone/state', route => route.fulfill({ json: { connection_id: 'test-connection' } }));
119
+ await page.route('**/api/clone/predict', async route => {
120
+ const request = route.request().postDataJSON();
121
+ await route.fulfill({ json: {
122
+ request_id: request.request_id, prediction_id: 'test-prediction', connection_id: request.connection_id,
123
+ session_id: request.session_id, draft_revision: request.draft.revision, context_revision: request.context_revision,
124
+ profile_revision: 'fixture', grant_revision: 1, status: 'suggested', completion: ' suggestion',
125
+ expires_at: Math.floor(Date.now() / 1000) + 60, context_truncated: false, usage: { prediction_units: 1 },
126
+ } });
127
+ });
128
+ await page.route('**/api/clone/events', route => route.fulfill({ status: 503, json: { detail: { code: 'offline' } } }));
129
+ await page.goto('/?connected=1' + (assistant ? '&assistant=1' : ''));
130
+ const input = page.getByRole('textbox', { name: 'Instruction' });
131
+ await expect(page.getByTestId('connection')).toHaveText('Connected');
132
+ await input.fill('draft');
133
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
134
+ await input.press('Tab');
135
+ await input.press('Enter');
136
+ await expect(page.getByTestId('receipt-count')).toHaveText('1');
137
+ await expect(input).toHaveValue('');
138
+ await page.getByRole('combobox', { name: 'Context' }).focus();
139
+ await expect.poll(async () => {
140
+ const events = JSON.parse(await page.getByTestId('observation-events').textContent() ?? '[]');
141
+ const submitted = events.find((event: { kind: string }) => event.kind === 'submitted');
142
+ return events.filter((event: { request_id: string; delivery: string }) =>
143
+ event.request_id === submitted?.request_id && event.delivery === 'failed')
144
+ .map((event: { kind: string }) => event.kind);
145
+ }).toEqual(['presented', 'accepted', 'submitted']);
146
+ });
147
+ test('next prediction receives submitted text, origin and a new conversation revision', async ({ page }) => {
148
+ const requests: CompletionRequest[] = [];
149
+ await page.route('**/api/clone/state', route => route.fulfill({ json: { connection_id: 'test-connection' } }));
150
+ await page.route('**/api/clone/events', route => route.fulfill({ json: { status: 'recorded' } }));
151
+ await page.route('**/api/clone/predict', async route => {
152
+ const request = route.request().postDataJSON();
153
+ requests.push(request);
154
+ await route.fulfill({ json: {
155
+ request_id: request.request_id, prediction_id: 'test-prediction', connection_id: request.connection_id,
156
+ session_id: request.session_id, draft_revision: request.draft.revision, context_revision: request.context_revision,
157
+ profile_revision: 'fixture', grant_revision: 1, status: 'suggested', completion: ' suggestion',
158
+ expires_at: Math.floor(Date.now() / 1000) + 60, context_truncated: false, usage: { prediction_units: 1 },
159
+ } });
160
+ });
161
+ await page.goto('/?connected=1' + (assistant ? '&assistant=1' : ''));
162
+ await expect(page.getByTestId('connection')).toHaveText('Connected');
163
+ const input = page.getByRole('textbox', { name: 'Instruction' });
164
+ await input.fill('draft');
165
+ await expect(page.locator('[data-clone-suggestion] span')).not.toBeEmpty();
166
+ const before = requests.at(-1);
167
+ await input.press('Tab');
168
+ await input.pressSequentially(' edited');
169
+ await expect(input).toHaveValue('draft suggestion edited');
170
+ await input.press('Enter');
171
+ await expect(page.getByTestId('receipt-count')).toHaveText('1');
172
+ await input.fill('follow up');
173
+ await expect.poll(() => requests.at(-1)?.draft.text).toBe('follow up');
174
+ const after = requests.at(-1);
175
+ if (!before?.artifact || !after?.artifact || !after.messages) {
176
+ throw new Error('Expected prediction requests with artifact and conversation context');
177
+ }
178
+ expect(after.messages.at(-1)).toEqual({ role: 'user', content: 'draft suggestion edited', origin: 'edited_prediction' });
179
+ expect(after.context_revision).not.toBe(before.context_revision);
180
+ expect(after.artifact.revision).toBe(before.artifact.revision);
181
+ expect(after.session_id).toBe(before.session_id);
182
+ await input.fill('manual correction');
183
+ await input.press('Enter');
184
+ await expect(page.getByTestId('receipt-count')).toHaveText('2');
185
+ await input.fill('another follow up');
186
+ await expect.poll(() => requests.at(-1)?.draft.text).toBe('another follow up');
187
+ expect(requests.at(-1)?.messages?.slice(-2)).toEqual([
188
+ { role: 'user', content: 'draft suggestion edited', origin: 'edited_prediction' },
189
+ { role: 'user', content: 'manual correction', origin: 'human' },
190
+ ]);
191
+ });
192
+ });
193
+ }
194
+
195
+ for (const assistant of [false, true]) {
196
+ test(`product context works before connecting and after disconnecting (${assistant ? 'assistant-ui' : 'React'})`, async ({ page }) => {
197
+ let connection: string | null = null;
198
+ const requests: CompletionRequest[] = [];
199
+ await page.route('**/api/clone/state', route => route.fulfill({ json: { connection_id: connection } }));
200
+ await page.route('**/api/clone/events', route => route.fulfill({ json: { status: 'recorded' } }));
201
+ await page.route('**/api/clone/connect', () => { throw new Error('Basic mode must not initiate Clone login'); });
202
+ await page.route('**/api/clone/disconnect', route => { connection = null; return route.fulfill({ json: { status: 'revoked' } }); });
203
+ await page.route('**/api/clone/predict', route => {
204
+ const request = route.request().postDataJSON(); requests.push(request);
205
+ return route.fulfill({ json: { request_id: request.request_id, prediction_id: 'p-' + request.request_id,
206
+ connection_id: request.connection_id ?? null, session_id: request.session_id,
207
+ draft_revision: request.draft.revision, context_revision: request.context_revision,
208
+ profile_revision: request.connection_id ? 'profile' : '', grant_revision: request.connection_id ? 1 : 0,
209
+ status: 'suggested', completion: request.connection_id ? ' personalized' : ' product suggestion',
210
+ expires_at: Math.floor(Date.now() / 1000) + 60, context_truncated: false, usage: { prediction_units: 1 } } });
211
+ });
212
+ await page.goto('/?connected=1' + (assistant ? '&assistant=1' : ''));
213
+ const input = page.getByRole('textbox', { name: 'Instruction' });
214
+ await input.focus();
215
+ await expect(page.locator('[data-clone-suggestion] span')).toHaveText(' product suggestion');
216
+ expect(requests.at(-1)?.mode).toBe('next_prompt');
217
+ expect(requests.at(-1)?.connection_id).toBeNull();
218
+ await input.fill('draft');
219
+ await expect(page.locator('[data-clone-suggestion] span')).toHaveText(' product suggestion');
220
+ await input.press('Tab');
221
+ await expect(input).toHaveValue('draft product suggestion');
222
+ await expect(page.getByTestId('receipt-count')).toHaveText('0');
223
+ await input.press('Enter');
224
+ await expect(page.getByTestId('receipt-count')).toHaveText('1');
225
+ await page.getByText('Optional personalization', { exact: true }).click();
226
+ connection = 'approved-test-grant';
227
+ await page.getByRole('button', { name: 'Refresh connection' }).click();
228
+ await input.fill('new draft');
229
+ await expect(page.locator('[data-clone-suggestion] span')).toHaveText(' personalized');
230
+ await page.getByRole('button', { name: 'Disconnect', exact: true }).click();
231
+ await expect(page.getByTestId('connection')).toHaveText('Product context');
232
+ await expect(page.locator('[data-clone-suggestion] span')).toBeEmpty();
233
+ await input.fill('after disconnect');
234
+ await expect(page.locator('[data-clone-suggestion] span')).toHaveText(' product suggestion');
235
+ expect(requests.at(-1)?.connection_id).toBeNull();
236
+ await page.screenshot({ path: `test-results/optional-clone-${assistant ? 'assistant' : 'react'}.png`, fullPage: true });
237
+ });
238
+ }