added components

This commit is contained in:
Saifeddine ALOUI 2024-11-18 00:16:17 +01:00
parent 35e13f8d84
commit 2324503ea4
3 changed files with 613 additions and 0 deletions

View File

@ -0,0 +1,169 @@
<template>
<div class="max-w-4xl mx-auto p-4">
<div class="flex flex-col sm:flex-row mb-4 gap-2">
<input
type="text"
v-model="newKey"
placeholder="Enter key"
@keyup.enter="addItem"
class="flex-grow px-4 py-2 border border-gray-300 rounded dark:bg-gray-700 dark:text-white text-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<input
type="text"
v-model="newValue"
:placeholder="placeholder"
@keyup.enter="addItem"
class="flex-grow px-4 py-2 border border-gray-300 rounded dark:bg-gray-700 dark:text-white text-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<button @click="addItem" class="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600 text-lg transition duration-300 ease-in-out">Add</button>
</div>
<ul class="space-y-4" v-if="Object.keys(modelValue).length > 0">
<li
v-for="(value, key) in modelValue"
:key="key"
class="flex flex-col sm:flex-row items-center p-4 bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition duration-300 ease-in-out"
:class="{ 'bg-gray-100 dark:bg-gray-700': draggingKey === key }"
>
<div class="flex-grow mb-2 sm:mb-0 sm:mr-4 w-full sm:w-auto">
<input
:value="key"
@input="updateKey(key, $event.target.value)"
class="w-full px-3 py-2 border border-gray-300 rounded dark:bg-gray-600 dark:text-white text-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
</div>
<div class="flex-grow mb-2 sm:mb-0 sm:mr-4 w-full sm:w-auto">
<input
:value="value"
@input="updateValue(key, $event.target.value)"
class="w-full px-3 py-2 border border-gray-300 rounded dark:bg-gray-600 dark:text-white text-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
</div>
<div class="flex items-center space-x-2">
<button
@click="removeItem(key)"
class="text-red-500 hover:text-red-700 p-2 rounded-full hover:bg-red-100 dark:hover:bg-red-900 transition duration-300 ease-in-out"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
<button
@click="moveUp(key)"
class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 p-2 rounded-full transition duration-300 ease-in-out"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd" />
</svg>
</button>
<button
@click="moveDown(key)"
class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 p-2 rounded-full transition duration-300 ease-in-out"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</li>
</ul>
<div class="mt-6" v-if="Object.keys(modelValue).length > 0">
<button @click="removeAll" class="bg-red-500 text-white px-6 py-2 rounded hover:bg-red-600 text-lg transition duration-300 ease-in-out">Remove All</button>
</div>
</div>
</template>
<script>
export default {
name: 'DictionaryManager',
props: {
modelValue: {
type: Object,
default: () => ({}),
},
placeholder: {
type: String,
default: 'Enter a value',
},
},
emits: ['update:modelValue', 'change'],
data() {
return {
newKey: '',
newValue: '',
draggingKey: null,
};
},
methods: {
addItem() {
if (this.newKey.trim() && this.newValue.trim()) {
const updatedDict = { ...this.modelValue };
updatedDict[this.newKey.trim()] = this.newValue.trim();
this.$emit('update:modelValue', updatedDict);
this.$emit('change');
this.newKey = '';
this.newValue = '';
}
},
removeItem(key) {
const updatedDict = { ...this.modelValue };
delete updatedDict[key];
this.$emit('update:modelValue', updatedDict);
this.$emit('change');
},
removeAll() {
this.$emit('update:modelValue', {});
this.$emit('change');
},
updateKey(oldKey, newKey) {
if (newKey.trim() && newKey !== oldKey) {
const updatedDict = { ...this.modelValue };
updatedDict[newKey.trim()] = updatedDict[oldKey];
delete updatedDict[oldKey];
this.$emit('update:modelValue', updatedDict);
this.$emit('change');
}
},
updateValue(key, newValue) {
const updatedDict = { ...this.modelValue };
updatedDict[key] = newValue.trim();
this.$emit('update:modelValue', updatedDict);
this.$emit('change');
},
moveUp(key) {
const keys = Object.keys(this.modelValue);
const index = keys.indexOf(key);
if (index > 0) {
const updatedDict = {};
keys.forEach((k, i) => {
if (i === index - 1) {
updatedDict[key] = this.modelValue[key];
}
if (k !== key) {
updatedDict[k] = this.modelValue[k];
}
});
this.$emit('update:modelValue', updatedDict);
this.$emit('change');
}
},
moveDown(key) {
const keys = Object.keys(this.modelValue);
const index = keys.indexOf(key);
if (index < keys.length - 1) {
const updatedDict = {};
keys.forEach((k, i) => {
if (k !== key) {
updatedDict[k] = this.modelValue[k];
}
if (i === index + 1) {
updatedDict[key] = this.modelValue[key];
}
});
this.$emit('update:modelValue', updatedDict);
this.$emit('change');
}
},
},
};
</script>

View File

@ -0,0 +1,24 @@
<template>
<div :title="title" :class="['text-2xl cursor-pointer', isOk ? 'text-green-500' : 'text-red-500']">
<i v-if="typeof icon === 'string'" :data-feather="icon"></i>
<b v-else class="text-2xl">{{ icon }}</b>
</div>
</template>
<script>
import feather from 'feather-icons'
export default {
props: {
isOk: Boolean,
icon: [String, Object],
title: String
},
mounted() {
if (typeof this.icon === 'string') {
this.$nextTick(() => {
feather.replace()
})
}
}
}
</script>

420
web/src/components/code.vue Normal file
View File

@ -0,0 +1,420 @@
<template>
<div class="bg-bg-light-tone-panel dark:bg-bg-dark-tone-panel p-2 rounded-lg shadow-sm">
<div class="bg-bg-light-tone-panel dark:bg-bg-dark-tone-panel p-2 rounded-lg shadow-sm">
<div ref="editorContainer" class="monaco-editor"></div>
<div v-if="isLoading" class="loading-overlay">
<span>Loading...</span>
</div>
<!-- ... rest of your template -->
</div>
<div class="flex flex-row bg-bg-light-tone-panel dark:bg-bg-dark-tone-panel p-2 rounded-lg shadow-sm">
<span class="text-2xl mr-2">{{ language.trim() }}</span>
<button @click="copyCode"
:title="isCopied ? 'Copied!' : 'Copy code'"
:class="isCopied ? 'bg-green-500' : ''"
class="px-2 py-1 mr-2 mb-2 text-left text-sm font-medium rounded-lg hover:bg-primary dark:hover:bg-primary text-white transition-colors duration-200"
>
<i data-feather="copy"></i>
</button>
<button v-if="['function', 'python', 'sh', 'shell', 'bash', 'cmd', 'powershell', 'latex', 'mermaid', 'graphviz', 'dot', 'javascript', 'html', 'html5', 'svg'].includes(language)" ref="btn_code_exec" @click="executeCode" title="execute"
class="px-2 py-1 mr-2 mb-2 text-left text-sm font-medium rounded-lg hover:bg-primary dark:hover:bg-primary text-white transition-colors duration-200"
:class="isExecuting?'bg-green-500':''">
<i data-feather="play-circle"></i>
</button>
<button v-if="['airplay', 'mermaid', 'graphviz', 'dot', 'javascript', 'html', 'html5', 'svg', 'css'].includes(language.trim())" ref="btn_code_exec_in_new_tab" @click="executeCode_in_new_tab" title="execute"
class="px-2 py-1 mr-2 mb-2 text-left text-sm font-medium rounded-lg hover:bg-primary dark:hover:bg-primary text-white transition-colors duration-200"
:class="isExecuting?'bg-green-500':''">
<i data-feather="airplay"></i>
</button>
<button @click="openFolder" title="open code project folder"
class="px-2 py-1 mr-2 mb-2 text-left text-sm font-medium rounded-lg hover:bg-primary dark:hover:bg-primary text-white transition-colors duration-200"
>
<i data-feather="folder"></i>
</button>
<button v-if="['python', 'latex', 'html'].includes(language.trim())" @click="openFolderVsCode" title="open code project folder in vscode"
class="px-2 py-1 mr-2 mb-2 text-left text-sm font-medium rounded-lg hover:bg-primary dark:hover:bg-primary text-white transition-colors duration-200"
>
<img src="@/assets/vscode_black.svg" width="25" height="25">
</button>
<button v-if="['python', 'latex', 'html'].includes(language.trim())" @click="openVsCode" title="open code in vscode"
class="px-2 py-1 mr-2 mb-2 text-left text-sm font-medium rounded-lg hover:bg-primary dark:hover:bg-primary text-white transition-colors duration-200"
>
<img src="@/assets/vscode.svg" width="25" height="25">
</button>
</div>
<span v-if="executionOutput" class="text-2xl">Execution output</span>
<pre class="hljs mt-0 p-1 rounded-md break-all grid grid-cols-1" v-if="executionOutput">
<div class="container h-[200px] overflow-x-auto break-all scrollbar-thin scrollbar-track-bg-light-tone scrollbar-thumb-bg-light-tone-panel hover:scrollbar-thumb-primary dark:scrollbar-track-bg-dark-tone dark:scrollbar-thumb-bg-dark-tone-panel dark:hover:scrollbar-thumb-primary active:scrollbar-thumb-secondary">
<div ref="execution_output" class="w-full h-full overflow-y-auto scrollbar-thin scrollbar-track-bg-light-tone scrollbar-thumb-bg-light-tone-panel hover:scrollbar-thumb-primary dark:scrollbar-track-bg-dark-tone dark:scrollbar-thumb-bg-dark-tone-panel dark:hover:scrollbar-thumb-primary active:scrollbar-thumb-secondary" v-html="executionOutput"></div>
</div>
</pre>
</div>
</template>
<script>
import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
import { nextTick } from 'vue'
import hljs from 'highlight.js'
import feather from 'feather-icons';
import 'highlight.js/styles/tomorrow-night-blue.css';
import 'highlight.js/styles/tokyo-night-dark.css';
hljs.configure({ languages: [] }); // Reset languages
hljs.configure({ languages: ['bash'] }); // Set bash as the default language
hljs.highlightAll();
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new jsonWorker();
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new cssWorker();
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return new htmlWorker();
}
if (label === 'typescript' || label === 'javascript') {
return new tsWorker();
}
return new editorWorker();
}
};
export default {
props: {
host: {
type: String,
required: false,
default: "http://localhost:9600",
},
language: {
type: String,
required: true,
},
client_id: {
type: String,
required: true,
},
code: {
type: String,
required: true,
},
discussion_id: {
type: [String, Number],
required: true,
},
message_id: {
type: [String, Number],
required: true,
},
},
data() {
return {
isExecuting:false,
isCopied: false,
executionOutput: '', // new property
editor: null,
isLoading: false,
};
},
mounted() {
nextTick(() => {
feather.replace();
this.initMonaco();
// Listen for theme changes
window.addEventListener('themeChanged', this.handleThemeChange);
});
},
beforeUnmount() {
if (this.editor) {
this.editor.dispose();
}
window.removeEventListener('themeChanged', this.handleThemeChange);
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.updateEditorTheme);
},
computed: {
highlightedCode() {
let validLanguage;
if (this.language === 'vue' || this.language === 'vue.js') {
validLanguage = 'javascript';
} else
if (this.language === 'function') {
validLanguage = 'json';
} else {
validLanguage = hljs.getLanguage(this.language) ? this.language : 'plaintext';
}
const trimmedCode = this.code.trim(); // Remove leading and trailing whitespace
const lines = trimmedCode.split('\n');
const lineNumberWidth = lines.length.toString().length;
const lineNumbers = lines.map((line, index) => {
const lineNumber = index + 1;
return lineNumber.toString().padStart(lineNumberWidth, ' ');
});
const lineNumbersContainer = document.createElement('div');
lineNumbersContainer.classList.add('line-numbers');
lineNumbersContainer.innerHTML = lineNumbers.join('<br>');
const codeContainer = document.createElement('div');
codeContainer.classList.add('code-container');
const codeContent = document.createElement('pre');
const codeContentCode = document.createElement('code');
codeContentCode.classList.add('code-content');
codeContentCode.innerHTML = hljs.highlight(trimmedCode, {language: validLanguage, ignoreIllegals: true }).value;
codeContent.appendChild(codeContentCode);
codeContainer.appendChild(lineNumbersContainer);
codeContainer.appendChild(codeContent);
return codeContainer.outerHTML;
}
},
watch: {
code(newValue) {
if (this.editor && newValue !== this.editor.getValue()) {
this.editor.setValue(newValue);
}
},
language(newValue) {
if (this.editor) {
monaco.editor.setModelLanguage(this.editor.getModel(), newValue);
}
}
},
methods: {
initMonaco() {
const isDarkMode = document.documentElement.classList.contains('dark');
this.editor = monaco.editor.create(this.$refs.editorContainer, {
value: this.code,
language: this.language,
theme: isDarkMode ? 'vs-dark' : 'vs-light',
fontSize: 16, // Increase font size
automaticLayout: true
});
this.editor.onDidChangeModelContent(() => {
this.$emit('update:code', this.editor.getValue());
});
// Listen for theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.updateEditorTheme);
},
updateEditorContentInChunks(newCode) {
this.isLoading = true;
// ... existing updateEditorContentInChunks code
const updateChunk = () => {
// ... existing updateChunk code
if (index < newCode.length) {
requestAnimationFrame(updateChunk);
} else {
this.isLoading = false;
}
};
requestAnimationFrame(updateChunk);
},
handleThemeChange() {
this.updateEditorTheme();
},
changeFontSize(size) {
if (this.editor) {
this.editor.updateOptions({ fontSize: size });
}
},
copyCode() {
this.isCopied = true;
console.log("Copying code")
const el = document.createElement('textarea');
el.value = this.code;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
nextTick(() => {
feather.replace()
})
},
executeCode() {
this.isExecuting=true;
const json = JSON.stringify({
'client_id': this.client_id,
'code': this.code,
'discussion_id': this.discussion_id?this.discussion_id:0,
'message_id': this.message_id?this.message_id:0,
'language': this.language
})
console.log(json)
fetch(`${this.host}/execute_code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: json
}).then(response=>{
this.isExecuting=false;
// Parse the JSON data from the response body
return response.json();
})
.then(jsonData => {
// Now you can work with the JSON data
console.log(jsonData);
this.executionOutput = jsonData.output;
})
.catch(error => {
this.isExecuting=false;
// Handle any errors that occurred during the fetch process
console.error('Fetch error:', error);
});
},
executeCode_in_new_tab(){
this.isExecuting=true;
const json = JSON.stringify({
'client_id': this.client_id,
'code': this.code,
'discussion_id': this.discussion_id,
'message_id': this.message_id,
'language': this.language
})
console.log(json)
fetch(`${this.host}/execute_code_in_new_tab`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: json
}).then(response=>{
this.isExecuting=false;
// Parse the JSON data from the response body
return response.json();
})
.then(jsonData => {
// Now you can work with the JSON data
console.log(jsonData);
this.executionOutput = jsonData.output;
})
.catch(error => {
this.isExecuting=false;
// Handle any errors that occurred during the fetch process
console.error('Fetch error:', error);
});
},
openFolderVsCode(){
const json = JSON.stringify({
'client_id': this.client_id,
'code': this.code,
'discussion_id': this.discussion_id,
'message_id': this.message_id
})
console.log(json)
fetch(`${this.host}/open_discussion_folder_in_vs_code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: json
}).then(response=>{
// Parse the JSON data from the response body
return response.json();
})
.then(jsonData => {
// Now you can work with the JSON data
console.log(jsonData);
})
.catch(error => {
// Handle any errors that occurred during the fetch process
console.error('Fetch error:', error);
});
},
openVsCode() {
const json = JSON.stringify({
'client_id': this.client_id,
'discussion_id': typeof this.discussion_id === 'string' ? parseInt(this.discussion_id) : this.discussion_id ,
'message_id': this.message_id,
'code': this.code
})
console.log(json)
fetch(`${this.host}/open_code_in_vs_code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: json
}).then(response=>{
// Parse the JSON data from the response body
return response.json();
})
.then(jsonData => {
// Now you can work with the JSON data
console.log(jsonData);
})
.catch(error => {
// Handle any errors that occurred during the fetch process
console.error('Fetch error:', error);
});
},
openFolder() {
const json = JSON.stringify({ 'client_id': this.client_id, 'discussion_id': this.discussion_id })
console.log(json)
fetch(`${this.host}/open_discussion_folder`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: json
}).then(response=>{
// Parse the JSON data from the response body
return response.json();
})
.then(jsonData => {
// Now you can work with the JSON data
console.log(jsonData);
})
.catch(error => {
// Handle any errors that occurred during the fetch process
console.error('Fetch error:', error);
});
},
},
};
</script>
<style>
.code-container {
display: flex;
margin: 0; /* Remove the default margin */
}
.line-numbers {
flex-shrink: 0;
padding-right: 5px; /* Adjust the padding as needed */
color: #999;
user-select: none; /* Prevent line numbers from being selected */
white-space: nowrap; /* Prevent line numbers from wrapping */
margin: 0; /* Remove the default margin */
}
.code-content {
flex-grow: 1;
margin: 0; /* Remove the default margin */
outline: none; /* Remove the default focus outline */
}
.monaco-editor {
width: 100%;
height: 400px; /* Adjust as needed */
}
/* Ensure the editor's internal elements use the full height */
.monaco-editor .overflow-guard {
height: 100% !important;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 1.2em;
}
</style>