Files
kandev/apps/web/components/task/task-chat-input.tsx
T
2026-01-15 02:55:50 +00:00

186 lines
5.2 KiB
TypeScript

'use client';
import CodeMirror from '@uiw/react-codemirror';
import { markdown } from '@codemirror/lang-markdown';
import { EditorView, keymap, placeholder as cmPlaceholder } from '@codemirror/view';
import { autocompletion, closeBrackets, closeBracketsKeymap, CompletionContext } from '@codemirror/autocomplete';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { Prec } from '@codemirror/state';
import type { Extension } from '@codemirror/state';
import { cn } from '@/lib/utils';
import { SHORTCUTS } from '@/lib/keyboard/constants';
import { matchesShortcut, shortcutToCodeMirrorKeybinding } from '@/lib/keyboard/utils';
type TaskChatInputProps = {
value: string;
onChange: (value: string) => void;
onSubmit: () => void;
placeholder?: string;
disabled?: boolean;
className?: string;
planModeEnabled?: boolean;
};
export function TaskChatInput({
value,
onChange,
onSubmit,
placeholder,
disabled,
className,
planModeEnabled,
}: TaskChatInputProps) {
const submitKey = shortcutToCodeMirrorKeybinding(SHORTCUTS.SUBMIT);
const completionSource = (context: CompletionContext) => {
const slashMatch = context.matchBefore(/(^|\s)\/[\w-]*/);
if (slashMatch) {
const from = slashMatch.from + (slashMatch.text.startsWith('/') ? 0 : 1);
return {
from,
options: [
{ label: '/plan', type: 'slash', info: 'Start a planning response' },
{ label: '/todo', type: 'slash', info: 'Add a todo list' },
{ label: '/review', type: 'slash', info: 'Request a review' },
{ label: '/summarize', type: 'slash', info: 'Summarize the task' },
],
};
}
const mentionMatch = context.matchBefore(/(^|\s)@[\w-]*/);
if (mentionMatch) {
const from = mentionMatch.from + (mentionMatch.text.startsWith('@') ? 0 : 1);
return {
from,
options: [
{ label: '@agent', type: 'mention', info: 'Notify the agent' },
{ label: '@reviewer', type: 'mention', info: 'Tag a reviewer' },
{ label: '@designer', type: 'mention', info: 'Tag design feedback' },
],
};
}
return null;
};
const editorTheme = EditorView.theme({
'&': {
color: 'var(--foreground)',
fontSize: '0.875rem',
backgroundColor: 'transparent',
},
'.cm-editor': {
borderRadius: 'inherit',
background: 'transparent',
outline: 'none',
boxShadow: 'none',
overflow: 'hidden',
},
'.cm-editor.cm-focused': {
outline: 'none',
boxShadow: 'none',
},
'.cm-scroller': {
fontFamily: 'var(--font-sans)',
borderRadius: 'inherit',
background: 'transparent',
backgroundColor: 'transparent',
overflow: 'auto',
},
'.cm-content': {
padding: '0.5rem',
fontFamily: 'var(--font-sans)',
color: 'var(--foreground)',
background: 'transparent',
},
'.cm-line': {
color: 'var(--foreground)',
},
'.cm-placeholder': {
color: 'var(--muted-foreground)',
opacity: '0.8',
},
'.cm-tooltip-autocomplete, .cm-tooltip-autocomplete li, .cm-completionDetail': {
fontSize: '0.875rem',
},
'.cm-cursor': {
borderLeftColor: 'var(--foreground)',
},
'.cm-selectionBackground': {
backgroundColor: 'var(--primary)',
opacity: '0.7',
},
'.cm-selectionMatch': {
backgroundColor: 'var(--primary)',
opacity: '0.6',
},
'.cm-selectionLayer .cm-selectionBackground': {
backgroundColor: 'var(--primary)',
opacity: '0.7',
},
});
const handleSubmitKey = () => {
onSubmit();
return true;
};
const submitKeymap = Prec.highest(
keymap.of([
{ key: submitKey, run: handleSubmitKey, preventDefault: true },
{ key: 'Mod-Enter', run: handleSubmitKey, preventDefault: true },
])
);
const extensions: Extension[] = [
EditorView.lineWrapping,
editorTheme,
EditorView.domEventHandlers({
keydown: (event) => {
if (matchesShortcut(event, SHORTCUTS.SUBMIT)) {
event.preventDefault();
event.stopPropagation();
onSubmit();
return true;
}
return false;
},
}),
history(),
closeBrackets(),
markdown(),
cmPlaceholder(placeholder ?? ''),
autocompletion({ override: [completionSource], aboveCursor: true }),
submitKeymap,
keymap.of([...defaultKeymap, ...historyKeymap, ...closeBracketsKeymap]),
];
return (
<div
className={cn(
'task-chat-editor rounded-md border border-input bg-muted/40 focus-within:border-ring focus-within:ring-[2px] focus-within:ring-ring/30 overflow-hidden',
planModeEnabled &&
'border-dashed border-primary/60 !bg-primary/10 shadow-[inset_0_0_0_1px_rgba(59,130,246,0.35)]',
className
)}
>
<CodeMirror
value={value}
onChange={onChange}
editable={!disabled}
minHeight="90px"
maxHeight="200px"
extensions={extensions}
basicSetup={{
lineNumbers: false,
foldGutter: false,
highlightActiveLine: false,
highlightSelectionMatches: true,
autocompletion: false,
}}
className="text-sm"
/>
</div>
);
}