Skip to content

Commit

Permalink
build: added treeview widget
Browse files Browse the repository at this point in the history
  • Loading branch information
mmiscool committed Dec 17, 2024
1 parent a46349d commit 704e9c9
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 166 deletions.
14 changes: 8 additions & 6 deletions public/ChatManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,10 @@ export class ChatManager {

this.promptsDialog = document.createElement('dialog');
this.promptsDialog.style.position = 'absolute';
this.promptsDialog.style.top = '50%';
this.promptsDialog.style.bottom = '15%';
this.promptsDialog.style.right = '15%';
this.promptsDialog.style.left = '15%';
this.promptsDialog.style.top = '30px';
this.promptsDialog.style.bottom = '30px';
this.promptsDialog.style.right = '30px';
this.promptsDialog.style.left = '30px';



Expand Down Expand Up @@ -253,7 +253,7 @@ export class ChatManager {
this.submitButton = document.createElement('button');
this.submitButton.textContent = 'Submit';
this.submitButton.style.width = '100%';
this.submitButton.style.marginBottom = '10px';
this.submitButton.style.marginBottom = '100px';
this.submitButton.addEventListener('click', async () => {
await this.submitButtonHandler();
});
Expand Down Expand Up @@ -520,6 +520,8 @@ export class ChatManager {
promptDiv.style.backgroundColor = 'rgba(0, 50, 100, 0.9)';
promptDiv.style.margin = '10px';
promptDiv.style.cursor = 'pointer';
// preformatted text
promptDiv.style.whiteSpace = 'pre-wrap';
promptDiv.addEventListener('click', async () => {
if (newConversation) await this.newChat(prompt);
await this.setInput(prompt);
Expand All @@ -544,7 +546,7 @@ export class ChatManager {
trashIcon.addEventListener('click', async (event) => {
event.stopPropagation();
this.promptsDialog.close();
const confirmDelete = await ConfirmDialog.confirm(`Delete prompt: \n "${prompt}"?`, 0, false);
const confirmDelete = await confirm(`Delete prompt: \n "${prompt}"?`, 0, false);
if (confirmDelete) {
customPrompts = customPrompts.filter((p) => p !== prompt);
await doAjax('./writeFile', {
Expand Down
2 changes: 2 additions & 0 deletions public/confirmDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const dialogStyle = {

const confirmButtonStyle = {
padding: '10px 20px',
margin: '10px',
backgroundColor: '#4CAF50', // Green
color: '#fff',
border: 'none',
Expand All @@ -26,6 +27,7 @@ const confirmButtonStyle = {

const cancelButtonStyle = {
padding: '10px 20px',
margin: '10px',
backgroundColor: '#f44336', // Red
color: '#fff',
border: 'none',
Expand Down
267 changes: 115 additions & 152 deletions public/fileDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,199 +2,162 @@
import { makeElement } from './domElementFactory.js';
import { doAjax } from './doAjax.js';

export async function fileDialog(fileListArray) {
return new Promise((resolve) => {
// Create modal elements using makeElement
const modal = makeElement('div', {
id: 'file-dialog',
style: {
position: 'fixed',
top: '0',
left: '0',
width: '100vw',
height: '100vh',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: '10000',
color: '#f0f0f0',
},
export function generateTreeView(targetElement, list, splitChar, onSingleClick, onDoubleClick) {
let clickTimeout;
const CLICK_DELAY = 300; // 300ms to differentiate single from double click

function buildTree(list) {
const tree = {};
list.forEach((item) => {
const parts = item.split(splitChar);
let current = tree;
parts.forEach((part, index) => {
if (!current[part]) {
current[part] = index === parts.length - 1 ? null : {};
}
current = current[part];
});
});
return tree;
}

function renderTree(tree, container, path = '', isRoot = false) {
Object.keys(tree).sort().forEach((key) => {
const item = makeElement('li', { style: { marginLeft: '10px' } });
const fullPath = `${path}${splitChar}${key}`.substring(1);
const isFolder = tree[key] !== null;

const label = makeElement('span', {
innerText: isFolder ? `📁 ${key}/` : `📄 ${key}`,
style: { cursor: 'pointer', color: isFolder ? '#ffffff' : '#66ccff' },
});

const dialog = makeElement('div', {
id: 'file-dialog-content',
style: {
backgroundColor: '#333',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.4)',
width: '400px',
maxHeight: '80vh',
overflowY: 'auto',
},
label.addEventListener('click', () => {
if (clickTimeout) clearTimeout(clickTimeout);
clickTimeout = setTimeout(() => onSingleClick(fullPath, label), CLICK_DELAY);
});

label.addEventListener('dblclick', () => {
if (clickTimeout) clearTimeout(clickTimeout);
onDoubleClick(fullPath);
});

item.appendChild(label);
container.appendChild(item);

if (isFolder) {
const subList = makeElement('ul', {
style: { listStyleType: 'none', paddingLeft: '20px', display: 'none' },
});
renderTree(tree[key], subList, `${path}${splitChar}${key}`);
item.appendChild(subList);

label.addEventListener('click', () => {
subList.style.display = subList.style.display === 'none' ? 'block' : 'none';
});
}
});
}

const tree = buildTree(list);
targetElement.innerHTML = '';
renderTree(tree, targetElement, '', true);
}

const title = makeElement('h2', {
innerText: 'Select a File',
export async function fileDialog(fileListArray) {
return new Promise((resolve) => {
let selectedFilePath = '';

const modal = makeElement('div', {
style: {
margin: '0 0 20px',
color: '#ffffff',
position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh',
backgroundColor: 'rgba(0, 0, 0, 0.8)', display: 'flex', justifyContent: 'center',
alignItems: 'center', zIndex: '10000', color: '#f0f0f0',
},
});

const fileTreeContainer = makeElement('ul', {
const dialog = makeElement('div', {
style: {
listStyleType: 'none',
padding: '0',
margin: '0',
backgroundColor: '#333', padding: '20px', borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.4)', width: '400px',
},
});

const buttonsContainer = makeElement('div', {
const title = makeElement('h2', { innerText: 'Select a File', style: { color: '#ffffff' } });
const fileTreeContainer = makeElement('ul', { style: { listStyleType: 'none', padding: '0' } });
const inputField = makeElement('input', {
type: 'text',
style: {
display: 'flex',
justifyContent: 'space-between',
marginTop: '20px',
width: '100%', padding: '10px', marginTop: '10px', boxSizing: 'border-box',
backgroundColor: '#222', color: '#f0f0f0', border: '1px solid #555',
},
readOnly: true,
placeholder: 'Selected file will appear here...',
});

const cancelButton = makeElement('button', {
innerText: 'Cancel',
const submitButton = makeElement('button', {
innerText: 'Submit',
style: {
padding: '10px 20px',
cursor: 'pointer',
backgroundColor: '#444',
border: 'none',
borderRadius: '4px',
color: '#f0f0f0',
padding: '10px 20px', marginTop: '10px', cursor: 'pointer',
backgroundColor: '#66ccff', border: 'none', borderRadius: '4px', color: '#333',
},
onclick: () => {
resolve(null);
if (selectedFilePath) resolve(selectedFilePath);
else alert('Please select a file.');
document.body.removeChild(modal);
},
});

const selectButton = makeElement('button', {
innerText: 'Select',
disabled: true,
const cancelButton = makeElement('button', {
innerText: 'Cancel',
style: {
padding: '10px 20px',
cursor: 'pointer',
backgroundColor: '#444',
border: 'none',
borderRadius: '4px',
color: '#f0f0f0',
padding: '10px 20px', marginTop: '10px', cursor: 'pointer', marginLeft: '10px',
backgroundColor: '#444', color: '#f0f0f0', border: 'none', borderRadius: '4px',
},
onclick: () => {
resolve(selectedFile.substring(1));
resolve(null);
document.body.removeChild(modal);
},
});

buttonsContainer.appendChild(cancelButton);
buttonsContainer.appendChild(selectButton);
const buttonContainer = makeElement('div', { style: { marginTop: '10px', display: 'flex' } });
buttonContainer.appendChild(submitButton);
buttonContainer.appendChild(cancelButton);

dialog.appendChild(title);
dialog.appendChild(fileTreeContainer);
dialog.appendChild(buttonsContainer);
dialog.appendChild(inputField);
dialog.appendChild(buttonContainer);
modal.appendChild(dialog);
document.body.appendChild(modal);

// Helper to build tree structure
function buildTree(files) {
const tree = {};
files.forEach((file) => {
const parts = file.split('/');
let current = tree;
parts.forEach((part, index) => {
if (!current[part]) {
current[part] = index === parts.length - 1 ? null : {};
}
current = current[part];
});
});
return tree;
}

function renderTree(tree, container, path = '', isRoot = false) {
// Sort keys: folders first, then files, both alphabetically
const sortedKeys = Object.keys(tree).sort((a, b) => {
const isFolderA = tree[a] !== null;
const isFolderB = tree[b] !== null;

if (isFolderA && !isFolderB) return -1; // Folders before files
if (!isFolderA && isFolderB) return 1; // Files after folders
return a.localeCompare(b); // Alphabetical order
});

sortedKeys.forEach((key) => {
const item = makeElement('li', {
style: {
marginLeft: '10px',
color: tree[key] === null ? '#66ccff' : '#ffffff',
},
});

const label = makeElement('span', {
innerText: tree[key] === null ? `📄 ${key}` : `📁 ${key}/`,
style: {
cursor: tree[key] === null ? 'pointer' : 'default',
},
onclick: () => {
if (tree[key] === null) {
// Deselect all other items
container.querySelectorAll('.selected').forEach((el) => el.classList.remove('selected'));

// Select the clicked file
label.classList.add('selected');
selectedFile = path + '/' + key;

// Resolve immediately on file click
resolve(selectedFile.substring(1));
document.body.removeChild(modal);
}
},
});

item.appendChild(label);
container.appendChild(item);

if (tree[key] !== null) {
const subList = makeElement('ul', {
style: {
listStyleType: 'none',
paddingLeft: '20px',
display: isRoot ? 'block' : 'none',
},
});

renderTree(tree[key], subList, path + '/' + key);
item.appendChild(subList);

label.style.cursor = 'pointer';
label.addEventListener('click', () => {
subList.style.display =
subList.style.display === 'none' ? 'block' : 'none';
});
}
});
}



// Build and render the tree
const tree = buildTree(fileListArray);
let selectedFile = null;
renderTree(tree, fileTreeContainer, '', true); // Root node expanded
generateTreeView(
fileTreeContainer,
fileListArray,
"/",
(filePath, label) => {
selectedFilePath = filePath;
inputField.value = filePath;
fileTreeContainer.querySelectorAll('.selected').forEach((el) => el.classList.remove('selected'));
label.classList.add('selected');
},
(filePath) => {
selectedFilePath = filePath;
resolve(filePath);
document.body.removeChild(modal);
}
);
});
}

export async function choseFile() {
const response = await doAjax('./getFilesList', {});
const fileList = response.files || [];
// Remove the first character from each file path
// because the first character is always '/'
// and we don't want to show it in the file dialog

const selectedFile = await fileDialog(fileList);
return selectedFile;
const fileList = (response.files || []).map((file) => file.substring(2));
console.log(fileList);
return await fileDialog(fileList);
}

1 change: 1 addition & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="light dark">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Coder</title>
<style>
* {
Expand Down
2 changes: 1 addition & 1 deletion public/toolsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,4 @@ export class toolsManager {
}
return true;
}
}
}
Loading

0 comments on commit 704e9c9

Please sign in to comment.