|
@@ -10,13 +10,15 @@ import { DateTime } from "luxon"
|
|
|
|
|
|
|
|
interface PartBase {
|
|
interface PartBase {
|
|
|
content: string
|
|
content: string
|
|
|
|
|
+ start: number
|
|
|
|
|
+ end: number
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-interface TextPart extends PartBase {
|
|
|
|
|
|
|
+export interface TextPart extends PartBase {
|
|
|
type: "text"
|
|
type: "text"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-interface FileAttachmentPart extends PartBase {
|
|
|
|
|
|
|
+export interface FileAttachmentPart extends PartBase {
|
|
|
type: "file"
|
|
type: "file"
|
|
|
path: string
|
|
path: string
|
|
|
selection?: TextSelection
|
|
selection?: TextSelection
|
|
@@ -34,7 +36,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
const local = useLocal()
|
|
const local = useLocal()
|
|
|
let editorRef!: HTMLDivElement
|
|
let editorRef!: HTMLDivElement
|
|
|
|
|
|
|
|
- const defaultParts = [{ type: "text", content: "" } as const]
|
|
|
|
|
|
|
+ const defaultParts = [{ type: "text", content: "", start: 0, end: 0 } as const]
|
|
|
const [store, setStore] = createStore<{
|
|
const [store, setStore] = createStore<{
|
|
|
contentParts: ContentPart[]
|
|
contentParts: ContentPart[]
|
|
|
popoverIsOpen: boolean
|
|
popoverIsOpen: boolean
|
|
@@ -51,7 +53,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
event.stopPropagation()
|
|
event.stopPropagation()
|
|
|
// @ts-expect-error
|
|
// @ts-expect-error
|
|
|
const plainText = (event.clipboardData || window.clipboardData)?.getData("text/plain") ?? ""
|
|
const plainText = (event.clipboardData || window.clipboardData)?.getData("text/plain") ?? ""
|
|
|
- addPart({ type: "text", content: plainText })
|
|
|
|
|
|
|
+ addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
onMount(() => {
|
|
onMount(() => {
|
|
@@ -74,7 +76,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
key: (x) => x,
|
|
key: (x) => x,
|
|
|
onSelect: (path) => {
|
|
onSelect: (path) => {
|
|
|
if (!path) return
|
|
if (!path) return
|
|
|
- addPart({ type: "file", path, content: "@" + getFilename(path) })
|
|
|
|
|
|
|
+ addPart({ type: "file", path, content: "@" + getFilename(path), start: 0, end: 0 })
|
|
|
setStore("popoverIsOpen", false)
|
|
setStore("popoverIsOpen", false)
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
@@ -117,17 +119,26 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
|
|
|
|
|
const parseFromDOM = (): ContentPart[] => {
|
|
const parseFromDOM = (): ContentPart[] => {
|
|
|
const newParts: ContentPart[] = []
|
|
const newParts: ContentPart[] = []
|
|
|
|
|
+ let position = 0
|
|
|
editorRef.childNodes.forEach((node) => {
|
|
editorRef.childNodes.forEach((node) => {
|
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
|
- if (node.textContent) newParts.push({ type: "text", content: node.textContent })
|
|
|
|
|
|
|
+ if (node.textContent) {
|
|
|
|
|
+ const content = node.textContent
|
|
|
|
|
+ newParts.push({ type: "text", content, start: position, end: position + content.length })
|
|
|
|
|
+ position += content.length
|
|
|
|
|
+ }
|
|
|
} else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type) {
|
|
} else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type) {
|
|
|
switch ((node as HTMLElement).dataset.type) {
|
|
switch ((node as HTMLElement).dataset.type) {
|
|
|
case "file":
|
|
case "file":
|
|
|
|
|
+ const content = node.textContent!
|
|
|
newParts.push({
|
|
newParts.push({
|
|
|
type: "file",
|
|
type: "file",
|
|
|
path: (node as HTMLElement).dataset.path!,
|
|
path: (node as HTMLElement).dataset.path!,
|
|
|
- content: node.textContent!,
|
|
|
|
|
|
|
+ content,
|
|
|
|
|
+ start: position,
|
|
|
|
|
+ end: position + content.length,
|
|
|
})
|
|
})
|
|
|
|
|
+ position += content.length
|
|
|
break
|
|
break
|
|
|
default:
|
|
default:
|
|
|
break
|
|
break
|
|
@@ -163,17 +174,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
const startIndex = atMatch ? atMatch.index! : cursorPosition
|
|
const startIndex = atMatch ? atMatch.index! : cursorPosition
|
|
|
const endIndex = atMatch ? cursorPosition : cursorPosition
|
|
const endIndex = atMatch ? cursorPosition : cursorPosition
|
|
|
|
|
|
|
|
- const pushText = (acc: { parts: ContentPart[] }, value: string) => {
|
|
|
|
|
|
|
+ const pushText = (acc: { parts: ContentPart[]; runningIndex: number }, value: string) => {
|
|
|
if (!value) return
|
|
if (!value) return
|
|
|
const last = acc.parts[acc.parts.length - 1]
|
|
const last = acc.parts[acc.parts.length - 1]
|
|
|
if (last && last.type === "text") {
|
|
if (last && last.type === "text") {
|
|
|
acc.parts[acc.parts.length - 1] = {
|
|
acc.parts[acc.parts.length - 1] = {
|
|
|
type: "text",
|
|
type: "text",
|
|
|
content: last.content + value,
|
|
content: last.content + value,
|
|
|
|
|
+ start: last.start,
|
|
|
|
|
+ end: last.end + value.length,
|
|
|
}
|
|
}
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- acc.parts.push({ type: "text", content: value })
|
|
|
|
|
|
|
+ acc.parts.push({ type: "text", content: value, start: acc.runningIndex, end: acc.runningIndex + value.length })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const {
|
|
const {
|
|
@@ -183,20 +196,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
} = store.contentParts.reduce(
|
|
} = store.contentParts.reduce(
|
|
|
(acc, item) => {
|
|
(acc, item) => {
|
|
|
if (acc.inserted) {
|
|
if (acc.inserted) {
|
|
|
- acc.parts.push(item)
|
|
|
|
|
|
|
+ acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
|
|
|
acc.runningIndex += item.content.length
|
|
acc.runningIndex += item.content.length
|
|
|
return acc
|
|
return acc
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const nextIndex = acc.runningIndex + item.content.length
|
|
const nextIndex = acc.runningIndex + item.content.length
|
|
|
if (nextIndex <= startIndex) {
|
|
if (nextIndex <= startIndex) {
|
|
|
- acc.parts.push(item)
|
|
|
|
|
|
|
+ acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
|
|
|
acc.runningIndex = nextIndex
|
|
acc.runningIndex = nextIndex
|
|
|
return acc
|
|
return acc
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (item.type !== "text") {
|
|
if (item.type !== "text") {
|
|
|
- acc.parts.push(item)
|
|
|
|
|
|
|
+ acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
|
|
|
acc.runningIndex = nextIndex
|
|
acc.runningIndex = nextIndex
|
|
|
return acc
|
|
return acc
|
|
|
}
|
|
}
|
|
@@ -207,24 +220,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
const tail = item.content.slice(tailLength)
|
|
const tail = item.content.slice(tailLength)
|
|
|
|
|
|
|
|
pushText(acc, head)
|
|
pushText(acc, head)
|
|
|
|
|
+ acc.runningIndex += head.length
|
|
|
|
|
|
|
|
if (part.type === "text") {
|
|
if (part.type === "text") {
|
|
|
pushText(acc, part.content)
|
|
pushText(acc, part.content)
|
|
|
|
|
+ acc.runningIndex += part.content.length
|
|
|
}
|
|
}
|
|
|
if (part.type !== "text") {
|
|
if (part.type !== "text") {
|
|
|
- acc.parts.push({ ...part })
|
|
|
|
|
|
|
+ acc.parts.push({ ...part, start: acc.runningIndex, end: acc.runningIndex + part.content.length })
|
|
|
|
|
+ acc.runningIndex += part.content.length
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const needsGap = Boolean(atMatch)
|
|
const needsGap = Boolean(atMatch)
|
|
|
const rest = needsGap ? (tail ? (/^\s/.test(tail) ? tail : ` ${tail}`) : " ") : tail
|
|
const rest = needsGap ? (tail ? (/^\s/.test(tail) ? tail : ` ${tail}`) : " ") : tail
|
|
|
pushText(acc, rest)
|
|
pushText(acc, rest)
|
|
|
|
|
+ acc.runningIndex += rest.length
|
|
|
|
|
|
|
|
const baseCursor = startIndex + part.content.length
|
|
const baseCursor = startIndex + part.content.length
|
|
|
const cursorAddition = needsGap && rest.length > 0 ? 1 : 0
|
|
const cursorAddition = needsGap && rest.length > 0 ? 1 : 0
|
|
|
acc.cursorPositionAfter = baseCursor + cursorAddition
|
|
acc.cursorPositionAfter = baseCursor + cursorAddition
|
|
|
|
|
|
|
|
acc.inserted = true
|
|
acc.inserted = true
|
|
|
- acc.runningIndex = nextIndex
|
|
|
|
|
return acc
|
|
return acc
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
@@ -237,9 +253,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
|
|
|
|
|
if (!inserted) {
|
|
if (!inserted) {
|
|
|
const baseParts = store.contentParts.filter((item) => !(item.type === "text" && item.content === ""))
|
|
const baseParts = store.contentParts.filter((item) => !(item.type === "text" && item.content === ""))
|
|
|
- const appendedAcc = { parts: [...baseParts] as ContentPart[] }
|
|
|
|
|
- if (part.type === "text") pushText(appendedAcc, part.content)
|
|
|
|
|
- if (part.type !== "text") appendedAcc.parts.push({ ...part })
|
|
|
|
|
|
|
+ const runningIndex = baseParts.reduce((sum, p) => sum + p.content.length, 0)
|
|
|
|
|
+ const appendedAcc = { parts: [...baseParts] as ContentPart[], runningIndex }
|
|
|
|
|
+ if (part.type === "text") {
|
|
|
|
|
+ pushText(appendedAcc, part.content)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (part.type !== "text") {
|
|
|
|
|
+ appendedAcc.parts.push({
|
|
|
|
|
+ ...part,
|
|
|
|
|
+ start: appendedAcc.runningIndex,
|
|
|
|
|
+ end: appendedAcc.runningIndex + part.content.length,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
const next = appendedAcc.parts.length > 0 ? appendedAcc.parts : defaultParts
|
|
const next = appendedAcc.parts.length > 0 ? appendedAcc.parts : defaultParts
|
|
|
setStore("contentParts", next)
|
|
setStore("contentParts", next)
|
|
|
setStore("popoverIsOpen", false)
|
|
setStore("popoverIsOpen", false)
|