浏览代码

enh: note selection edit support

Timothy Jaeryang Baek 2 月之前
父节点
当前提交
3af2938efc

+ 5 - 0
src/app.css

@@ -40,6 +40,11 @@ code {
 	width: auto;
 	width: auto;
 }
 }
 
 
+.editor-selection {
+	background: rgba(180, 213, 255, 0.5);
+	border-radius: 2px;
+}
+
 .font-secondary {
 .font-secondary {
 	font-family: 'InstrumentSerif', sans-serif;
 	font-family: 'InstrumentSerif', sans-serif;
 }
 }

+ 35 - 1
src/lib/components/common/RichTextInput.svelte

@@ -56,6 +56,7 @@
 
 
 	import { Fragment, DOMParser } from 'prosemirror-model';
 	import { Fragment, DOMParser } from 'prosemirror-model';
 	import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state';
 	import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state';
+	import { Decoration, DecorationSet } from 'prosemirror-view';
 	import { Editor, Extension } from '@tiptap/core';
 	import { Editor, Extension } from '@tiptap/core';
 
 
 	// Yjs imports
 	// Yjs imports
@@ -167,6 +168,8 @@
 		});
 		});
 	};
 	};
 
 
+	export let onSelectionUpdate = (e) => {};
+
 	export let id = '';
 	export let id = '';
 	export let value = '';
 	export let value = '';
 	export let html = '';
 	export let html = '';
@@ -192,6 +195,8 @@
 	let jsonValue = '';
 	let jsonValue = '';
 	let mdValue = '';
 	let mdValue = '';
 
 
+	let lastSelectionBookmark = null;
+
 	// Yjs setup
 	// Yjs setup
 	let ydoc = null;
 	let ydoc = null;
 	let yXmlFragment = null;
 	let yXmlFragment = null;
@@ -827,6 +832,33 @@
 		}
 		}
 	};
 	};
 
 
+	const SelectionDecoration = Extension.create({
+		name: 'selectionDecoration',
+		addProseMirrorPlugins() {
+			return [
+				new Plugin({
+					key: new PluginKey('selection'),
+					props: {
+						decorations: (state) => {
+							const { selection } = state;
+							const { focused } = this.editor;
+
+							if (focused || selection.empty) {
+								return null;
+							}
+
+							return DecorationSet.create(state.doc, [
+								Decoration.inline(selection.from, selection.to, {
+									class: 'editor-selection'
+								})
+							]);
+						}
+					}
+				})
+			];
+		}
+	});
+
 	onMount(async () => {
 	onMount(async () => {
 		content = value;
 		content = value;
 
 
@@ -882,6 +914,7 @@
 					link: link
 					link: link
 				}),
 				}),
 				Placeholder.configure({ placeholder }),
 				Placeholder.configure({ placeholder }),
+				SelectionDecoration,
 
 
 				CodeBlockLowlight.configure({
 				CodeBlockLowlight.configure({
 					lowlight
 					lowlight
@@ -1168,7 +1201,8 @@
 				if (files) {
 				if (files) {
 					editor.storage.files = files;
 					editor.storage.files = files;
 				}
 				}
-			}
+			},
+			onSelectionUpdate: onSelectionUpdate
 		});
 		});
 
 
 		if (messageInput) {
 		if (messageInput) {

+ 15 - 0
src/lib/components/notes/NoteEditor.svelte

@@ -121,6 +121,8 @@
 	let showPanel = false;
 	let showPanel = false;
 	let selectedPanel = 'chat';
 	let selectedPanel = 'chat';
 
 
+	let selectedContent = null;
+
 	let showDeleteConfirm = false;
 	let showDeleteConfirm = false;
 	let showAccessControlModal = false;
 	let showAccessControlModal = false;
 
 
@@ -1199,6 +1201,16 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
 							image={true}
 							image={true}
 							placeholder={$i18n.t('Write something...')}
 							placeholder={$i18n.t('Write something...')}
 							editable={versionIdx === null && !editing}
 							editable={versionIdx === null && !editing}
+							onSelectionUpdate={({ editor }) => {
+								const { from, to } = editor.state.selection;
+								const selectedText = editor.state.doc.textBetween(from, to, ' ');
+
+								selectedContent = {
+									text: selectedText,
+									from: from,
+									to: to
+								};
+							}}
 							onChange={(content) => {
 							onChange={(content) => {
 								note.data.content.html = content.html;
 								note.data.content.html = content.html;
 								note.data.content.md = content.md;
 								note.data.content.md = content.md;
@@ -1398,6 +1410,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
 				bind:editing
 				bind:editing
 				bind:streaming
 				bind:streaming
 				bind:stopResponseFlag
 				bind:stopResponseFlag
+				{editor}
+				{inputElement}
+				{selectedContent}
 				{files}
 				{files}
 				onInsert={insertHandler}
 				onInsert={insertHandler}
 				onStop={stopResponseHandler}
 				onStop={stopResponseHandler}

+ 47 - 15
src/lib/components/notes/NoteEditor/Chat.svelte

@@ -56,11 +56,14 @@
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
+	export let editor = null;
+
 	export let editing = false;
 	export let editing = false;
 	export let streaming = false;
 	export let streaming = false;
 	export let stopResponseFlag = false;
 	export let stopResponseFlag = false;
 
 
 	export let note = null;
 	export let note = null;
+	export let selectedContent = null;
 
 
 	export let files = [];
 	export let files = [];
 	export let messages = [];
 	export let messages = [];
@@ -79,20 +82,23 @@
 	let messagesContainerElement: HTMLDivElement;
 	let messagesContainerElement: HTMLDivElement;
 
 
 	let system = '';
 	let system = '';
-	let editorEnabled = false;
+	let editEnabled = false;
 	let chatInputElement = null;
 	let chatInputElement = null;
 
 
 	const DEFAULT_DOCUMENT_EDITOR_PROMPT = `You are an expert document editor.
 	const DEFAULT_DOCUMENT_EDITOR_PROMPT = `You are an expert document editor.
 
 
 ## Task
 ## Task
-Based on the user's instruction, update and enhance the existing notes by incorporating relevant and accurate information from the provided context in the content's primary language. Ensure all edits strictly follow the user’s intent.
+Based on the user's instruction, update and enhance the existing notes or selection by incorporating relevant and accurate information from the provided context in the content's primary language. Ensure all edits strictly follow the user’s intent.
 
 
 ## Input Structure
 ## Input Structure
 - Existing notes: Enclosed within <notes></notes> XML tags.
 - Existing notes: Enclosed within <notes></notes> XML tags.
 - Additional context: Enclosed within <context></context> XML tags.
 - Additional context: Enclosed within <context></context> XML tags.
+- Current note selection: Enclosed within <selection></selection> XML tags.
 - Editing instruction: Provided in the user message.
 - Editing instruction: Provided in the user message.
 
 
 ## Output Instructions
 ## Output Instructions
+- If a selection is provided, edit **only** the content within <selection></selection>. Leave unselected parts unchanged.
+- If no selection is provided, edit the entire notes.
 - Deliver a single, rewritten version of the notes in markdown format.
 - Deliver a single, rewritten version of the notes in markdown format.
 - Integrate information from the context only if it directly supports the user's instruction.
 - Integrate information from the context only if it directly supports the user's instruction.
 - Use clear, organized markdown elements: headings, lists, task lists ([ ]) where tasks or checklists are strongly implied, bold and italic text as appropriate.
 - Use clear, organized markdown elements: headings, lists, task lists ([ ]) where tasks or checklists are strongly implied, bold and italic text as appropriate.
@@ -155,7 +161,7 @@ Based on the user's instruction, update and enhance the existing notes by incorp
 
 
 		system = '';
 		system = '';
 
 
-		if (editorEnabled) {
+		if (editEnabled) {
 			system = `${DEFAULT_DOCUMENT_EDITOR_PROMPT}\n\n`;
 			system = `${DEFAULT_DOCUMENT_EDITOR_PROMPT}\n\n`;
 		} else {
 		} else {
 			system = `You are a helpful assistant. Please answer the user's questions based on the context provided.\n\n`;
 			system = `You are a helpful assistant. Please answer the user's questions based on the context provided.\n\n`;
@@ -165,7 +171,8 @@ Based on the user's instruction, update and enhance the existing notes by incorp
 			`<notes>${note?.data?.content?.md ?? ''}</notes>` +
 			`<notes>${note?.data?.content?.md ?? ''}</notes>` +
 			(files && files.length > 0
 			(files && files.length > 0
 				? `\n<context>${files.map((file) => `${file.name}: ${file?.file?.data?.content ?? 'Could not extract content'}\n`).join('')}</context>`
 				? `\n<context>${files.map((file) => `${file.name}: ${file?.file?.data?.content ?? 'Could not extract content'}\n`).join('')}</context>`
-				: '');
+				: '') +
+			(selectedContent ? `\n<selection>${selectedContent?.text}</selection>` : '');
 
 
 		const chatMessages = JSON.parse(
 		const chatMessages = JSON.parse(
 			JSON.stringify([
 			JSON.stringify([
@@ -206,7 +213,7 @@ Based on the user's instruction, update and enhance the existing notes by incorp
 						controller.abort('User: Stop Response');
 						controller.abort('User: Stop Response');
 					}
 					}
 
 
-					if (editorEnabled) {
+					if (editEnabled) {
 						editing = false;
 						editing = false;
 						streaming = false;
 						streaming = false;
 						onEdited();
 						onEdited();
@@ -222,8 +229,20 @@ Based on the user's instruction, update and enhance the existing notes by incorp
 						if (line !== '') {
 						if (line !== '') {
 							console.log(line);
 							console.log(line);
 							if (line === 'data: [DONE]') {
 							if (line === 'data: [DONE]') {
-								if (editorEnabled) {
+								if (editEnabled) {
 									responseMessage.content = `<status title="${$i18n.t('Edited')}" done="true" />`;
 									responseMessage.content = `<status title="${$i18n.t('Edited')}" done="true" />`;
+
+									if (selectedContent && editor) {
+										editor.commands.insertContentAt(
+											{
+												from: selectedContent.from,
+												to: selectedContent.to
+											},
+											enhancedContent.html || enhancedContent.md || ''
+										);
+
+										selectedContent = null;
+									}
 								}
 								}
 
 
 								responseMessage.done = true;
 								responseMessage.done = true;
@@ -236,20 +255,23 @@ Based on the user's instruction, update and enhance the existing notes by incorp
 								if (responseMessage.content == '' && deltaContent == '\n') {
 								if (responseMessage.content == '' && deltaContent == '\n') {
 									continue;
 									continue;
 								} else {
 								} else {
-									if (editorEnabled) {
+									if (editEnabled) {
 										editing = true;
 										editing = true;
 										streaming = true;
 										streaming = true;
 
 
 										enhancedContent.md += deltaContent;
 										enhancedContent.md += deltaContent;
 										enhancedContent.html = marked.parse(enhancedContent.md);
 										enhancedContent.html = marked.parse(enhancedContent.md);
 
 
-										note.data.content.md = enhancedContent.md;
-										note.data.content.html = enhancedContent.html;
-										note.data.content.json = null;
+										if (!selectedContent) {
+											note.data.content.md = enhancedContent.md;
+											note.data.content.html = enhancedContent.html;
+											note.data.content.json = null;
+
+											scrollToBottomHandler();
+										}
 
 
 										responseMessage.content = `<status title="${$i18n.t('Editing')}" done="false" />`;
 										responseMessage.content = `<status title="${$i18n.t('Editing')}" done="false" />`;
 
 
-										scrollToBottomHandler();
 										messages = messages;
 										messages = messages;
 									} else {
 									} else {
 										messageContent += deltaContent;
 										messageContent += deltaContent;
@@ -297,7 +319,7 @@ Based on the user's instruction, update and enhance the existing notes by incorp
 	};
 	};
 
 
 	onMount(async () => {
 	onMount(async () => {
-		editorEnabled = localStorage.getItem('noteEditorEnabled') === 'true';
+		editEnabled = localStorage.getItem('noteEditEnabled') === 'true';
 
 
 		loaded = true;
 		loaded = true;
 
 
@@ -355,6 +377,16 @@ Based on the user's instruction, update and enhance the existing notes by incorp
 				</div>
 				</div>
 
 
 				<div class=" pb-2">
 				<div class=" pb-2">
+					{#if selectedContent}
+						<div class="text-xs rounded-xl px-3.5 py-3 w-full markdown-prose-xs">
+							<blockquote>
+								<div class=" line-clamp-3">
+									{selectedContent?.text}
+								</div>
+							</blockquote>
+						</div>
+					{/if}
+
 					<MessageInput
 					<MessageInput
 						bind:chatInputElement
 						bind:chatInputElement
 						acceptFiles={false}
 						acceptFiles={false}
@@ -368,12 +400,12 @@ Based on the user's instruction, update and enhance the existing notes by incorp
 								<Tooltip content={$i18n.t('Edit')} placement="top">
 								<Tooltip content={$i18n.t('Edit')} placement="top">
 									<button
 									<button
 										on:click|preventDefault={() => {
 										on:click|preventDefault={() => {
-											editorEnabled = !editorEnabled;
+											editEnabled = !editEnabled;
 
 
-											localStorage.setItem('noteEditorEnabled', editorEnabled ? 'true' : 'false');
+											localStorage.setItem('noteEditEnabled', editEnabled ? 'true' : 'false');
 										}}
 										}}
 										type="button"
 										type="button"
-										class="px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {editorEnabled
+										class="px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {editEnabled
 											? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
 											? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
 											: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
 											: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
 									>
 									>