mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-30 20:39:46 +02:00
improve chat @ mentions
This commit is contained in:
parent
5c433805f6
commit
baaec77451
2 changed files with 77 additions and 2 deletions
|
|
@ -1026,6 +1026,46 @@ export const PromptInputTextarea = ({
|
||||||
form?.requestSubmit();
|
form?.requestSubmit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle backspace to delete entire mention at once
|
||||||
|
if (e.key === "Backspace" && controller) {
|
||||||
|
const textarea = e.currentTarget;
|
||||||
|
const cursorPos = textarea.selectionStart;
|
||||||
|
const selectionEnd = textarea.selectionEnd;
|
||||||
|
|
||||||
|
// Only handle if no text is selected (cursor is at a single position)
|
||||||
|
if (cursorPos === selectionEnd) {
|
||||||
|
// Check if cursor is right after a mention
|
||||||
|
for (const label of mentionLabels) {
|
||||||
|
const mentionText = `@${label}`;
|
||||||
|
const startPos = cursorPos - mentionText.length;
|
||||||
|
if (startPos >= 0) {
|
||||||
|
const textBefore = currentValue.substring(startPos, cursorPos);
|
||||||
|
if (textBefore === mentionText) {
|
||||||
|
// Check if it's at word boundary (start of string or preceded by whitespace)
|
||||||
|
if (startPos === 0 || /\s/.test(currentValue[startPos - 1])) {
|
||||||
|
e.preventDefault();
|
||||||
|
const newText = currentValue.substring(0, startPos) + currentValue.substring(cursorPos);
|
||||||
|
controller.textInput.setInput(newText);
|
||||||
|
// Remove the mention from state
|
||||||
|
if (mentionsCtx) {
|
||||||
|
const mentionToRemove = mentionsCtx.mentions.find(m => m.displayName === label);
|
||||||
|
if (mentionToRemove) {
|
||||||
|
mentionsCtx.removeMention(mentionToRemove.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set cursor position after React updates
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.selectionStart = startPos;
|
||||||
|
textarea.selectionEnd = startPos;
|
||||||
|
}, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove last attachment when Backspace is pressed and textarea is empty
|
// Remove last attachment when Backspace is pressed and textarea is empty
|
||||||
if (
|
if (
|
||||||
e.key === "Backspace" &&
|
e.key === "Backspace" &&
|
||||||
|
|
@ -1094,7 +1134,7 @@ export const PromptInputTextarea = ({
|
||||||
segment.highlighted ? (
|
segment.highlighted ? (
|
||||||
<span
|
<span
|
||||||
key={`mention-${index}`}
|
key={`mention-${index}`}
|
||||||
className="rounded bg-primary/20 text-transparent ring-1 ring-primary/15 px-1 py-0.5 [box-decoration-break:clone]"
|
className="rounded bg-primary/20 text-transparent [box-decoration-break:clone] shadow-[inset_0_0_0_1px_hsl(var(--primary)/0.15),-3px_0_0_hsl(var(--primary)/0.2),3px_0_0_hsl(var(--primary)/0.2),0_-2px_0_hsl(var(--primary)/0.2),0_2px_0_hsl(var(--primary)/0.2)]"
|
||||||
>
|
>
|
||||||
{segment.text}
|
{segment.text}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,7 @@ export function ChatSidebar({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
// If mention popover is open, let it handle navigation keys
|
// If mention popover is open, let it handle navigation keys
|
||||||
if (activeMention && ['ArrowDown', 'ArrowUp', 'Tab', 'Escape'].includes(e.key)) {
|
if (activeMention && ['ArrowDown', 'ArrowUp', 'Tab', 'Escape'].includes(e.key)) {
|
||||||
return
|
return
|
||||||
|
|
@ -311,6 +311,41 @@ export function ChatSidebar({
|
||||||
handleSubmit()
|
handleSubmit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle backspace to delete entire mention at once
|
||||||
|
if (e.key === 'Backspace') {
|
||||||
|
const textarea = e.currentTarget
|
||||||
|
const cursorPos = textarea.selectionStart
|
||||||
|
const selectionEnd = textarea.selectionEnd
|
||||||
|
|
||||||
|
// Only handle if no text is selected (cursor is at a single position)
|
||||||
|
if (cursorPos !== selectionEnd) return
|
||||||
|
|
||||||
|
// Check if cursor is right after a mention
|
||||||
|
for (const label of mentionLabels) {
|
||||||
|
const mentionText = `@${label}`
|
||||||
|
const startPos = cursorPos - mentionText.length
|
||||||
|
if (startPos >= 0) {
|
||||||
|
const textBefore = message.substring(startPos, cursorPos)
|
||||||
|
if (textBefore === mentionText) {
|
||||||
|
// Check if it's at word boundary (start of string or preceded by whitespace)
|
||||||
|
if (startPos === 0 || /\s/.test(message[startPos - 1])) {
|
||||||
|
e.preventDefault()
|
||||||
|
const newText = message.substring(0, startPos) + message.substring(cursorPos)
|
||||||
|
onMessageChange(newText)
|
||||||
|
// Remove the mention from state
|
||||||
|
setMentions(prev => prev.filter(m => m.displayName !== label))
|
||||||
|
// Set cursor position after React updates
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.selectionStart = startPos
|
||||||
|
textarea.selectionEnd = startPos
|
||||||
|
}, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderConversationItem = (item: ConversationItem) => {
|
const renderConversationItem = (item: ConversationItem) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue