mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-16 14:13:48 +00:00
Assistant Builder improvements (#851)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
b1459523d9
commit
88aab302a2
37 files changed
+4098
-482
No files matched your search
@@ -34,6 +34,16 @@
|
||||
src: url('fonts/roboto-v30-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* JetBrainsMono-Regular - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('fonts/JetBrainsMono-Regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
|
||||
.mud-text-list .mud-list-item-icon {
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -291,3 +301,89 @@
|
||||
gap: 0.75rem;
|
||||
color: var(--mud-palette-text-secondary);
|
||||
}
|
||||
|
||||
.code-editor {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(2.8rem, auto) minmax(0, 1fr);
|
||||
min-height: 32rem;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--mw-code-editor-border, rgba(0,0,0,0.11764705882352941));
|
||||
border-radius: 4px;
|
||||
background: var(--mw-code-editor-background, rgba(255,255,255,1));
|
||||
color: var(--mw-code-editor-foreground, rgba(66,66,66,1));
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 0.65rem;
|
||||
line-height: 1.45;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.code-editor-line-numbers {
|
||||
overflow: hidden;
|
||||
padding: 0.8rem 0.65rem 0.8rem 0.5rem;
|
||||
border-right: 1px solid var(--mw-code-editor-border, rgba(0,0,0,0.11764705882352941));
|
||||
color: var(--mw-code-editor-foreground, rgba(66,66,66,1));
|
||||
opacity: 0.55;
|
||||
text-align: right;
|
||||
white-space: pre;
|
||||
user-select: none;
|
||||
font: inherit;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.code-editor-input {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 0.8rem;
|
||||
color: var(--mw-code-editor-foreground, rgba(66,66,66,1));
|
||||
caret-color: var(--mw-code-editor-foreground, rgba(66,66,66,1));
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
tab-size: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.code-editor .lua-comment {
|
||||
color: var(--mw-code-editor-comment, #6a9955);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.code-editor .lua-string {
|
||||
color: var(--mw-code-editor-string, #a31515);
|
||||
}
|
||||
|
||||
.code-editor .lua-number {
|
||||
color: var(--mw-code-editor-number, #098658);
|
||||
}
|
||||
|
||||
.code-editor .lua-keyword {
|
||||
color: var(--mw-code-editor-keyword, #0000ff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.code-editor .lua-literal {
|
||||
color: var(--mw-code-editor-literal, #0000ff);
|
||||
}
|
||||
|
||||
.code-editor .lua-built-in {
|
||||
color: var(--mw-code-editor-built-in, #795e26);
|
||||
}
|
||||
|
||||
.code-editor .lua-constant {
|
||||
color: var(--mw-code-editor-constant, #0070c1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.code-editor .lua-function {
|
||||
color: var(--mw-code-editor-function, #795e26);
|
||||
}
|
||||
|
||||
.code-editor .lua-property {
|
||||
color: var(--mw-code-editor-property, #001080);
|
||||
}
|
||||
|
||||
.code-editor .lua-variable {
|
||||
color: var(--mw-code-editor-variable, #267f99);
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
- Added support for OpenAI GPT-5.6 Sol, Terra, and Luna; Anthropic Claude Fable 5 and Mythos 5; and Google Gemini 3 Flash, Gemini 3.1 Flash-Lite, Gemini 3.1 Pro, and Gemini 3.5 Flash.
|
||||
- Added a log viewer assistant that shows AI Studio log files in a read-only view with search, log filters, highlighting, and auto-refresh.
|
||||
- Added audio and video transcription for chats and assistants. AI Studio now prepares supported media locally, sends only normalized audio to the configured transcription provider, and attaches the resulting transcript instead of the original media.
|
||||
- Added AI-assisted editing and revision for assistants created with the Assistant Builder. Thanks, Nils Kruthoff (`nilskruthoff`), for this contribution.
|
||||
- Added options to view and edit the code of AI-generated assistants and to delete your own generated assistants. Thanks, Nils Kruthoff (`nilskruthoff`), for this contribution.
|
||||
- Added enterprise configuration options to hide the last changelog and vision panels on the welcome page. Thanks, Dominic Neuburg (`donework`), for the contribution.
|
||||
- Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks.
|
||||
- Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,433 @@
|
||||
import { CodeJar } from "./codejar.js?v=20260707";
|
||||
|
||||
const editors = new Map();
|
||||
|
||||
const LUA_KEYWORDS = new Set([
|
||||
'and', 'break', 'do', 'else', 'elseif', 'end', 'for', 'function', 'goto',
|
||||
'if', 'in', 'local', 'not', 'or', 'repeat', 'return', 'then', 'until', 'while'
|
||||
]);
|
||||
const LUA_LITERALS = new Set(['false', 'nil', 'true']);
|
||||
const LUA_BUILT_INS = new Set([
|
||||
'_G', '_VERSION', 'assert', 'collectgarbage', 'dofile', 'error', 'getmetatable',
|
||||
'ipairs', 'load', 'loadfile', 'next', 'pairs', 'pcall', 'print', 'rawequal',
|
||||
'rawget', 'rawlen', 'rawset', 'require', 'select', 'setmetatable', 'tonumber',
|
||||
'tostring', 'type', 'xpcall', 'coroutine', 'debug', 'io', 'math', 'os',
|
||||
'package', 'string', 'table', 'utf8'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Creates a CodeJar editor for the Blazor component instance.
|
||||
*
|
||||
* CodeJar's public surface is intentionally small:
|
||||
* - CodeJar(element, highlighter, options) turns a 'contenteditable' element into an editor.
|
||||
* - updateCode(code) replaces the editor content and reruns highlighting.
|
||||
* - toString() reads the plain text content back out.
|
||||
* - destroy() removes listeners created by CodeJar.
|
||||
*
|
||||
* The highlighter callback receives the editor DOM node. It must write highlighted
|
||||
* HTML back into that node, so every token emitted by our highlighter is HTML-escaped.
|
||||
*/
|
||||
export function init(id, element, lineNumbersElement, code, language) {
|
||||
const codeJar = CodeJar(element, getHighlighter(language), {
|
||||
tab: ' ',
|
||||
spellcheck: false
|
||||
});
|
||||
// CodeJar enables soft wrapping by default, which cannot stay aligned with a newline-based gutter.
|
||||
element.style.whiteSpace = 'pre';
|
||||
element.style.overflowWrap = 'normal';
|
||||
const scrollHandler = () => syncLineNumbersScroll(element, lineNumbersElement);
|
||||
|
||||
codeJar.updateCode(code ?? '');
|
||||
updateLineNumbers(lineNumbersElement, codeJar.toString());
|
||||
codeJar.onUpdate(updatedCode => updateLineNumbers(lineNumbersElement, updatedCode));
|
||||
element.addEventListener('scroll', scrollHandler);
|
||||
editors.set(id, { codeJar, element, scrollHandler });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current plain text from a CodeJar instance.
|
||||
*/
|
||||
export function getCode(id) {
|
||||
return editors.get(id)?.codeJar.toString() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the editor content through CodeJar so the cursor/history/highlighter
|
||||
* state stays consistent with CodeJar's internal model.
|
||||
*/
|
||||
export function setCode(id, code) {
|
||||
const editor = editors.get(id);
|
||||
if (!editor)
|
||||
return;
|
||||
|
||||
editor.codeJar.updateCode(code ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes one editor instance and removes it from the JS-side registry.
|
||||
*/
|
||||
export function destroy(id) {
|
||||
const editor = editors.get(id);
|
||||
if (!editor)
|
||||
return;
|
||||
|
||||
editor.element.removeEventListener('scroll', editor.scrollHandler);
|
||||
editor.codeJar.destroy();
|
||||
editors.delete(id);
|
||||
}
|
||||
|
||||
function updateLineNumbers(lineNumbersElement, code) {
|
||||
const lineCount = (code.match(/\n/g)?.length ?? 0) + 1;
|
||||
let lineNumbers = '';
|
||||
for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
|
||||
if (lineNumber > 1)
|
||||
lineNumbers += '\n';
|
||||
|
||||
lineNumbers += lineNumber;
|
||||
}
|
||||
|
||||
lineNumbersElement.textContent = lineNumbers;
|
||||
}
|
||||
|
||||
function syncLineNumbersScroll(editorElement, lineNumbersElement) {
|
||||
lineNumbersElement.scrollTop = editorElement.scrollTop;
|
||||
}
|
||||
|
||||
function highlightLua(editor) {
|
||||
editor.innerHTML = highlightLuaCode(editor.textContent ?? '');
|
||||
}
|
||||
|
||||
function highlightPlainText() {
|
||||
}
|
||||
|
||||
function getHighlighter(language) {
|
||||
switch ((language ?? '').toLowerCase()) {
|
||||
case 'lua':
|
||||
return highlightLua;
|
||||
|
||||
default:
|
||||
return highlightPlainText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight Lua highlighter.
|
||||
*
|
||||
* This intentionally does not use one large regex. Lua comments, long strings
|
||||
* (`[[...]]`, `[=[...]=]`), quoted strings and numbers can overlap with words
|
||||
* that would otherwise look like keywords or variables. A small scanner lets us
|
||||
* consume those regions first and only tokenize identifiers after that.
|
||||
*/
|
||||
function highlightLuaCode(code) {
|
||||
let html = '';
|
||||
let index = 0;
|
||||
const localVariables = collectLuaLocalVariables(code);
|
||||
|
||||
while (index < code.length) {
|
||||
const char = code[index];
|
||||
const next = code[index + 1];
|
||||
|
||||
if (char === '-' && next === '-') {
|
||||
const longCommentEnd = readLuaLongBracketEnd(code, index + 2);
|
||||
if (longCommentEnd) {
|
||||
html += wrapLuaToken(code.slice(index, longCommentEnd.end), 'comment');
|
||||
index = longCommentEnd.end;
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineEnd = findLineEnd(code, index);
|
||||
html += wrapLuaToken(code.slice(index, lineEnd), 'comment');
|
||||
index = lineEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
const longStringEnd = readLuaLongBracketEnd(code, index);
|
||||
if (longStringEnd) {
|
||||
html += wrapLuaToken(code.slice(index, longStringEnd.end), 'string');
|
||||
index = longStringEnd.end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
const stringEnd = readQuotedStringEnd(code, index, char);
|
||||
html += wrapLuaToken(code.slice(index, stringEnd), 'string');
|
||||
index = stringEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isNumberStart(code, index)) {
|
||||
const numberEnd = readNumberEnd(code, index);
|
||||
html += wrapLuaToken(code.slice(index, numberEnd), 'number');
|
||||
index = numberEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isIdentifierStart(char)) {
|
||||
const functionCallEnd = readFunctionCallNameEnd(code, index);
|
||||
if (functionCallEnd > index) {
|
||||
html += wrapLuaToken(code.slice(index, functionCallEnd), 'function');
|
||||
index = functionCallEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
const identifierEnd = readIdentifierEnd(code, index);
|
||||
const identifier = code.slice(index, identifierEnd);
|
||||
const previousChar = findPreviousNonWhitespaceChar(code, index);
|
||||
html += highlightLuaIdentifier(identifier, localVariables, previousChar);
|
||||
index = identifierEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
html += escapeHtml(char);
|
||||
index++;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies one Lua identifier after the scanner has ruled out comments,
|
||||
* strings and numbers.
|
||||
*/
|
||||
function highlightLuaIdentifier(identifier, localVariables, previousChar) {
|
||||
if (LUA_KEYWORDS.has(identifier))
|
||||
return wrapLuaToken(identifier, 'keyword');
|
||||
|
||||
if (LUA_LITERALS.has(identifier))
|
||||
return wrapLuaToken(identifier, 'literal');
|
||||
|
||||
if (LUA_BUILT_INS.has(identifier))
|
||||
return wrapLuaToken(identifier, 'built-in');
|
||||
|
||||
if (isLuaConstant(identifier))
|
||||
return wrapLuaToken(identifier, 'constant');
|
||||
|
||||
if (previousChar === '.' || previousChar === ':')
|
||||
return wrapLuaToken(identifier, 'property');
|
||||
|
||||
if (localVariables.has(identifier))
|
||||
return wrapLuaToken(identifier, 'variable');
|
||||
|
||||
return escapeHtml(identifier);
|
||||
}
|
||||
|
||||
function wrapLuaToken(text, tokenClass) {
|
||||
return `<span class="lua-token lua-${tokenClass}">${escapeHtml(text)}</span>`;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function findLineEnd(code, start) {
|
||||
const lineEnd = code.indexOf('\n', start);
|
||||
return lineEnd < 0 ? code.length : lineEnd;
|
||||
}
|
||||
|
||||
function readQuotedStringEnd(code, start, quote) {
|
||||
let index = start + 1;
|
||||
while (index < code.length) {
|
||||
if (code[index] === '\\') {
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[index] === quote)
|
||||
return index + 1;
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return code.length;
|
||||
}
|
||||
|
||||
function readLuaLongBracketEnd(code, start) {
|
||||
if (code[start] !== '[')
|
||||
return null;
|
||||
|
||||
let equalsCount = 0;
|
||||
let index = start + 1;
|
||||
while (code[index] === '=') {
|
||||
equalsCount++;
|
||||
index++;
|
||||
}
|
||||
|
||||
if (code[index] !== '[')
|
||||
return null;
|
||||
|
||||
const close = `]${'='.repeat(equalsCount)}]`;
|
||||
const closeIndex = code.indexOf(close, index + 1);
|
||||
return {
|
||||
end: closeIndex < 0 ? code.length : closeIndex + close.length
|
||||
};
|
||||
}
|
||||
|
||||
function isNumberStart(code, index) {
|
||||
const char = code[index];
|
||||
const next = code[index + 1];
|
||||
return isDigit(char) || char === '.' && isDigit(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a permissive Lua number token.
|
||||
*
|
||||
* The character class covers decimal numbers, hex numbers (`0xff`), exponents
|
||||
* (`1e-3`, `0x1p+4`) and separators/dots used while the user is still typing.
|
||||
*/
|
||||
function readNumberEnd(code, start) {
|
||||
let index = start;
|
||||
while (index < code.length && /[0-9a-fA-FxXpPeE+\-_.]/.test(code[index]))
|
||||
index++;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function isIdentifierStart(char) {
|
||||
return /[A-Za-z_]/.test(char);
|
||||
}
|
||||
|
||||
function readIdentifierEnd(code, start) {
|
||||
let index = start + 1;
|
||||
while (index < code.length && /[A-Za-z0-9_]/.test(code[index]))
|
||||
index++;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function isDigit(char) {
|
||||
return /[0-9]/.test(char);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects function-call expressions such as `print(`, `table.insert(` or
|
||||
* `object:method(` and colors the whole call target as a function.
|
||||
*/
|
||||
function readFunctionCallNameEnd(code, start) {
|
||||
let index = readIdentifierEnd(code, start);
|
||||
let hasMember = false;
|
||||
|
||||
while (code[index] === '.' || code[index] === ':') {
|
||||
const memberStart = index + 1;
|
||||
if (!isIdentifierStart(code[memberStart]))
|
||||
break;
|
||||
|
||||
hasMember = true;
|
||||
index = readIdentifierEnd(code, memberStart);
|
||||
}
|
||||
|
||||
const nextIndex = skipWhitespace(code, index);
|
||||
if (code[nextIndex] === '(' && (hasMember || LUA_BUILT_INS.has(code.slice(start, index))))
|
||||
return index;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects names declared after `local` so later identifier tokens can be styled
|
||||
* as local variables. The scanner skips comments and strings first to avoid
|
||||
* treating text inside them as declarations.
|
||||
*/
|
||||
function collectLuaLocalVariables(code) {
|
||||
const variables = new Set();
|
||||
let index = 0;
|
||||
|
||||
while (index < code.length) {
|
||||
const char = code[index];
|
||||
const next = code[index + 1];
|
||||
|
||||
if (char === '-' && next === '-') {
|
||||
const longCommentEnd = readLuaLongBracketEnd(code, index + 2);
|
||||
index = longCommentEnd?.end ?? findLineEnd(code, index);
|
||||
continue;
|
||||
}
|
||||
|
||||
const longStringEnd = readLuaLongBracketEnd(code, index);
|
||||
if (longStringEnd) {
|
||||
index = longStringEnd.end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
index = readQuotedStringEnd(code, index, char);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isIdentifierStart(char)) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const identifierEnd = readIdentifierEnd(code, index);
|
||||
const identifier = code.slice(index, identifierEnd);
|
||||
if (identifier !== 'local') {
|
||||
index = identifierEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
index = readLocalDeclarationVariables(code, identifierEnd, variables);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
function readLocalDeclarationVariables(code, start, variables) {
|
||||
let index = skipWhitespace(code, start);
|
||||
|
||||
if (code.startsWith('function', index) && !isIdentifierPart(code[index + 'function'.length])) {
|
||||
index = skipWhitespace(code, index + 'function'.length);
|
||||
if (isIdentifierStart(code[index])) {
|
||||
const functionNameEnd = readIdentifierEnd(code, index);
|
||||
variables.add(code.slice(index, functionNameEnd));
|
||||
return functionNameEnd;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
while (index < code.length) {
|
||||
index = skipWhitespace(code, index);
|
||||
if (!isIdentifierStart(code[index]))
|
||||
break;
|
||||
|
||||
const nameEnd = readIdentifierEnd(code, index);
|
||||
variables.add(code.slice(index, nameEnd));
|
||||
index = skipWhitespace(code, nameEnd);
|
||||
|
||||
if (code[index] !== ',')
|
||||
break;
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function findPreviousNonWhitespaceChar(code, start) {
|
||||
let index = start - 1;
|
||||
while (index >= 0 && /\s/.test(code[index]))
|
||||
index--;
|
||||
|
||||
return index < 0 ? '' : code[index];
|
||||
}
|
||||
|
||||
function skipWhitespace(code, start) {
|
||||
let index = start;
|
||||
while (index < code.length && /\s/.test(code[index]))
|
||||
index++;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function isIdentifierPart(char) {
|
||||
return /[A-Za-z0-9_]/.test(char ?? '');
|
||||
}
|
||||
|
||||
function isLuaConstant(identifier) {
|
||||
// Constants are a convention here, not Lua syntax: `TRANSLATION_SYSTEM_PROMPT`.
|
||||
return identifier.length > 1 && /^[A-Z][A-Z0-9_]*$/.test(identifier);
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
const globalWindow = window;
|
||||
export function CodeJar(editor, highlight, opt = {}) {
|
||||
const options = {
|
||||
tab: '\t',
|
||||
indentOn: /[({\[]$/,
|
||||
moveToNewLine: /^[)}\]]/,
|
||||
spellcheck: false,
|
||||
catchTab: true,
|
||||
preserveIdent: true,
|
||||
addClosing: true,
|
||||
history: true,
|
||||
window: globalWindow,
|
||||
autoclose: {
|
||||
open: `([{'"`,
|
||||
close: `)]}'"`
|
||||
},
|
||||
...opt,
|
||||
};
|
||||
const window = options.window;
|
||||
const document = window.document;
|
||||
const listeners = [];
|
||||
const history = [];
|
||||
let at = -1;
|
||||
let focus = false;
|
||||
let onUpdate = () => void 0;
|
||||
let prev; // code content prior keydown event
|
||||
editor.setAttribute('contenteditable', 'plaintext-only');
|
||||
editor.setAttribute('spellcheck', options.spellcheck ? 'true' : 'false');
|
||||
editor.style.outline = 'none';
|
||||
editor.style.overflowWrap = 'break-word';
|
||||
editor.style.overflowY = 'auto';
|
||||
editor.style.whiteSpace = 'pre-wrap';
|
||||
const doHighlight = (editor, pos) => {
|
||||
highlight(editor, pos);
|
||||
};
|
||||
const matchFirefoxVersion = window.navigator.userAgent.match(/Firefox\/([0-9]+)\./);
|
||||
const firefoxVersion = matchFirefoxVersion
|
||||
? parseInt(matchFirefoxVersion[1])
|
||||
: 0;
|
||||
let isLegacy = false; // true if plaintext-only is not supported
|
||||
if (editor.contentEditable !== "plaintext-only" || firefoxVersion >= 136)
|
||||
isLegacy = true;
|
||||
if (isLegacy)
|
||||
editor.setAttribute("contenteditable", "true");
|
||||
const debounceHighlight = debounce(() => {
|
||||
const pos = save();
|
||||
doHighlight(editor, pos);
|
||||
restore(pos);
|
||||
}, 30);
|
||||
let recording = false;
|
||||
const shouldRecord = (event) => {
|
||||
return !isUndo(event) && !isRedo(event)
|
||||
&& event.key !== 'Meta'
|
||||
&& event.key !== 'Control'
|
||||
&& event.key !== 'Alt'
|
||||
&& !event.key.startsWith('Arrow');
|
||||
};
|
||||
const debounceRecordHistory = debounce((event) => {
|
||||
if (shouldRecord(event)) {
|
||||
recordHistory();
|
||||
recording = false;
|
||||
}
|
||||
}, 300);
|
||||
const on = (type, fn) => {
|
||||
listeners.push([type, fn]);
|
||||
editor.addEventListener(type, fn);
|
||||
};
|
||||
on('keydown', event => {
|
||||
if (event.defaultPrevented)
|
||||
return;
|
||||
prev = toString();
|
||||
if (options.preserveIdent)
|
||||
handleNewLine(event);
|
||||
else
|
||||
legacyNewLineFix(event);
|
||||
if (options.catchTab)
|
||||
handleTabCharacters(event);
|
||||
if (options.addClosing)
|
||||
handleSelfClosingCharacters(event);
|
||||
if (options.history) {
|
||||
handleUndoRedo(event);
|
||||
if (shouldRecord(event) && !recording) {
|
||||
recordHistory();
|
||||
recording = true;
|
||||
}
|
||||
}
|
||||
if (isLegacy && !isCopy(event))
|
||||
restore(save());
|
||||
});
|
||||
on('keyup', event => {
|
||||
if (event.defaultPrevented)
|
||||
return;
|
||||
if (event.isComposing)
|
||||
return;
|
||||
if (prev !== toString())
|
||||
debounceHighlight();
|
||||
debounceRecordHistory(event);
|
||||
onUpdate(toString());
|
||||
});
|
||||
on('focus', _event => {
|
||||
focus = true;
|
||||
});
|
||||
on('blur', _event => {
|
||||
focus = false;
|
||||
});
|
||||
on('paste', event => {
|
||||
recordHistory();
|
||||
handlePaste(event);
|
||||
recordHistory();
|
||||
onUpdate(toString());
|
||||
});
|
||||
on('cut', event => {
|
||||
recordHistory();
|
||||
handleCut(event);
|
||||
recordHistory();
|
||||
onUpdate(toString());
|
||||
});
|
||||
function save() {
|
||||
const s = getSelection();
|
||||
const pos = { start: 0, end: 0, dir: undefined };
|
||||
let { anchorNode, anchorOffset, focusNode, focusOffset } = s;
|
||||
if (!anchorNode || !focusNode)
|
||||
throw 'error1';
|
||||
// If the anchor and focus are the editor element, return either a full
|
||||
// highlight or a start/end cursor position depending on the selection
|
||||
if (anchorNode === editor && focusNode === editor) {
|
||||
pos.start = (anchorOffset > 0 && editor.textContent) ? editor.textContent.length : 0;
|
||||
pos.end = (focusOffset > 0 && editor.textContent) ? editor.textContent.length : 0;
|
||||
pos.dir = (focusOffset >= anchorOffset) ? '->' : '<-';
|
||||
return pos;
|
||||
}
|
||||
// Selection anchor and focus are expected to be text nodes,
|
||||
// so normalize them.
|
||||
if (anchorNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const node = document.createTextNode('');
|
||||
anchorNode.insertBefore(node, anchorNode.childNodes[anchorOffset]);
|
||||
anchorNode = node;
|
||||
anchorOffset = 0;
|
||||
}
|
||||
if (focusNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const node = document.createTextNode('');
|
||||
focusNode.insertBefore(node, focusNode.childNodes[focusOffset]);
|
||||
focusNode = node;
|
||||
focusOffset = 0;
|
||||
}
|
||||
visit(editor, el => {
|
||||
if (el === anchorNode && el === focusNode) {
|
||||
pos.start += anchorOffset;
|
||||
pos.end += focusOffset;
|
||||
pos.dir = anchorOffset <= focusOffset ? '->' : '<-';
|
||||
return 'stop';
|
||||
}
|
||||
if (el === anchorNode) {
|
||||
pos.start += anchorOffset;
|
||||
if (!pos.dir) {
|
||||
pos.dir = '->';
|
||||
}
|
||||
else {
|
||||
return 'stop';
|
||||
}
|
||||
}
|
||||
else if (el === focusNode) {
|
||||
pos.end += focusOffset;
|
||||
if (!pos.dir) {
|
||||
pos.dir = '<-';
|
||||
}
|
||||
else {
|
||||
return 'stop';
|
||||
}
|
||||
}
|
||||
if (el.nodeType === Node.TEXT_NODE) {
|
||||
if (pos.dir != '->')
|
||||
pos.start += el.nodeValue.length;
|
||||
if (pos.dir != '<-')
|
||||
pos.end += el.nodeValue.length;
|
||||
}
|
||||
});
|
||||
editor.normalize(); // collapse empty text nodes
|
||||
return pos;
|
||||
}
|
||||
function restore(pos) {
|
||||
const s = getSelection();
|
||||
let startNode, startOffset = 0;
|
||||
let endNode, endOffset = 0;
|
||||
if (!pos.dir)
|
||||
pos.dir = '->';
|
||||
if (pos.start < 0)
|
||||
pos.start = 0;
|
||||
if (pos.end < 0)
|
||||
pos.end = 0;
|
||||
// Flip start and end if the direction reversed
|
||||
if (pos.dir == '<-') {
|
||||
const { start, end } = pos;
|
||||
pos.start = end;
|
||||
pos.end = start;
|
||||
}
|
||||
let current = 0;
|
||||
visit(editor, el => {
|
||||
if (el.nodeType !== Node.TEXT_NODE)
|
||||
return;
|
||||
const len = (el.nodeValue || '').length;
|
||||
if (current + len > pos.start) {
|
||||
if (!startNode) {
|
||||
startNode = el;
|
||||
startOffset = pos.start - current;
|
||||
}
|
||||
if (current + len > pos.end) {
|
||||
endNode = el;
|
||||
endOffset = pos.end - current;
|
||||
return 'stop';
|
||||
}
|
||||
}
|
||||
current += len;
|
||||
});
|
||||
if (!startNode)
|
||||
startNode = editor, startOffset = editor.childNodes.length;
|
||||
if (!endNode)
|
||||
endNode = editor, endOffset = editor.childNodes.length;
|
||||
// Flip back the selection
|
||||
if (pos.dir == '<-') {
|
||||
[startNode, startOffset, endNode, endOffset] = [endNode, endOffset, startNode, startOffset];
|
||||
}
|
||||
{
|
||||
// If nodes not editable, create a text node.
|
||||
const startEl = uneditable(startNode);
|
||||
if (startEl) {
|
||||
const node = document.createTextNode('');
|
||||
startEl.parentNode?.insertBefore(node, startEl);
|
||||
startNode = node;
|
||||
startOffset = 0;
|
||||
}
|
||||
const endEl = uneditable(endNode);
|
||||
if (endEl) {
|
||||
const node = document.createTextNode('');
|
||||
endEl.parentNode?.insertBefore(node, endEl);
|
||||
endNode = node;
|
||||
endOffset = 0;
|
||||
}
|
||||
}
|
||||
s.setBaseAndExtent(startNode, startOffset, endNode, endOffset);
|
||||
editor.normalize(); // collapse empty text nodes
|
||||
}
|
||||
function uneditable(node) {
|
||||
while (node && node !== editor) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node;
|
||||
if (el.getAttribute('contenteditable') == 'false') {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
}
|
||||
function beforeCursor() {
|
||||
const s = getSelection();
|
||||
const r0 = s.getRangeAt(0);
|
||||
const r = document.createRange();
|
||||
r.selectNodeContents(editor);
|
||||
r.setEnd(r0.startContainer, r0.startOffset);
|
||||
return r.toString();
|
||||
}
|
||||
function afterCursor() {
|
||||
const s = getSelection();
|
||||
const r0 = s.getRangeAt(0);
|
||||
const r = document.createRange();
|
||||
r.selectNodeContents(editor);
|
||||
r.setStart(r0.endContainer, r0.endOffset);
|
||||
return r.toString();
|
||||
}
|
||||
function handleNewLine(event) {
|
||||
if (event.key === 'Enter') {
|
||||
const before = beforeCursor();
|
||||
const after = afterCursor();
|
||||
let [padding] = findPadding(before);
|
||||
let newLinePadding = padding;
|
||||
// If last symbol is "{" ident new line
|
||||
if (options.indentOn.test(before)) {
|
||||
newLinePadding += options.tab;
|
||||
}
|
||||
// Preserve padding
|
||||
if (newLinePadding.length > 0) {
|
||||
preventDefault(event);
|
||||
event.stopPropagation();
|
||||
insert('\n' + newLinePadding);
|
||||
}
|
||||
else {
|
||||
legacyNewLineFix(event);
|
||||
}
|
||||
// Place adjacent "}" on next line
|
||||
if (newLinePadding !== padding && options.moveToNewLine.test(after)) {
|
||||
const pos = save();
|
||||
insert('\n' + padding);
|
||||
restore(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
function legacyNewLineFix(event) {
|
||||
// Firefox does not support plaintext-only mode
|
||||
// and puts <div><br></div> on Enter. Let's help.
|
||||
if (isLegacy && event.key === 'Enter') {
|
||||
preventDefault(event);
|
||||
event.stopPropagation();
|
||||
if (afterCursor() == '') {
|
||||
insert('\n ');
|
||||
const pos = save();
|
||||
pos.start = --pos.end;
|
||||
restore(pos);
|
||||
}
|
||||
else {
|
||||
insert('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleSelfClosingCharacters(event) {
|
||||
const open = options.autoclose.open;
|
||||
const close = options.autoclose.close;
|
||||
if (open.includes(event.key)) {
|
||||
preventDefault(event);
|
||||
const pos = save();
|
||||
const wrapText = pos.start == pos.end ? '' : getSelection().toString();
|
||||
const text = event.key + wrapText + (close[open.indexOf(event.key)] ?? "");
|
||||
insert(text);
|
||||
pos.start++;
|
||||
pos.end++;
|
||||
restore(pos);
|
||||
}
|
||||
}
|
||||
function handleTabCharacters(event) {
|
||||
if (event.key === 'Tab') {
|
||||
preventDefault(event);
|
||||
if (event.shiftKey) {
|
||||
const before = beforeCursor();
|
||||
let [padding, start] = findPadding(before);
|
||||
if (padding.length > 0) {
|
||||
const pos = save();
|
||||
// Remove full length tab or just remaining padding
|
||||
const len = Math.min(options.tab.length, padding.length);
|
||||
restore({ start, end: start + len });
|
||||
document.execCommand('delete');
|
||||
pos.start -= len;
|
||||
pos.end -= len;
|
||||
restore(pos);
|
||||
}
|
||||
}
|
||||
else {
|
||||
insert(options.tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleUndoRedo(event) {
|
||||
if (isUndo(event)) {
|
||||
preventDefault(event);
|
||||
at--;
|
||||
const record = history[at];
|
||||
if (record) {
|
||||
editor.innerHTML = record.html;
|
||||
restore(record.pos);
|
||||
}
|
||||
if (at < 0)
|
||||
at = 0;
|
||||
}
|
||||
if (isRedo(event)) {
|
||||
preventDefault(event);
|
||||
at++;
|
||||
const record = history[at];
|
||||
if (record) {
|
||||
editor.innerHTML = record.html;
|
||||
restore(record.pos);
|
||||
}
|
||||
if (at >= history.length)
|
||||
at--;
|
||||
}
|
||||
}
|
||||
function recordHistory() {
|
||||
if (!focus)
|
||||
return;
|
||||
const html = editor.innerHTML;
|
||||
const pos = save();
|
||||
const lastRecord = history[at];
|
||||
if (lastRecord) {
|
||||
if (lastRecord.html === html
|
||||
&& lastRecord.pos.start === pos.start
|
||||
&& lastRecord.pos.end === pos.end)
|
||||
return;
|
||||
}
|
||||
at++;
|
||||
history[at] = { html, pos };
|
||||
history.splice(at + 1);
|
||||
const maxHistory = 300;
|
||||
if (at > maxHistory) {
|
||||
at = maxHistory;
|
||||
history.splice(0, 1);
|
||||
}
|
||||
}
|
||||
function handlePaste(event) {
|
||||
if (event.defaultPrevented)
|
||||
return;
|
||||
preventDefault(event);
|
||||
const originalEvent = event.originalEvent ?? event;
|
||||
const text = originalEvent.clipboardData.getData('text/plain').replace(/\r\n?/g, '\n');
|
||||
const pos = save();
|
||||
insert(text);
|
||||
doHighlight(editor);
|
||||
restore({
|
||||
start: Math.min(pos.start, pos.end) + text.length,
|
||||
end: Math.min(pos.start, pos.end) + text.length,
|
||||
dir: '<-',
|
||||
});
|
||||
}
|
||||
function handleCut(event) {
|
||||
const pos = save();
|
||||
const selection = getSelection();
|
||||
const originalEvent = event.originalEvent ?? event;
|
||||
originalEvent.clipboardData.setData('text/plain', selection.toString());
|
||||
document.execCommand('delete');
|
||||
doHighlight(editor);
|
||||
restore({
|
||||
start: Math.min(pos.start, pos.end),
|
||||
end: Math.min(pos.start, pos.end),
|
||||
dir: '<-',
|
||||
});
|
||||
preventDefault(event);
|
||||
}
|
||||
function visit(editor, visitor) {
|
||||
const queue = [];
|
||||
if (editor.firstChild)
|
||||
queue.push(editor.firstChild);
|
||||
let el = queue.pop();
|
||||
while (el) {
|
||||
if (visitor(el) === 'stop')
|
||||
break;
|
||||
if (el.nextSibling)
|
||||
queue.push(el.nextSibling);
|
||||
if (el.firstChild)
|
||||
queue.push(el.firstChild);
|
||||
el = queue.pop();
|
||||
}
|
||||
}
|
||||
function isCtrl(event) {
|
||||
return event.metaKey || event.ctrlKey;
|
||||
}
|
||||
function isUndo(event) {
|
||||
return isCtrl(event) && !event.shiftKey && getKeyCode(event) === 'Z';
|
||||
}
|
||||
function isRedo(event) {
|
||||
return isCtrl(event) && event.shiftKey && getKeyCode(event) === 'Z';
|
||||
}
|
||||
function isCopy(event) {
|
||||
return isCtrl(event) && getKeyCode(event) === 'C';
|
||||
}
|
||||
function getKeyCode(event) {
|
||||
let key = event.key || event.keyCode || event.which;
|
||||
if (!key)
|
||||
return undefined;
|
||||
return (typeof key === 'string' ? key : String.fromCharCode(key)).toUpperCase();
|
||||
}
|
||||
function insert(text) {
|
||||
text = text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
document.execCommand('insertHTML', false, text);
|
||||
}
|
||||
function debounce(cb, wait) {
|
||||
let timeout = 0;
|
||||
return (...args) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = window.setTimeout(() => cb(...args), wait);
|
||||
};
|
||||
}
|
||||
function findPadding(text) {
|
||||
// Find beginning of previous line.
|
||||
let i = text.length - 1;
|
||||
while (i >= 0 && text[i] !== '\n')
|
||||
i--;
|
||||
i++;
|
||||
// Find padding of the line.
|
||||
let j = i;
|
||||
while (j < text.length && /[ \t]/.test(text[j]))
|
||||
j++;
|
||||
return [text.substring(i, j) || '', i, j];
|
||||
}
|
||||
function toString() {
|
||||
return editor.textContent || '';
|
||||
}
|
||||
function preventDefault(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
function getSelection() {
|
||||
// @ts-ignore
|
||||
return editor.getRootNode().getSelection();
|
||||
}
|
||||
return {
|
||||
updateOptions(newOptions) {
|
||||
Object.assign(options, newOptions);
|
||||
},
|
||||
updateCode(code, callOnUpdate = true) {
|
||||
editor.textContent = code;
|
||||
doHighlight(editor);
|
||||
callOnUpdate && onUpdate(code);
|
||||
},
|
||||
onUpdate(callback) {
|
||||
onUpdate = callback;
|
||||
},
|
||||
toString,
|
||||
save,
|
||||
restore,
|
||||
recordHistory,
|
||||
destroy() {
|
||||
for (let [type, fn] of listeners) {
|
||||
editor.removeEventListener(type, fn);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user