autocomplete UX

This commit is contained in:
mp 2024-11-21 05:58:43 -08:00
parent cd77542a9e
commit 53d19d819c
7 changed files with 212 additions and 54 deletions

View file

@ -73,22 +73,23 @@ const z = 3
export const getFIMPrompt: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
// if no prefix or suffix, return empty string
if (!fimInfo.prefix.trim() && !fimInfo.suffix.trim()) return ''
const { prefix: fullPrefix, suffix: fullSuffix } = fimInfo
const prefix = fullPrefix.split('\n').slice(-20).join('\n')
const suffix = fullSuffix.split('\n').slice(0, 20).join('\n')
// instruct model to generate a single line if there is text immediately after the cursor
const suffixLines = fimInfo.suffix.split('\n');
const afterCursor = suffixLines[0] || '';
const generateSingleLine = afterCursor.trim().length > 0;
const singleLinePrompt = generateSingleLine ? `Please produce a single line of code that fills in the middle.` : ''
console.log('prefix', JSON.stringify(prefix))
console.log('suffix', JSON.stringify(suffix))
if (!prefix.trim() && !suffix.trim()) return ''
// TODO may want to trim the prefix and suffix
switch (voidConfig.default.whichApi) {
case 'ollama':
if (voidConfig.ollama.model === 'codestral') {
return `${singleLinePrompt}[SUFFIX]${fimInfo.suffix}[PREFIX] ${fimInfo.prefix}`
return `[SUFFIX]${suffix}[PREFIX] ${prefix}`
} else if (voidConfig.ollama.model.includes('qwen')) {
return `${singleLinePrompt}<|fim_prefix|>${fimInfo.prefix}<|fim_suffix|>${fimInfo.suffix}<|fim_middle|>`
return `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>`
}
return ''
case 'anthropic':
@ -101,14 +102,13 @@ export const getFIMPrompt: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
default:
return `## START:
\`\`\`
${fimInfo.prefix}
${prefix}
\`\`\`
## END:
\`\`\`
${fimInfo.suffix}
${suffix}
\`\`\`
`
}
}

View file

@ -5,7 +5,7 @@ import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeA
import { VoidConfig } from '../webviews/common/contextForConfig'
import { getFIMPrompt, getFIMSystem } from './getPrompt';
export type AbortRef = { current: (() => void) | null }
export type AbortRef = { current: (() => void) }
export type OnText = (newText: string, fullText: string) => void
@ -21,9 +21,12 @@ export type LLMMessage = {
content: string,
}
type LLMMessageOptions = { stopTokens?: string[] }
type SendLLMMessageFnTypeInternal = (params: {
mode: 'chat' | 'fim',
messages: LLMMessage[],
options?: LLMMessageOptions,
onText: OnText,
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
@ -34,8 +37,9 @@ type SendLLMMessageFnTypeInternal = (params: {
type SendLLMMessageFnTypeExternal = (params: (
| { mode?: 'chat', messages: LLMMessage[], fimInfo?: undefined, }
| { mode: 'fim', fimInfo: FimInfo, messages?: undefined, }
| { mode: 'fim', messages?: undefined, fimInfo: FimInfo, }
) & {
options?: LLMMessageOptions,
onText: OnText,
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
@ -242,7 +246,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ""
@ -278,6 +282,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ mode, messages, on
prompt: prompt,
stream: true,
raw: true,
options: { stop: options?.stopTokens }
})
}
@ -293,6 +298,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ mode, messages, on
abortRef.current = () => {
didAbort = true
stream.abort()
}
for await (const chunk of stream) {
if (didAbort) return;
@ -386,7 +392,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ mode, messages, fimInfo, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ options, mode, messages, fimInfo, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
if (!voidConfig)
return onError('No config file found for LLM.');
@ -406,27 +412,29 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ mode, messages, f
{ role: 'system', content: system },
{ role: 'user', content: prompt }
] as const)
.filter(m => m.content.trim() !== '')
}
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
.filter(m => m.content !== '')
if (messages.length === 0)
return onError('No messages provided to LLM.');
switch (voidConfig.default.whichApi) {
case 'anthropic':
return sendAnthropicMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
return sendAnthropicMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'openAI':
case 'openRouter':
case 'openAICompatible':
return sendOpenAIMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
return sendOpenAIMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'gemini':
return sendGeminiMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
return sendGeminiMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'ollama':
return sendOllamaMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
return sendOllamaMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'greptile':
return sendGreptileMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
return sendGreptileMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
default:
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
}

View file

@ -3,6 +3,47 @@ import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage';
import { getVoidConfigFromPartial, VoidConfig } from '../webviews/common/contextForConfig';
import { LRUCache } from 'lru-cache';
/*
A summary of autotab:
Postprocessing
-one common problem for all models is outputting unbalanced parentheses
we solve this by trimming all extra closing parentheses from the generated string
in future, should make sure parentheses are always balanced
-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()"
we complete up to first matchup character
but should instead complete the whole line / block (difficult because of parenthesis accuracy)
-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards
this should happen automatically with caching system
should break preloaded responses into \n\n chunks
Preprocessing
- we don't generate if cursor is at end / beginning of a line (no spaces)
- we generate 1 line if there is text to the right of cursor
- we generate 1 line if variable declaration
- (in many cases want to show 1 line but generate multiple)
State
- cache based on prefix (and do some trimming first)
- when press tab on one line, should have an immediate followup response
to do this, show autocompletes before they're fully finished
- [todo] remove each autotab when accepted
- [todo] treat windows \r\n separately from \n
!- [todo] provide type information
Details
-generated results are trimmed up to 1 leading/trailing space
-prefixes are cached up to 1 trailing newline
-
*/
type AutocompletionStatus = 'pending' | 'finished' | 'error';
type Autocompletion = {
id: number,
@ -24,15 +65,21 @@ const MAX_PENDING_REQUESTS = 2
// postprocesses the result
const postprocessResult = (result: string) => {
// remove leading whitespace from result
return result.trimStart()
console.log('result: ', JSON.stringify(result))
// trim all whitespace except for a single leading/trailing space
const hasLeadingSpace = result.startsWith(' ');
const hasTrailingSpace = result.endsWith(' ');
return (hasLeadingSpace ? ' ' : '')
+ result.trim()
+ (hasTrailingSpace ? ' ' : '');
}
const extractCodeFromResult = (result: string) => {
// extract the code between triple backticks
const parts = result.split(/```/);
const parts = result.split(/```(?:\s*\w+)?\n?/);
// if there is no ``` then return the raw result
if (parts.length === 1) {
@ -58,6 +105,28 @@ const trimPrefix = (prefix: string) => {
return trimmedPrefix
}
function getStringUpToUnbalancedParenthesis(s: string, prefixToTheLeft: string): string {
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
// todo find first open bracket in prefix and get all brackets beyond it in prefix
// get all bracets in prefix
let stack: string[] = []
const firstOpenIdx = prefixToTheLeft.search(/[[({]/);
if (firstOpenIdx !== -1) stack = prefixToTheLeft.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c))
// Iterate through each character
for (let i = 0; i < s.length; i++) {
const char = s[i];
if (char === '(' || char === '{' || char === '[') { stack.push(char); }
else if (char === ')' || char === '}' || char === ']') {
if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); }
}
}
return s;
}
// finds the text in the autocompletion to display, assuming the prefix is already matched
// example:
// originalPrefix = abcd
@ -65,26 +134,69 @@ const trimPrefix = (prefix: string) => {
// originalSuffix = ijkl
// the user has typed "ef" so prefix = abcdef
// we want to return the rest of the generatedMiddle, which is "gh"
const toInlineCompletion = ({ prefix, autocompletion, position }: { prefix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => {
const toInlineCompletion = ({ prefix, suffix, autocompletion, position }: { prefix: string, suffix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => {
const originalPrefix = autocompletion.prefix
const generatedMiddle = autocompletion.result
const trimmedOriginalPrefix = trimPrefix(originalPrefix)
const trimmedCurrentPrefix = trimPrefix(prefix)
const lastMatchupIndex = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length
const suffixLines = suffix.split('\n')
const prefixLines = trimmedCurrentPrefix.split('\n')
const suffixToTheRightOfCursor = suffixLines[0].trim()
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1].trim()
if (lastMatchupIndex < 0) {
const generatedLines = generatedMiddle.split('\n')
// compute startIdx
let startIdx = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length
if (startIdx < 0) {
return new vscode.InlineCompletionItem('')
}
const completionStr = generatedMiddle.substring(lastMatchupIndex)
console.log('completionStr: ', completionStr)
// compute endIdx
// hacks to get the suffix to render properly with lower quality models
// if the generated text matches with the suffix on the current line, stop
let endIdx: number | undefined = generatedMiddle.length // exclusive bounds
return new vscode.InlineCompletionItem(
completionStr,
new vscode.Range(position, position)
)
if (suffixToTheRightOfCursor !== '') { // completing in the middle of a line
console.log('1')
// complete until there is a match
const matchIndex = generatedMiddle.lastIndexOf(suffixToTheRightOfCursor[0])
if (matchIndex > 0) { endIdx = matchIndex }
}
if (prefixToTheLeftOfCursor !== '') { // completing the end of a line
console.log('2')
// show a single line
const newlineIdx = generatedMiddle.indexOf('\n')
if (newlineIdx > -1) { endIdx = newlineIdx }
}
// // if a generated line matches with a suffix line, stop
// if (suffixLines.length > 1) {
// console.log('3')
// const lines = []
// for (const generatedLine of generatedLines) {
// if (suffixLines.slice(0, 10).some(suffixLine =>
// generatedLine.trim() !== '' && suffixLine.trim() !== ''
// && generatedLine.trim().startsWith(suffixLine.trim())
// )) break;
// lines.push(generatedLine)
// }
// endIdx = lines.join('\n').length // this is hacky, remove or refactor in future
// }
let completionStr = generatedMiddle.slice(startIdx, endIdx)
// filter out unbalanced parentheses
console.log('completionStrBeforeParens: ', JSON.stringify(completionStr))
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefixLines.slice(-2).join('\n'))
console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
console.log('finalCompletionStr: ', JSON.stringify(completionStr))
return new vscode.InlineCompletionItem(completionStr, new vscode.Range(position, position))
}
@ -105,11 +217,39 @@ const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: str
}
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
const prefixLines = prefix.split('\n')
const suffixLines = suffix.split('\n')
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
const suffixToRightOfCursor = suffixLines[0]
// default parameters
let shouldGenerate = true
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
// specific cases
if (suffixToRightOfCursor.trim() !== '') { // typing between something
stopTokens = ['\n', '\r\n']
}
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
// stopTokens = ['\n\n', '\r\n\r\n']
// }
if (prefixToLeftOfCursor === '' || suffixToRightOfCursor === '') { // at beginning or end of line
shouldGenerate = false
}
console.log('shouldGenerate:', shouldGenerate, stopTokens)
return { shouldGenerate, stopTokens }
}
export class AutocompleteProvider implements vscode.InlineCompletionItemProvider {
private _extensionContext: vscode.ExtensionContext;
private _autocompletionId: number = 0;
@ -123,7 +263,7 @@ export class AutocompleteProvider implements vscode.InlineCompletionItemProvider
}
// used internally by vscode
// fires after every keystroke
// fires after every keystroke and returns the completion to show
async provideInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
@ -136,6 +276,7 @@ export class AutocompleteProvider implements vscode.InlineCompletionItemProvider
const docUriStr = document.uri.toString()
const fullText = document.getText();
const cursorOffset = document.offsetAt(position);
const prefix = fullText.substring(0, cursorOffset)
@ -147,11 +288,17 @@ export class AutocompleteProvider implements vscode.InlineCompletionItemProvider
if (!this._autocompletionsOfDocument[docUriStr]) {
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>({
max: MAX_CACHE_SIZE,
dispose: (autocompletion) => { autocompletion.abortRef.current() }
dispose: (autocompletion) => {
autocompletion.abortRef.current()
}
})
}
this._lastPrefix = prefix
console.log('cache size: ', this._autocompletionsOfDocument[docUriStr].size)
// get all pending autocompletions
let __c = 0
this._autocompletionsOfDocument[docUriStr].forEach(a => { if (a.status === 'pending') __c += 1 })
console.log('pending: ' + __c)
// get autocompletion from cache
let cachedAutocompletion: Autocompletion | undefined = undefined
@ -167,17 +314,18 @@ export class AutocompleteProvider implements vscode.InlineCompletionItemProvider
if (cachedAutocompletion) {
if (cachedAutocompletion.status === 'finished') {
console.log('AAA1')
console.log('A1')
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, position })
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
return [inlineCompletion]
} else if (cachedAutocompletion.status === 'pending') {
console.log('AAA2')
console.log('A2')
try {
await cachedAutocompletion.llmPromise;
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, position })
console.log('id: ' + cachedAutocompletion.id)
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
return [inlineCompletion]
} catch (e) {
@ -186,7 +334,7 @@ export class AutocompleteProvider implements vscode.InlineCompletionItemProvider
}
} else if (cachedAutocompletion.status === 'error') {
console.log('AAA3')
console.log('A3')
}
return []
@ -211,7 +359,7 @@ export class AutocompleteProvider implements vscode.InlineCompletionItemProvider
return []
}
console.log('BBB')
console.log('B')
// if there are too many pending requests, cancel the oldest one
let numPending = 0
@ -230,6 +378,10 @@ export class AutocompleteProvider implements vscode.InlineCompletionItemProvider
}
}
const { shouldGenerate, stopTokens } = getCompletionOptions({ prefix, suffix })
if (!shouldGenerate) return []
// create a new autocompletion and add it to cache
const newAutocompletion: Autocompletion = {
id: this._autocompletionId++,
@ -249,12 +401,13 @@ export class AutocompleteProvider implements vscode.InlineCompletionItemProvider
sendLLMMessage({
mode: 'fim',
fimInfo: { prefix, suffix },
options: { stopTokens },
onText: async (tokenStr, completionStr) => {
newAutocompletion.result = completionStr
// if generation doesn't match the prefix for the first few tokens generated, reject it
if (completionStr.length < 20 && !doesPrefixMatchAutocompletion({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
if (!doesPrefixMatchAutocompletion({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
reject('LLM response did not match user\'s text.')
}
},
@ -296,9 +449,10 @@ export class AutocompleteProvider implements vscode.InlineCompletionItemProvider
// show autocompletion
try {
await newAutocompletion.llmPromise;
await newAutocompletion.llmPromise
console.log('id: ' + newAutocompletion.id)
const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, position })
const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, suffix, position })
return [inlineCompletion]
} catch (e) {

View file

@ -50,7 +50,6 @@ ${completedStr}
isAnyChangeSoFar = true
}
const isRecentMatchup = false
// the final NUM_MATCHUP_TOKENS characters of fullCompletedStr are the same as the final NUM_MATCHUP_TOKENS characters of the last item in the diffs of oldFileStr that had 0 changes

View file

@ -114,7 +114,7 @@ export function activate(context: vscode.ExtensionContext) {
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
const abortRef: AbortRef = { current: null }
const abortRef: AbortRef = { current: () => { } }
if (m.type === 'requestFiles') {
@ -187,15 +187,12 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.languages.registerInlineCompletionItemProvider('*', autocompleteProvider));
const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
const abortRef: AbortRef = { current: null }
// setupAutocomplete({ voidConfig, abortRef })
// 7. Language Server
console.log('run lsp')
let disposable = vscode.commands.registerCommand('typeInspector.inspect', runTreeSitter);
context.subscriptions.push(disposable);

View file

@ -121,7 +121,7 @@ const voidConfigInfo: Record<
model: configEnum(
'Ollama model to use.',
'codestral',
["codestral", "qwen2.5-coder", "qwen2.5-coder:0.5B", "qwen2.5-coder:1.5B", "qwen2.5-coder:3B", "qwen2.5-coder:7B", "qwen2.5-coder:14B", "qwen2.5-coder:32B", "codegemma", "codegemma:2b", "codegemma:7b", "codellama", "codellama:7b", "codellama:13b", "codellama:34b", "codellama:70b", "codellama:code", "codellama:python", "command-r", "command-r:35b", "command-r-plus", "command-r-plus:104b", "deepseek-coder-v2", "deepseek-coder-v2:16b", "deepseek-coder-v2:236b", "falcon2", "falcon2:11b", "firefunction-v2", "firefunction-v2:70b", "gemma", "gemma:2b", "gemma:7b", "gemma2", "gemma2:2b", "gemma2:9b", "gemma2:27b", "llama2", "llama2:7b", "llama2:13b", "llama2:70b", "llama3", "llama3:8b", "llama3:70b", "llama3-chatqa", "llama3-chatqa:8b", "llama3-chatqa:70b", "llama3-gradient", "llama3-gradient:8b", "llama3-gradient:70b", "llama3.1", "llama3.2", "llama3.1:8b", "llama3.1:70b", "llama3.1:405b", "llava", "llava:7b", "llava:13b", "llava:34b", "llava-llama3", "llava-llama3:8b", "llava-phi3", "llava-phi3:3.8b", "mistral", "mistral:7b", "mistral-large", "mistral-large:123b", "mistral-nemo", "mistral-nemo:12b", "mixtral", "mixtral:8x7b", "mixtral:8x22b", "moondream", "moondream:1.8b", "openhermes", "openhermes:v2.5", "phi3", "phi3:3.8b", "phi3:14b", "phi3.5", "phi3.5:3.8b", "qwen", "qwen:7b", "qwen:14b", "qwen:32b", "qwen:72b", "qwen:110b", "qwen2", "qwen2:0.5b", "qwen2:1.5b", "qwen2:7b", "qwen2:72b", "smollm", "smollm:135m", "smollm:360m", "smollm:1.7b"] as const
["codestral", "qwen2.5-coder", "qwen2.5-coder:0.5b", "qwen2.5-coder:1.5b", "qwen2.5-coder:3b", "qwen2.5-coder:7b", "qwen2.5-coder:14b", "qwen2.5-coder:32b", "codegemma", "codegemma:2b", "codegemma:7b", "codellama", "codellama:7b", "codellama:13b", "codellama:34b", "codellama:70b", "codellama:code", "codellama:python", "command-r", "command-r:35b", "command-r-plus", "command-r-plus:104b", "deepseek-coder-v2", "deepseek-coder-v2:16b", "deepseek-coder-v2:236b", "falcon2", "falcon2:11b", "firefunction-v2", "firefunction-v2:70b", "gemma", "gemma:2b", "gemma:7b", "gemma2", "gemma2:2b", "gemma2:9b", "gemma2:27b", "llama2", "llama2:7b", "llama2:13b", "llama2:70b", "llama3", "llama3:8b", "llama3:70b", "llama3-chatqa", "llama3-chatqa:8b", "llama3-chatqa:70b", "llama3-gradient", "llama3-gradient:8b", "llama3-gradient:70b", "llama3.1", "llama3.2", "llama3.1:8b", "llama3.1:70b", "llama3.1:405b", "llava", "llava:7b", "llava:13b", "llava:34b", "llava-llama3", "llava-llama3:8b", "llava-phi3", "llava-phi3:3.8b", "mistral", "mistral:7b", "mistral-large", "mistral-large:123b", "mistral-nemo", "mistral-nemo:12b", "mixtral", "mixtral:8x7b", "mixtral:8x22b", "moondream", "moondream:1.8b", "openhermes", "openhermes:v2.5", "phi3", "phi3:3.8b", "phi3:14b", "phi3.5", "phi3.5:3.8b", "qwen", "qwen:7b", "qwen:14b", "qwen:32b", "qwen:72b", "qwen:110b", "qwen2", "qwen2:0.5b", "qwen2:1.5b", "qwen2:7b", "qwen2:72b", "smollm", "smollm:135m", "smollm:360m", "smollm:1.7b"] as const
),
},
openRouter: {

View file

@ -156,7 +156,7 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
// state of chat
const [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false)
const abortFnRef = useRef<(() => void) | null>(null)
const abortFnRef = useRef<(() => void)>(() => { })
const [latestError, setLatestError] = useState('')