Skip to content

Commit beb66f5

Browse files
committed
inline suggestions feature
Signed-off-by: Akshat Batra <[email protected]>
1 parent 2f9da66 commit beb66f5

File tree

8 files changed

+286
-38
lines changed

8 files changed

+286
-38
lines changed

src/ai-assistant/autocompletion.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import * as monaco from 'monaco-editor';
2+
import { sendMessage } from './chatRelay';
3+
import useAppStore from '../store/store';
4+
import { editorsContent } from '../types/components/AIAssistant.types';
5+
6+
let completionTimeout: NodeJS.Timeout | null = null;
7+
let lastRequestTime: number = 0;
8+
let isProcessing: boolean = false;
9+
10+
export const registerAutocompletion = (
11+
language: 'concerto' | 'markdown' | 'json',
12+
monacoInstance: typeof monaco
13+
) => {
14+
try {
15+
const provider = monacoInstance.languages.registerInlineCompletionsProvider(language, {
16+
provideInlineCompletions: async (model, position, _context, token) => {
17+
if (token.isCancellationRequested) {
18+
return { items: [] };
19+
}
20+
21+
const { aiConfig } = useAppStore.getState();
22+
const enableInlineSuggestions = aiConfig?.enableInlineSuggestions !== false;
23+
if (!enableInlineSuggestions) {
24+
return { items: [] };
25+
}
26+
27+
const currentTime = Date.now();
28+
29+
if (completionTimeout) {
30+
clearTimeout(completionTimeout);
31+
}
32+
33+
if (isProcessing || (currentTime - lastRequestTime < 2000)) {
34+
return { items: [] };
35+
}
36+
37+
isProcessing = true;
38+
lastRequestTime = currentTime;
39+
40+
try {
41+
const result = await getInlineCompletions(model, position, language, monacoInstance);
42+
return result;
43+
} finally {
44+
45+
isProcessing = false;
46+
}
47+
},
48+
freeInlineCompletions: (_completions) => {
49+
},
50+
});
51+
return provider;
52+
} catch (error) {
53+
console.error('Error registering completion provider:', error);
54+
return null;
55+
}
56+
};
57+
58+
const getInlineCompletions = async (
59+
model: monaco.editor.ITextModel,
60+
position: monaco.Position,
61+
language: 'concerto' | 'markdown' | 'json',
62+
monacoInstance: typeof monaco
63+
): Promise<{ items: monaco.languages.InlineCompletion[] }> => {
64+
const { aiConfig } = useAppStore.getState();
65+
66+
if (!aiConfig) {
67+
return { items: [] };
68+
}
69+
70+
const lineContent = model.getLineContent(position.lineNumber);
71+
const textBeforeCursor = lineContent.substring(0, position.column - 1);
72+
const textAfterCursor = lineContent.substring(position.column - 1);
73+
74+
if (!textBeforeCursor.trim() || textBeforeCursor.length < 2) {
75+
return {
76+
items: []
77+
};
78+
}
79+
80+
const startLine = Math.max(1, position.lineNumber - 20);
81+
const endLine = Math.min(model.getLineCount(), position.lineNumber + 20);
82+
const contextLines: string[] = [];
83+
84+
for (let i = startLine; i <= endLine; i++) {
85+
if (i === position.lineNumber) {
86+
const fullCurrentLine = textBeforeCursor + '<CURSOR>' + textAfterCursor;
87+
contextLines.push(fullCurrentLine);
88+
} else if (i < position.lineNumber) {
89+
contextLines.push(model.getLineContent(i));
90+
} else {
91+
contextLines.push(model.getLineContent(i));
92+
}
93+
}
94+
95+
const contextText = contextLines.join('\n');
96+
97+
const editorsContent: editorsContent = {
98+
editorTemplateMark: useAppStore.getState().editorValue,
99+
editorModelCto: useAppStore.getState().editorModelCto,
100+
editorAgreementData: useAppStore.getState().editorAgreementData,
101+
};
102+
const prompt = `Current context:\n${contextText}`;
103+
104+
try {
105+
let completion = '';
106+
107+
await sendMessage(
108+
prompt,
109+
'inlineSuggestion',
110+
editorsContent,
111+
false,
112+
language,
113+
(chunk) => {
114+
completion += chunk;
115+
},
116+
(error) => {
117+
console.error('Autocompletion error:', error);
118+
}
119+
);
120+
121+
completion = completion.trim();
122+
123+
completion = completion.replace(/^```[\s\S]*?\n/, '').replace(/\n```$/, '');
124+
completion = completion.replace(/^`/, '').replace(/`$/, '');
125+
126+
if (!completion || completion.length < 2) {
127+
return { items: [] };
128+
}
129+
130+
const inlineCompletion: monaco.languages.InlineCompletion = {
131+
insertText: completion,
132+
range: new monacoInstance.Range(
133+
position.lineNumber,
134+
position.column,
135+
position.lineNumber,
136+
position.column
137+
),
138+
filterText: textBeforeCursor,
139+
};
140+
141+
return {
142+
items: [inlineCompletion],
143+
};
144+
} catch (error) {
145+
console.error('Error getting AI completion:', error);
146+
return { items: [] };
147+
}
148+
};

src/ai-assistant/chatRelay.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export const loadConfigFromLocalStorage = () => {
1818
const savedIncludeData = localStorage.getItem('aiIncludeData') === 'true';
1919

2020
const savedShowFullPrompt = localStorage.getItem('aiShowFullPrompt') === 'true';
21-
const savedEnableCodeSelectionMenu = localStorage.getItem('aiEnableCodeSelectionMenu') === 'true';
21+
const savedEnableCodeSelectionMenu = localStorage.getItem('aiEnableCodeSelectionMenu') !== 'false';
22+
const savedEnableInlineSuggestions = localStorage.getItem('aiEnableInlineSuggestions') !== 'false';
2223

2324
if (savedProvider && savedModel && savedApiKey) {
2425
const config: AIConfig = {
@@ -30,6 +31,7 @@ export const loadConfigFromLocalStorage = () => {
3031
includeDataContent: savedIncludeData,
3132
showFullPrompt: savedShowFullPrompt,
3233
enableCodeSelectionMenu: savedEnableCodeSelectionMenu,
34+
enableInlineSuggestions: savedEnableInlineSuggestions,
3335
};
3436

3537
if (savedCustomEndpoint && savedProvider === 'openai-compatible') {
@@ -132,6 +134,8 @@ export const sendMessage = async (
132134
systemPrompt = prepareSystemPrompt.createConcertoModel(editorsContent, aiConfig);
133135
} else if (promptPreset === "explainCode") {
134136
systemPrompt = prepareSystemPrompt.explainCode(editorsContent, aiConfig, editorType);
137+
} else if (promptPreset === "inlineSuggestion") {
138+
systemPrompt = prepareSystemPrompt.inlineSuggestion(editorsContent, aiConfig, editorType);
135139
} else {
136140
systemPrompt = prepareSystemPrompt.default(editorsContent, aiConfig);
137141
}

src/ai-assistant/prompts.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ export const prepareSystemPrompt = {
5151
return includeEditorContents(prompt, aiConfig, editorsContent);
5252
},
5353

54+
inlineSuggestion: (editorsContent: editorsContent, aiConfig?: any, editorType?: 'markdown' | 'concerto' | 'json') => {
55+
let prompt = `You are a helpful assistant that provides inline code completion suggestions for Accord Project
56+
${editorType === 'markdown' ? 'TemplateMark' : editorType} code. You should only suggest valid code that will
57+
compile. For instance, while making a suggestion in TemplateMark you must make sure that it conforms with
58+
provided (if any) Concerto model/JSON data.
59+
60+
IMPORTANT: Your response will be directly used for inline suggestions in the code editor.
61+
- <CURSOR> represents the current cursor position in the editor
62+
- Return ONLY the code completion text that should be inserted at the cursor position.
63+
- Do NOT include any explanations, markdown formatting, backticks, or additional text.
64+
- Do NOT repeat the existing code that's already in the editor.
65+
- Do NOT suggest replacement for existing code after the cursor, just addition.
66+
- Provide only the logical continuation from the cursor position.
67+
- If no meaningful completion can be suggested, return an empty response.
68+
- Responses should ideally not be very long unless the situation demands it.
69+
- Focus on syntactically correct and contextually appropriate completions\n\n`;
70+
return includeEditorContents(prompt, aiConfig, editorsContent);
71+
},
72+
5473
default: (editorsContent: editorsContent, aiConfig?: any) => {
5574
let prompt = `You are a helpful assistant that answers questions about open source Accord Project. You assist the user
5675
to work with TemplateMark, Concerto models and JSON data. Code blocks returned by you should enclosed in backticks\n\n`;

src/components/AIConfigPopup.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const AIConfigPopup = ({ isOpen, onClose, onSave }: AIConfigPopupProps) => {
1111

1212
const [showFullPrompt, setShowFullPrompt] = useState<boolean>(false);
1313
const [enableCodeSelectionMenu, setEnableCodeSelectionMenu] = useState<boolean>(true);
14+
const [enableInlineSuggestions, setEnableInlineSuggestions] = useState<boolean>(true);
1415

1516
useEffect(() => {
1617
if (isOpen) {
@@ -22,6 +23,7 @@ const AIConfigPopup = ({ isOpen, onClose, onSave }: AIConfigPopupProps) => {
2223

2324
const savedShowFullPrompt = localStorage.getItem('aiShowFullPrompt') === 'true';
2425
const savedEnableCodeSelection = localStorage.getItem('aiEnableCodeSelectionMenu') !== 'false';
26+
const savedEnableInlineSuggestions = localStorage.getItem('aiEnableInlineSuggestions') !== 'false';
2527

2628
if (savedProvider) setProvider(savedProvider);
2729
if (savedModel) setModel(savedModel);
@@ -31,6 +33,7 @@ const AIConfigPopup = ({ isOpen, onClose, onSave }: AIConfigPopupProps) => {
3133

3234
setShowFullPrompt(savedShowFullPrompt);
3335
setEnableCodeSelectionMenu(savedEnableCodeSelection);
36+
setEnableInlineSuggestions(savedEnableInlineSuggestions);
3437
}
3538
}, [isOpen]);
3639

@@ -53,6 +56,7 @@ const AIConfigPopup = ({ isOpen, onClose, onSave }: AIConfigPopupProps) => {
5356

5457
localStorage.setItem('aiShowFullPrompt', showFullPrompt.toString());
5558
localStorage.setItem('aiEnableCodeSelectionMenu', enableCodeSelectionMenu.toString());
59+
localStorage.setItem('aiEnableInlineSuggestions', enableInlineSuggestions.toString());
5660

5761
onSave();
5862
onClose();
@@ -235,6 +239,22 @@ const AIConfigPopup = ({ isOpen, onClose, onSave }: AIConfigPopupProps) => {
235239
<div className="mt-1 text-xs text-gray-500">
236240
When enabled, selecting code in editors will show a menu with "Explain" and "Chat" options
237241
</div>
242+
243+
<div className="flex items-center">
244+
<input
245+
type="checkbox"
246+
id="enableInlineSuggestions"
247+
checked={enableInlineSuggestions}
248+
onChange={(e) => setEnableInlineSuggestions(e.target.checked)}
249+
className="h-4 w-4 text-blue-500 rounded focus:ring-blue-400"
250+
/>
251+
<label htmlFor="enableInlineSuggestions" className="ml-2 text-sm text-gray-700">
252+
Enable AI Inline Suggestions
253+
</label>
254+
</div>
255+
<div className="mt-1 text-xs text-gray-500">
256+
When enabled, AI will provide ghost text suggestions as you type in the editors
257+
</div>
238258
</div>
239259
)}
240260
</div>

src/editors/ConcertoEditor.tsx

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { lazy, Suspense, useCallback, useEffect, useMemo } from "react";
33
import * as monaco from "monaco-editor";
44
import useAppStore from "../store/store";
55
import { useCodeSelection } from "../components/CodeSelectionMenu";
6+
import { registerAutocompletion } from "../ai-assistant/autocompletion";
67

78
const MonacoEditor = lazy(() =>
89
import("@monaco-editor/react").then((mod) => ({ default: mod.Editor }))
@@ -103,23 +104,6 @@ const handleEditorWillMount = (monacoInstance: typeof monaco) => {
103104
],
104105
},
105106
});
106-
107-
monacoInstance.editor.defineTheme("concertoTheme", {
108-
base: "vs",
109-
inherit: true,
110-
rules: [
111-
{ token: "keyword", foreground: "cd2184" },
112-
{ token: "type", foreground: "008080" },
113-
{ token: "identifier", foreground: "000000" },
114-
{ token: "string", foreground: "008000" },
115-
{ token: "string.escape", foreground: "800000" },
116-
{ token: "comment", foreground: "808080" },
117-
{ token: "white", foreground: "FFFFFF" },
118-
],
119-
colors: {},
120-
});
121-
122-
monacoInstance.editor.setTheme("concertoTheme");
123107
};
124108

125109
interface ConcertoEditorProps {
@@ -135,27 +119,48 @@ export default function ConcertoEditor({
135119
const monacoInstance = useMonaco();
136120
const error = useAppStore((state) => state.error);
137121
const backgroundColor = useAppStore((state) => state.backgroundColor);
122+
const aiConfig = useAppStore((state) => state.aiConfig);
138123
const ctoErr = error?.startsWith("c:") ? error : undefined;
139124

140125
const themeName = useMemo(
141126
() => (backgroundColor ? "darkTheme" : "lightTheme"),
142127
[backgroundColor]
143128
);
144129

145-
const options: monaco.editor.IStandaloneEditorConstructionOptions = {
130+
const options: monaco.editor.IStandaloneEditorConstructionOptions = useMemo(() => ({
146131
minimap: { enabled: false },
147132
wordWrap: "on",
148133
automaticLayout: true,
149134
scrollBeyondLastLine: false,
150135
autoClosingBrackets: "languageDefined",
151136
autoSurround: "languageDefined",
152137
bracketPairColorization: { enabled: true },
153-
};
154-
155-
const handleEditorDidMount = (editor: any) => {
138+
inlineSuggest: {
139+
enabled: aiConfig?.enableInlineSuggestions !== false,
140+
mode: "prefix",
141+
suppressSuggestions: false,
142+
fontFamily: "inherit",
143+
keepOnBlur: true,
144+
},
145+
suggest: {
146+
preview: true,
147+
showInlineDetails: true,
148+
},
149+
quickSuggestions: false,
150+
suggestOnTriggerCharacters: false,
151+
acceptSuggestionOnCommitCharacter: false,
152+
acceptSuggestionOnEnter: "off",
153+
tabCompletion: "off",
154+
}), [aiConfig?.enableInlineSuggestions]);
155+
156+
const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
156157
editor.onDidChangeCursorSelection(() => {
157158
handleSelection(editor);
158159
});
160+
161+
if (monacoInstance) {
162+
registerAutocompletion('concerto', monacoInstance);
163+
}
159164
};
160165

161166
const handleChange = useCallback(

0 commit comments

Comments
 (0)