Skip to content

Commit baa8345

Browse files
terryyincursoragent
andcommitted
feat: two-step extract preview with editable fields before create
Extract now calls extract-note-preview and shows an editable preview in the refinement modal; Create note persists via create-extracted-note. E2E covers the two-step flow and saving edited preview content. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 34b414f commit baa8345

9 files changed

Lines changed: 462 additions & 44 deletions

File tree

e2e_test/features/assimilation/note_refinement.feature

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,19 @@ Feature: Note refinement
5858
| C | 2 | true |
5959
| D | 1 | |
6060
| E | 1 | |
61-
And I extract refinement layout points "B" and "D" to a new note
61+
And I open extraction preview for refinement layout points "B" and "D"
62+
And I create the note from the extraction preview
6263
Then the note title should be "Point B and D"
6364
And I should see folder "Sample tree/Context" containing these notes:
6465
| note-title |
6566
| Sample |
6667
| Point B and D |
68+
69+
Scenario: Save edited extraction preview content
70+
Given OpenAI will extract layout points "B and D" to a new note with title "Point B and D" and content "Combined B and D" and updated parent content "A. C. E."
71+
When I am assimilating the note "Sample"
72+
And I open extraction preview for refinement layout points "B" and "D"
73+
And I edit the extraction preview to title "Edited B and D" and content "Edited combined content" and updated parent content "A. C. E. edited"
74+
And I create the note from the extraction preview
75+
Then the note title should be "Edited B and D"
76+
And I should see note "Sample tree/Context/Sample" has content "A. C. E. edited"

e2e_test/start/pageObjects/assimilationPage/refinementLayoutExpectations.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,18 @@ import {
33
refinementLayoutPanel,
44
removeRefinementLayoutButton,
55
waitForExtractNote,
6+
waitForExtractNotePreview,
67
} from './shared'
78

9+
type ExtractionPreviewFields = {
10+
newNoteTitle: string
11+
newNoteContent: string
12+
updatedOriginalNoteContent: string
13+
}
14+
15+
const extractionPreviewPanel = () =>
16+
cy.get('[data-test-id="extraction-preview"]')
17+
818
export function assimilationRefinementLayoutExpectations() {
919
const showRefinementLayout = function (this: {
1020
openRefineNoteModal(): unknown
@@ -52,6 +62,12 @@ export function assimilationRefinementLayoutExpectations() {
5262
return this
5363
},
5464
extractLayoutPointsToNewNote(...layoutPointTexts: string[]) {
65+
this.openExtractionPreviewForLayoutPoints(...layoutPointTexts)
66+
this.createNoteFromExtractionPreview()
67+
return this
68+
},
69+
openExtractionPreviewForLayoutPoints(...layoutPointTexts: string[]) {
70+
showRefinementLayout.call(this)
5571
refinementLayoutPanel().within(() => {
5672
layoutPointTexts.forEach((layoutPointText) => {
5773
cy.contains('[data-layout-level] > label', layoutPointText)
@@ -61,6 +77,28 @@ export function assimilationRefinementLayoutExpectations() {
6177
})
6278
cy.findByRole('button', { name: 'Extract' }).click()
6379
})
80+
waitForExtractNotePreview()
81+
extractionPreviewPanel().should('be.visible')
82+
return this
83+
},
84+
editExtractionPreviewFields(fields: ExtractionPreviewFields) {
85+
extractionPreviewPanel().within(() => {
86+
cy.get('[data-test-id="extraction-preview-new-title"]')
87+
.clear()
88+
.type(fields.newNoteTitle)
89+
cy.get('[data-test-id="extraction-preview-new-content"]')
90+
.clear()
91+
.type(fields.newNoteContent)
92+
cy.get('[data-test-id="extraction-preview-original-content"]')
93+
.clear()
94+
.type(fields.updatedOriginalNoteContent)
95+
})
96+
return this
97+
},
98+
createNoteFromExtractionPreview() {
99+
extractionPreviewPanel()
100+
.find('[data-test-id="extraction-preview-create"]')
101+
.click()
64102
waitForExtractNote()
65103
return this
66104
},

e2e_test/start/pageObjects/assimilationPage/shared.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export const refinementLayoutPanel = () =>
4141
export const removeRefinementLayoutButton = () =>
4242
refinementLayoutPanel().find('[data-test-id="remove-refinement-layout"]')
4343

44+
export const waitForExtractNotePreview = () => {
45+
cy.contains('p.loading-message', 'AI is generating preview...', {
46+
timeout: 15000,
47+
}).should('not.exist')
48+
}
49+
4450
export const waitForExtractNote = () => {
4551
cy.contains('p.loading-message', 'AI is creating note...', {
4652
timeout: 15000,

e2e_test/step_definitions/note.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,34 @@ When(
828828
}
829829
)
830830

831+
When(
832+
'I open extraction preview for refinement layout points {string} and {string}',
833+
(firstPoint: string, secondPoint: string) => {
834+
start
835+
.assumeAssimilationPage()
836+
.openExtractionPreviewForLayoutPoints(firstPoint, secondPoint)
837+
}
838+
)
839+
840+
When('I create the note from the extraction preview', () => {
841+
start.assumeAssimilationPage().createNoteFromExtractionPreview()
842+
})
843+
844+
When(
845+
'I edit the extraction preview to title {string} and content {string} and updated parent content {string}',
846+
(
847+
newNoteTitle: string,
848+
newNoteContent: string,
849+
updatedOriginalNoteContent: string
850+
) => {
851+
start.assumeAssimilationPage().editExtractionPreviewFields({
852+
newNoteTitle,
853+
newNoteContent,
854+
updatedOriginalNoteContent,
855+
})
856+
}
857+
)
858+
831859
Then(
832860
'the link {string} should link to the note with the same title',
833861
(linkText: string) => {

frontend/src/components/recall/NoteRefinement.vue

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
class="mb-4 rounded-lg bg-accent p-4"
55
data-test-id="refinement-layout"
66
>
7-
<div class="text-base">
7+
<div v-if="!showExtractionPreview" class="text-base">
88
<div class="font-semibold mb-3 text-accent-content">
99
Note layout:
1010
</div>
@@ -62,12 +62,76 @@
6262
</button>
6363
</div>
6464
</div>
65+
66+
<div
67+
v-else
68+
class="text-base"
69+
data-test-id="extraction-preview"
70+
>
71+
<div class="font-semibold mb-3 text-accent-content">
72+
Extract preview:
73+
</div>
74+
75+
<div
76+
v-if="createError"
77+
class="daisy-alert daisy-alert-error mb-3 text-sm"
78+
data-test-id="extraction-preview-error"
79+
>
80+
{{ createError }}
81+
</div>
82+
83+
<label class="block mb-3 text-accent-content">
84+
<span class="font-medium">Updated original note content</span>
85+
<textarea
86+
v-model="extractionPreview.updatedOriginalNoteContent"
87+
data-test-id="extraction-preview-original-content"
88+
class="daisy-textarea daisy-textarea-bordered mt-1 w-full min-h-24"
89+
/>
90+
</label>
91+
92+
<label class="block mb-3 text-accent-content">
93+
<span class="font-medium">New note title</span>
94+
<textarea
95+
v-model="extractionPreview.newNoteTitle"
96+
data-test-id="extraction-preview-new-title"
97+
class="daisy-textarea daisy-textarea-bordered mt-1 w-full"
98+
rows="1"
99+
/>
100+
</label>
101+
102+
<label class="block mb-3 text-accent-content">
103+
<span class="font-medium">New note content</span>
104+
<textarea
105+
v-model="extractionPreview.newNoteContent"
106+
data-test-id="extraction-preview-new-content"
107+
class="daisy-textarea daisy-textarea-bordered mt-1 w-full min-h-24"
108+
/>
109+
</label>
110+
111+
<div class="flex gap-2 mt-4">
112+
<button
113+
data-test-id="extraction-preview-back"
114+
class="daisy-btn daisy-btn-ghost daisy-btn-sm"
115+
@click="backToLayout"
116+
>
117+
Back
118+
</button>
119+
<button
120+
data-test-id="extraction-preview-create"
121+
class="daisy-btn daisy-btn-primary daisy-btn-sm"
122+
@click="createExtractedNote"
123+
>
124+
Create note
125+
</button>
126+
</div>
127+
</div>
65128
</div>
66129
</template>
67130

68131
<script setup lang="ts">
69132
import type {
70133
Note,
134+
NoteExtractionResult,
71135
NoteRefinementLayoutItem,
72136
} from "@generated/doughnut-backend-api"
73137
import { AiController } from "@generated/doughnut-backend-api/sdk.gen"
@@ -76,6 +140,7 @@ import {
76140
apiCallWithLoading,
77141
runWithBlockingApiLoading,
78142
} from "@/managedApi/clientSetup"
143+
import { toOpenApiError } from "@/managedApi/openApiError"
79144
import { useRefinementLayoutSelection } from "@/composables/useRefinementLayoutSelection"
80145
import usePopups from "../commons/Popups/usePopups"
81146
import RefinementLayoutItemRow from "./RefinementLayoutItemRow.vue"
@@ -93,6 +158,13 @@ const emit = defineEmits<{
93158
}>()
94159
95160
const refinementLayoutItems = ref<NoteRefinementLayoutItem[]>([])
161+
const showExtractionPreview = ref(false)
162+
const extractionPreview = ref<NoteExtractionResult>({
163+
newNoteTitle: "",
164+
newNoteContent: "",
165+
updatedOriginalNoteContent: "",
166+
})
167+
const createError = ref("")
96168
97169
const {
98170
selectedItemIds,
@@ -102,6 +174,16 @@ const {
102174
clearSelection,
103175
} = useRefinementLayoutSelection(refinementLayoutItems)
104176
177+
const resetExtractionPreview = () => {
178+
showExtractionPreview.value = false
179+
createError.value = ""
180+
extractionPreview.value = {
181+
newNoteTitle: "",
182+
newNoteContent: "",
183+
updatedOriginalNoteContent: "",
184+
}
185+
}
186+
105187
const loadRefinementLayout = async () => {
106188
try {
107189
const result = await apiCallWithLoading(() =>
@@ -113,10 +195,12 @@ const loadRefinementLayout = async () => {
113195
refinementLayoutItems.value =
114196
!result.error && result.data?.items ? result.data.items : []
115197
clearSelection()
198+
resetExtractionPreview()
116199
} catch (err) {
117200
console.error("Failed to generate note layout:", err)
118201
refinementLayoutItems.value = []
119202
clearSelection()
203+
resetExtractionPreview()
120204
}
121205
}
122206
@@ -173,7 +257,7 @@ const extractNote = async () => {
173257
try {
174258
await runWithBlockingApiLoading(async () => {
175259
const response = await apiCallWithLoading(() =>
176-
AiController.extractNote({
260+
AiController.extractNotePreview({
177261
path: { note: props.note.id },
178262
body: {
179263
layout: { items: refinementLayoutItems.value },
@@ -183,7 +267,46 @@ const extractNote = async () => {
183267
)
184268
185269
if (response.error || !response.data) {
186-
await popups.alert("Failed to create note with AI")
270+
await popups.alert("Failed to generate extract preview")
271+
return
272+
}
273+
274+
extractionPreview.value = { ...response.data }
275+
createError.value = ""
276+
showExtractionPreview.value = true
277+
}, "AI is generating preview...")
278+
} catch (err) {
279+
console.error("Failed to generate extract preview:", err)
280+
await popups.alert(`Error: ${err}`)
281+
}
282+
}
283+
284+
const backToLayout = () => {
285+
showExtractionPreview.value = false
286+
createError.value = ""
287+
}
288+
289+
const createExtractedNote = async () => {
290+
createError.value = ""
291+
292+
try {
293+
await runWithBlockingApiLoading(async () => {
294+
const response = await apiCallWithLoading(() =>
295+
AiController.createExtractedNote({
296+
path: { note: props.note.id },
297+
body: {
298+
newNoteTitle: extractionPreview.value.newNoteTitle,
299+
newNoteContent: extractionPreview.value.newNoteContent,
300+
updatedOriginalNoteContent:
301+
extractionPreview.value.updatedOriginalNoteContent,
302+
},
303+
})
304+
)
305+
306+
if (response.error || !response.data) {
307+
const openApiError = toOpenApiError(response.error)
308+
createError.value =
309+
openApiError.message ?? "Failed to create note from preview"
187310
return
188311
}
189312
@@ -192,8 +315,8 @@ const extractNote = async () => {
192315
.focusNoteRealm(router, response.data)
193316
}, "AI is creating note...")
194317
} catch (err) {
195-
console.error("Failed to extract note:", err)
196-
await popups.alert(`Error: ${err}`)
318+
console.error("Failed to create extracted note:", err)
319+
createError.value = `Error: ${err}`
197320
}
198321
}
199322
</script>

0 commit comments

Comments
 (0)