Monorepo Integration: Unified Backend, Frontend & Documentation

- Reorganize project into monorepo structure
  - backend/app/ - New FastAPI backend (modular with src/)
  - backend/legacy/ - Legacy database modules (relational & vector)
  - frontend/ - React text editor application

- Add launcher.py for easy full-stack startup
- Complete documentation in README.md
  - Quick start guide
  - API endpoints reference
  - Development setup
  - Troubleshooting

- Refactor main.py to 35 lines (app configuration only)
- Update .gitignore for full-stack project
- Add CHANGELOG.md with version history (v0.1.0-v0.1.1)

Structure is now clean and ready for team collaboration.
This commit is contained in:
Krzysztof Cieślik
2026-04-09 17:06:59 +02:00
parent fddaad962b
commit 6bbb24e633
55 changed files with 808 additions and 93 deletions

359
frontend/src/TextEditor.js Normal file
View File

@@ -0,0 +1,359 @@
import React, { useState } from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import { StarterKit } from '@tiptap/starter-kit';
import { Table } from '@tiptap/extension-table';
import { TableRow } from '@tiptap/extension-table-row';
import { TableCell } from '@tiptap/extension-table-cell';
import { TableHeader } from '@tiptap/extension-table-header';
import { Link } from '@tiptap/extension-link';
import { Image } from '@tiptap/extension-image';
import { Mathematics } from '@tiptap/extension-mathematics';
import { Underline } from '@tiptap/extension-underline';
import { CharacterCount } from '@tiptap/extension-character-count';
import MathField from './MathField';
import 'katex/dist/katex.min.css';
const TextEditor = () => {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Table.configure({ resizable: true }),
TableRow,
TableHeader,
TableCell,
Link.configure({
openOnClick: false,
HTMLAttributes: { class: 'editor-link' }
}),
Image.extend({
addAttributes() {
return {
...this.parent?.(),
width: {
default: '100%',
renderHTML: attributes => ({ width: attributes.width }),
parseHTML: element => element.getAttribute('width'),
},
};
},
}),
Mathematics,
CharacterCount,
],
editorProps: {
attributes: {
class: 'tiptap-editor',
style: 'outline: none; padding: 20px; min-height: 500px;',
},
// Obsługa klawisza Tab
handleKeyDown: (view, event) => {
if (event.key === 'Tab') {
if (editor.isActive('table')) return false; // Pozwalamy domyślnej obsłudze Tab w tabeli
event.preventDefault();
const { state, dispatch } = view;
const { selection, tr } = state;
const { $from } = selection; // Wyciągamy pozycję kursora jako objekt ResolvedPos
if (event.shiftKey) { // Obsługa Shift+Tab
const textBefore = $from.parent.textBetween(0, $from.parentOffset);
const match = textBefore.match(/ +$/);
if (match) {
const spacesFound = match[0].length;
const amountToRemove = Math.min(spacesFound, 4);
const transaction = tr.delete($from.pos - amountToRemove, $from.pos);
dispatch(transaction);
}
} else {
// Dla samego Tab wstawiamy 4 spacje
const transaction = tr.insertText(' ');
dispatch(transaction);
}
return true;
}
return false;
},
},
content: `<h1>Nowy Dokument</h1><p>Zacznij pisać tutaj...</p>`,
});
// Logika dodawania obrazu
const addImage = () => {
// Jeżeli kursor jest już na obrazie, pobierz jego aktualne atrybuty
const existingAttrs = editor.getAttributes('image');
const previousUrl = existingAttrs.src || "";
const previousWidth = existingAttrs.width || "100%";
// Jeśli obrazek już istnieje, możemy najpierw zapytać o nową szerokość
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
// Wczywanie/zmiana rozmiaru obrazu
if (previousUrl) {
const width = prompt("Szerokość obrazu (np. 50% lub 300px):", previousWidth);
editor.chain()
.focus()
.setImage({
src: previousUrl,
width: width || '100%'
})
.run();
} else {
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const width = prompt("Szerokość obrazu (np. 50% lub 300px):");
editor.chain()
.focus()
.setImage({
src: e.target.result,
width: width || '100%'
})
.run();
};
reader.readAsDataURL(file);
}
};
input.click();}
};
// Logika ustawiania linku
const setLink = () => {
const previousUrl = editor.getAttributes('link').href;
const url = window.prompt('URL:', previousUrl);
if (url === null) return;
if (url === '') {
editor.chain().focus().extendMarkRange('link').unsetLink().run();
return;
}
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
};
// Logika wstawiania formuły matematycznej
const [isMathModalOpen, setMathModalOpen] = useState(false);
const [tempLatex, setTempLatex] = useState("");
const insertMath = () => {
const existing = editor.getAttributes('inlineMath').latex || '';
setTempLatex(existing);
setMathModalOpen(true);
};
const handleMathConfirm = () => {
if (editor.isActive('inlineMath')) {
editor.chain().focus().updateAttributes('inlineMath', { latex: tempLatex }).run();
} else {
editor.chain().focus().insertContent({
type: 'inlineMath',
attrs: { latex: tempLatex }
}).run();
}
setMathModalOpen(false);
};
// Logika dodawania tabeli
const addTable = () => {
const rows = window.prompt("Liczba wierszy:", "3");
if (rows === null) return; // Anulowanie
const cols = window.prompt("Liczba kolumn:", "3");
if (cols === null) return; // Anulowanie
editor.chain().focus().insertTable({
rows: parseInt(rows) || 3,
cols: parseInt(cols) || 3,
withHeaderRow: true
}).run();
};
// Logika zapisu dokumentu
const handleSave = async () => {
const defaultTitle = "Dokument Archivium_" + new Date().toLocaleDateString();
const userTitle = window.prompt("Podaj nazwę pliku:", defaultTitle);
if (userTitle === null) return; // Anulowanie
const finalTitle = userTitle.trim() || defaultTitle;
const docData = {
title: finalTitle,
content: editor.getJSON(),
};
try {
const response = await fetch('http://127.0.0.1:8000/save-document', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(docData),
});
const result = await response.json();
if (result.status === "success") {
alert(`Zapisano pomyślnie jako: ${finalTitle}`);
}
} catch (error) {
alert("Błąd połączenia z modułem zapisu!");
}
};
// Logika wczytywania dokumentu
const handleLoad = async () => {
const fileName = window.prompt("Podaj nazwę dokumentu do wczytania:", "");
if (fileName === null) return; // Anulowanie
try {
let url = 'http://127.0.0.1:8000/load-document';
if (fileName.trim() !== "") { // Dodaj nazwę pliku jako parametr zapytania, jeśli została podana
url += `?title=${encodeURIComponent(fileName.trim())}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.content) {
editor.commands.setContent(data.content);
alert(`Wczytano: ${data.title || fileName}`);
} else {
alert("Błąd wczytywania: " + (data.error || "Nie znaleziono pliku o tej nazwie."));
}
} catch (error) {
alert("Błąd połączenia z modułem wczytywania!");
}
};
// Nie renderuj edytora, jeśli nie jest gotowy
if (!editor) return null;
return (
<div className="tools-container" style={{ border: '1px solid #b3b3b3', borderRadius: '4px', overflow: 'hidden', fontFamily: 'sans-serif', position: 'relative' }}>
{/* Pasek narzędzi formatowania */}
<div className="main-toolbar" style={{ background: '#f8f9fa', borderBottom: '1px solid #ddd', padding: '10px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{/* Grupa nagłówków */}
<div className="group" style={{ display: 'flex', gap: '2px', borderRight: '1px solid #ddd', paddingRight: '8px' }}>
<button onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} style={{ fontWeight: editor.isActive('heading', { level: 1 }) ? 'bold' : 'normal' }}>H1</button>
<button onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} style={{ fontWeight: editor.isActive('heading', { level: 2 }) ? 'bold' : 'normal' }}>H2</button>
<button onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} style={{ fontWeight: editor.isActive('heading', { level: 3 }) ? 'bold' : 'normal' }}>H3</button>
</div>
{/* Grupa formatowania tekstu */}
<div className="group" style={{ display: 'flex', gap: '2px', borderRight: '1px solid #ddd', paddingRight: '8px' }}>
<button onClick={() => editor.chain().focus().toggleBold().run()} style={{ fontWeight: editor.isActive('bold') ? 'bold' : 'normal' }}>B</button>
<button onClick={() => editor.chain().focus().toggleItalic().run()} style={{ fontStyle: editor.isActive('italic') ? 'italic' : 'normal' }}>I</button>
<button onClick={() => editor.chain().focus().toggleUnderline().run()} style={{ textDecoration: editor.isActive('underline') ? 'underline' : 'none' }}>U</button>
<button onClick={() => editor.chain().focus().toggleStrike().run()} style={{ textDecoration: editor.isActive('strike') ? 'line-through' : 'none' }}>S</button>
</div>
{/* Grupa list */}
<div className="group" style={{ display: 'flex', gap: '2px', borderRight: '1px solid #ddd', paddingRight: '8px' }}>
<button onClick={() => editor.chain().focus().toggleBulletList().run()}> List</button>
<button onClick={() => editor.chain().focus().toggleOrderedList().run()}>1. List</button>
</div>
{/* Grupa wstawiania elementów */}
<div className="group" style={{ display: 'flex', gap: '2px' }}>
<button onClick={setLink} style={{ color: editor.isActive('link') ? '#2196F3' : 'black' }}>Link</button>
<button onClick={addImage}>Image</button>
<button onClick={insertMath}>Σ Formula</button>
<button onClick={addTable}>Tabela</button>
</div>
</div>
{/* Pasek narzędzi tabeli */}
{editor.isActive('table') && (
<div className="table-controls" style={{ background: '#e9ecef', padding: '5px 10px', display: 'flex', gap: '10px', alignItems: 'center', fontSize: '12px' }}>
<span>Tabela:</span>
<button onClick={() => editor.chain().focus().addColumnAfter().run()}>+Kolumna</button>
<button onClick={() => editor.chain().focus().deleteColumn().run()}>-Kolumna</button>
<button onClick={() => editor.chain().focus().addRowAfter().run()}>+Wiersz</button>
<button onClick={() => editor.chain().focus().deleteRow().run()}>-Wiersz</button>
<button onClick={() => editor.chain().focus().mergeCells().run()}>Scal</button>
<button onClick={() => editor.chain().focus().splitCell().run()}>Rozdziel</button>
<button onClick={() => editor.chain().focus().deleteTable().run()} style={{ color: 'red' }}>Usuń</button>
</div>
)}
{/* Obszar edycji */}
<div style={{ background: 'white' }}>
<EditorContent editor={editor} />
</div>
{/* Pasek statusu */}
<div className="status-bar" style={{ background: '#f8f9fa', borderTop: '1px solid #ddd', padding: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ fontSize: '12px', color: '#666' }}>
Znaki: {editor.storage.characterCount.characters()} | Słowa: {editor.storage.characterCount.words()}
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<button onClick={handleLoad} style={{ background: '#f0f0f0', border: '1px solid #ccc', padding: '5px 15px', cursor: 'pointer' }}>Wczytaj</button>
<button onClick={handleSave} style={{ background: '#4CAF50', color: 'white', border: 'none', padding: '5px 20px', borderRadius: '3px', cursor: 'pointer' }}>Zapisz</button>
</div>
</div>
{/* Okno Modala dla edytora funkcji matematycznych */}
{isMathModalOpen && (
<>
{/* Tło (Overlay) */}
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
zIndex: 999
}}
/>
{/* Okno Modala */}
<div
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'white',
padding: '25px',
zIndex: 1000,
boxShadow: '0 10px 30px rgba(0,0,0,0.5)',
borderRadius: '12px',
width: '450px'
}}
>
<h3 style={{ marginTop: 0, color: '#333' }}>Wizualny Edytor Równań</h3>
<MathField
value={tempLatex}
onChange={setTempLatex}
/>
<div style={{ marginTop: '25px', display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button
onClick={() => setMathModalOpen(false)}
style={{
padding: '10px 20px',
cursor: 'pointer',
border: '1px solid #ccc',
borderRadius: '6px',
background: 'white'
}}
>
Anuluj
</button>
<button
onClick={handleMathConfirm}
style={{
background: '#4CAF50',
color: 'white',
border: 'none',
padding: '10px 25px',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
Wstaw do dokumentu
</button>
</div>
</div>
</>
)}
</div>
);
};
export default TextEditor;