fixed cursor feature
This commit is contained in:
parent
60887682a5
commit
6f0585192a
4 changed files with 190 additions and 63 deletions
|
|
@ -74,6 +74,12 @@
|
||||||
color: var(--text-secondary, #666);
|
color: var(--text-secondary, #666);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dragHint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary, #999);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.fileItems {
|
.fileItems {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -84,8 +90,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 16px;
|
padding: 6px 16px;
|
||||||
cursor: pointer;
|
cursor: grab;
|
||||||
transition: background 0.1s;
|
transition: background 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,13 +99,32 @@
|
||||||
background: var(--hover-bg, #f5f5f5);
|
background: var(--hover-bg, #f5f5f5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileItemSelected {
|
.fileItem:active {
|
||||||
background: var(--selected-bg, #e8f0fe);
|
cursor: grabbing;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileCheckbox {
|
.dragHandle {
|
||||||
color: var(--primary-color, #4a90d9);
|
color: var(--text-secondary, #ccc);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileItem:hover .dragHandle {
|
||||||
|
color: var(--text-secondary, #999);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateGroup {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateGroupHeader {
|
||||||
|
padding: 6px 16px 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #999);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileIcon {
|
.fileIcon {
|
||||||
|
|
@ -161,6 +186,12 @@
|
||||||
background: var(--disabled-bg, #f5f5f5);
|
background: var(--disabled-bg, #f5f5f5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inputDropTarget {
|
||||||
|
border-color: var(--primary-color, #4a90d9);
|
||||||
|
background: var(--selected-bg, #e8f0fe);
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-color, #4a90d9) inset;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mode Toggle */
|
/* Mode Toggle */
|
||||||
.modeToggle {
|
.modeToggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* Main page for the CodeEditor feature.
|
* Main page for the CodeEditor feature.
|
||||||
* Three-panel layout: FileList (left) | Chat (center) | DiffPreview (right)
|
* Three-panel layout: FileList (left) | Chat (center) | DiffPreview (right)
|
||||||
* Supports simple mode (Phase 1) and agent mode (Phase 2).
|
* Files are dragged from FileList into the prompt textarea as @fileName references.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
|
|
@ -22,11 +22,10 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [mode, setMode] = useState<'simple' | 'agent'>('simple');
|
const [mode, setMode] = useState<'simple' | 'agent'>('simple');
|
||||||
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
selectedFileIds,
|
|
||||||
toggleFileSelection,
|
|
||||||
pendingEdits,
|
pendingEdits,
|
||||||
acceptEdit,
|
acceptEdit,
|
||||||
rejectEdit,
|
rejectEdit,
|
||||||
|
|
@ -64,9 +63,9 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const trimmed = inputValue.trim();
|
const trimmed = inputValue.trim();
|
||||||
if (!trimmed || isProcessing) return;
|
if (!trimmed || isProcessing) return;
|
||||||
sendMessage(trimmed, selectedFileIds, mode);
|
sendMessage(trimmed, mode);
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
}, [inputValue, isProcessing, sendMessage, selectedFileIds, mode]);
|
}, [inputValue, isProcessing, sendMessage, mode]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
|
@ -75,6 +74,50 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [handleSubmit]);
|
}, [handleSubmit]);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||||
|
if (e.dataTransfer.types.includes('application/x-codeeditor-file')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
setIsDragOver(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragLeave = useCallback(() => {
|
||||||
|
setIsDragOver(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragOver(false);
|
||||||
|
|
||||||
|
const fileDataStr = e.dataTransfer.getData('application/x-codeeditor-file');
|
||||||
|
if (!fileDataStr) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileData = JSON.parse(fileDataStr);
|
||||||
|
const tag = `@${fileData.fileName}`;
|
||||||
|
const textarea = inputRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
const pos = textarea.selectionStart || inputValue.length;
|
||||||
|
const before = inputValue.slice(0, pos);
|
||||||
|
const after = inputValue.slice(pos);
|
||||||
|
const spaceBefore = before.length > 0 && !before.endsWith(' ') && !before.endsWith('\n') ? ' ' : '';
|
||||||
|
const spaceAfter = after.length > 0 && !after.startsWith(' ') && !after.startsWith('\n') ? ' ' : '';
|
||||||
|
const newValue = `${before}${spaceBefore}${tag}${spaceAfter}${after}`;
|
||||||
|
setInputValue(newValue);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const newPos = pos + spaceBefore.length + tag.length + spaceAfter.length;
|
||||||
|
textarea.focus();
|
||||||
|
textarea.setSelectionRange(newPos, newPos);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setInputValue(prev => prev + (prev && !prev.endsWith(' ') ? ' ' : '') + tag + ' ');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore malformed drop data
|
||||||
|
}
|
||||||
|
}, [inputValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -83,11 +126,7 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
>
|
>
|
||||||
{/* Left: File List */}
|
{/* Left: File List */}
|
||||||
<div className={styles.filePanel} style={{ width: `${fileListWidth}%` }}>
|
<div className={styles.filePanel} style={{ width: `${fileListWidth}%` }}>
|
||||||
<FileListPanel
|
<FileListPanel files={files} />
|
||||||
files={files}
|
|
||||||
selectedFileIds={selectedFileIds}
|
|
||||||
onToggle={toggleFileSelection}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.divider} onMouseDown={fileListMouseDown} />
|
<div className={styles.divider} onMouseDown={fileListMouseDown} />
|
||||||
|
|
@ -116,7 +155,7 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
className={`${styles.modeButton} ${mode === 'simple' ? styles.modeActive : ''}`}
|
className={`${styles.modeButton} ${mode === 'simple' ? styles.modeActive : ''}`}
|
||||||
onClick={() => setMode('simple')}
|
onClick={() => setMode('simple')}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
title="Single AI call with selected files as context"
|
title="Single AI call -- drag files into prompt as @references"
|
||||||
>
|
>
|
||||||
<FaEdit /> Simple
|
<FaEdit /> Simple
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -132,21 +171,24 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={styles.input}
|
className={`${styles.input} ${isDragOver ? styles.inputDropTarget : ''}`}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
placeholder={mode === 'agent'
|
placeholder={mode === 'agent'
|
||||||
? "Describe a complex task (e.g. 'Document all Python files')..."
|
? "Describe a complex task (e.g. 'Document all Python files')..."
|
||||||
: "Describe what you want to change..."
|
: "Drag files here and describe changes (e.g. 'In @config.yaml change the port to 8080')..."
|
||||||
}
|
}
|
||||||
rows={2}
|
rows={3}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
/>
|
/>
|
||||||
<div className={styles.inputActions}>
|
<div className={styles.inputActions}>
|
||||||
<span className={styles.fileCount}>
|
<span className={styles.fileCount}>
|
||||||
{mode === 'simple'
|
{mode === 'simple'
|
||||||
? `${selectedFileIds.length} file(s) selected`
|
? `Drag files from the list into your prompt`
|
||||||
: `Agent mode: AI reads files autonomously`
|
: `Agent mode: AI reads files autonomously`
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* FileListPanel
|
* FileListPanel
|
||||||
*
|
*
|
||||||
* Lists uploaded text files with checkbox selection.
|
* Lists text files grouped by date, draggable into the prompt textarea.
|
||||||
* Selected files are sent as context to the AI.
|
* Drag a file into the chat input to insert an @fileName reference.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { FaFile, FaCheckSquare, FaRegSquare } from 'react-icons/fa';
|
import { FaFile, FaGripVertical } from 'react-icons/fa';
|
||||||
import styles from './CodeEditor.module.css';
|
import styles from './CodeEditor.module.css';
|
||||||
|
|
||||||
export interface FileInfo {
|
export interface FileInfo {
|
||||||
|
|
@ -14,48 +14,95 @@ export interface FileInfo {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
sizeBytes: number;
|
sizeBytes: number;
|
||||||
|
modifiedAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileListPanelProps {
|
interface FileListPanelProps {
|
||||||
files: FileInfo[];
|
files: FileInfo[];
|
||||||
selectedFileIds: string[];
|
|
||||||
onToggle: (fileId: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileListPanel: React.FC<FileListPanelProps> = ({ files, selectedFileIds, onToggle }) => {
|
interface DateGroup {
|
||||||
|
label: string;
|
||||||
|
files: FileInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileListPanel: React.FC<FileListPanelProps> = ({ files }) => {
|
||||||
|
const groups = useMemo(() => _groupByDate(files), [files]);
|
||||||
|
|
||||||
|
const handleDragStart = (e: React.DragEvent, file: FileInfo) => {
|
||||||
|
e.dataTransfer.setData('application/x-codeeditor-file', JSON.stringify({
|
||||||
|
fileId: file.fileId,
|
||||||
|
fileName: file.fileName,
|
||||||
|
}));
|
||||||
|
e.dataTransfer.setData('text/plain', `@${file.fileName}`);
|
||||||
|
e.dataTransfer.effectAllowed = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.fileList}>
|
<div className={styles.fileList}>
|
||||||
<div className={styles.panelHeader}>
|
<div className={styles.panelHeader}>
|
||||||
<h3>Files ({files.length})</h3>
|
<h3>Files ({files.length})</h3>
|
||||||
<span className={styles.selectedCount}>{selectedFileIds.length} selected</span>
|
<span className={styles.dragHint}>drag into prompt</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.fileItems}>
|
<div className={styles.fileItems}>
|
||||||
{files.length === 0 ? (
|
{files.length === 0 ? (
|
||||||
<div className={styles.emptyState}>No text files uploaded yet</div>
|
<div className={styles.emptyState}>No text files uploaded yet</div>
|
||||||
) : (
|
) : (
|
||||||
files.map((file) => {
|
groups.map((group) => (
|
||||||
const isSelected = selectedFileIds.includes(file.fileId);
|
<div key={group.label} className={styles.dateGroup}>
|
||||||
return (
|
<div className={styles.dateGroupHeader}>{group.label}</div>
|
||||||
|
{group.files.map((file) => (
|
||||||
<div
|
<div
|
||||||
key={file.fileId}
|
key={file.fileId}
|
||||||
className={`${styles.fileItem} ${isSelected ? styles.fileItemSelected : ''}`}
|
className={styles.fileItem}
|
||||||
onClick={() => onToggle(file.fileId)}
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, file)}
|
||||||
>
|
>
|
||||||
<span className={styles.fileCheckbox}>
|
<FaGripVertical className={styles.dragHandle} />
|
||||||
{isSelected ? <FaCheckSquare /> : <FaRegSquare />}
|
|
||||||
</span>
|
|
||||||
<FaFile className={styles.fileIcon} />
|
<FaFile className={styles.fileIcon} />
|
||||||
<span className={styles.fileName}>{file.fileName}</span>
|
<span className={styles.fileName}>{file.fileName}</span>
|
||||||
<span className={styles.fileSize}>{_formatSize(file.sizeBytes)}</span>
|
<span className={styles.fileSize}>{_formatSize(file.sizeBytes)}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
})
|
</div>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function _groupByDate(files: FileInfo[]): DateGroup[] {
|
||||||
|
const now = new Date();
|
||||||
|
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime() / 1000;
|
||||||
|
const yesterdayStart = todayStart - 86400;
|
||||||
|
|
||||||
|
const today: FileInfo[] = [];
|
||||||
|
const yesterday: FileInfo[] = [];
|
||||||
|
const older: FileInfo[] = [];
|
||||||
|
|
||||||
|
const sorted = [...files].sort((a, b) => (b.modifiedAt || 0) - (a.modifiedAt || 0));
|
||||||
|
|
||||||
|
for (const file of sorted) {
|
||||||
|
const ts = file.modifiedAt || 0;
|
||||||
|
if (ts >= todayStart) {
|
||||||
|
today.push(file);
|
||||||
|
} else if (ts >= yesterdayStart) {
|
||||||
|
yesterday.push(file);
|
||||||
|
} else {
|
||||||
|
older.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: DateGroup[] = [];
|
||||||
|
if (today.length > 0) groups.push({ label: 'Today', files: today });
|
||||||
|
if (yesterday.length > 0) groups.push({ label: 'Yesterday', files: yesterday });
|
||||||
|
if (older.length > 0) groups.push({ label: 'Older', files: older });
|
||||||
|
if (groups.length === 0 && files.length > 0) groups.push({ label: 'All files', files: sorted });
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
function _formatSize(bytes: number): string {
|
function _formatSize(bytes: number): string {
|
||||||
if (bytes < 1024) return `${bytes} B`;
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* useCodeEditor Hook
|
* useCodeEditor Hook
|
||||||
*
|
*
|
||||||
* Manages SSE connection, file selection, message state, edit proposals,
|
* Manages SSE connection, message state, edit proposals, and agent progress.
|
||||||
* and agent mode progress for the CodeEditor feature.
|
* File references are extracted from @fileName tags in the prompt text.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
|
@ -21,13 +21,11 @@ export interface AgentProgress {
|
||||||
|
|
||||||
interface UseCodeEditorReturn {
|
interface UseCodeEditorReturn {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
selectedFileIds: string[];
|
|
||||||
toggleFileSelection: (fileId: string) => void;
|
|
||||||
pendingEdits: FileEditProposal[];
|
pendingEdits: FileEditProposal[];
|
||||||
acceptEdit: (editId: string) => void;
|
acceptEdit: (editId: string) => void;
|
||||||
rejectEdit: (editId: string) => void;
|
rejectEdit: (editId: string) => void;
|
||||||
isProcessing: boolean;
|
isProcessing: boolean;
|
||||||
sendMessage: (prompt: string, fileIds: string[], mode?: 'simple' | 'agent') => void;
|
sendMessage: (prompt: string, mode?: 'simple' | 'agent') => void;
|
||||||
stopProcessing: () => void;
|
stopProcessing: () => void;
|
||||||
files: FileInfo[];
|
files: FileInfo[];
|
||||||
agentProgress: AgentProgress | null;
|
agentProgress: AgentProgress | null;
|
||||||
|
|
@ -35,7 +33,6 @@ interface UseCodeEditorReturn {
|
||||||
|
|
||||||
export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
|
||||||
const [pendingEdits, setPendingEdits] = useState<FileEditProposal[]>([]);
|
const [pendingEdits, setPendingEdits] = useState<FileEditProposal[]>([]);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [files, setFiles] = useState<FileInfo[]>([]);
|
const [files, setFiles] = useState<FileInfo[]>([]);
|
||||||
|
|
@ -48,15 +45,11 @@ export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
_loadFiles(instanceId, setFiles);
|
_loadFiles(instanceId, setFiles);
|
||||||
}, [instanceId]);
|
}, [instanceId]);
|
||||||
|
|
||||||
const toggleFileSelection = useCallback((fileId: string) => {
|
const sendMessage = useCallback((prompt: string, mode: 'simple' | 'agent' = 'simple') => {
|
||||||
setSelectedFileIds(prev =>
|
|
||||||
prev.includes(fileId) ? prev.filter(id => id !== fileId) : [...prev, fileId]
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sendMessage = useCallback((prompt: string, fileIds: string[], mode: 'simple' | 'agent' = 'simple') => {
|
|
||||||
if (!instanceId || isProcessing) return;
|
if (!instanceId || isProcessing) return;
|
||||||
|
|
||||||
|
const referencedFileIds = _extractFileRefs(prompt, files);
|
||||||
|
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setAgentProgress(null);
|
setAgentProgress(null);
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
|
|
@ -96,7 +89,7 @@ export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
listFileId: fileIds,
|
listFileId: referencedFileIds,
|
||||||
}),
|
}),
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
signal: abortRef.current.signal,
|
signal: abortRef.current.signal,
|
||||||
|
|
@ -150,7 +143,7 @@ export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
}]);
|
}]);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
});
|
});
|
||||||
}, [instanceId, isProcessing, workflowId]);
|
}, [instanceId, isProcessing, workflowId, files]);
|
||||||
|
|
||||||
const stopProcessing = useCallback(() => {
|
const stopProcessing = useCallback(() => {
|
||||||
if (abortRef.current) {
|
if (abortRef.current) {
|
||||||
|
|
@ -185,8 +178,6 @@ export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
selectedFileIds,
|
|
||||||
toggleFileSelection,
|
|
||||||
pendingEdits,
|
pendingEdits,
|
||||||
acceptEdit,
|
acceptEdit,
|
||||||
rejectEdit,
|
rejectEdit,
|
||||||
|
|
@ -204,6 +195,22 @@ function _loadFiles(instanceId: string, setFiles: React.Dispatch<React.SetStateA
|
||||||
.catch(err => console.error('Failed to load files:', err));
|
.catch(err => console.error('Failed to load files:', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _extractFileRefs(prompt: string, files: FileInfo[]): string[] {
|
||||||
|
const atPattern = /@([\w.\-]+)/g;
|
||||||
|
const matchedIds: string[] = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = atPattern.exec(prompt)) !== null) {
|
||||||
|
const refName = match[1];
|
||||||
|
const file = files.find(f => f.fileName === refName || f.fileName.toLowerCase() === refName.toLowerCase());
|
||||||
|
if (file && !matchedIds.includes(file.fileId)) {
|
||||||
|
matchedIds.push(file.fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedIds;
|
||||||
|
}
|
||||||
|
|
||||||
function _handleSseEvent(
|
function _handleSseEvent(
|
||||||
event: any,
|
event: any,
|
||||||
setMessages: React.Dispatch<React.SetStateAction<Message[]>>,
|
setMessages: React.Dispatch<React.SetStateAction<Message[]>>,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue