adding gitea service

This commit is contained in:
2026-03-26 19:36:25 -03:00
parent 76cdb9654e
commit 83b1cb397d
16 changed files with 658 additions and 166 deletions

View File

@@ -0,0 +1,89 @@
.file-tree {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 0.75rem;
max-height: 380px;
overflow-y: auto;
font-size: 0.9rem;
}
.tree-folder {
margin-bottom: 2px;
}
.tree-folder-header {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
border-radius: 4px;
cursor: pointer;
user-select: none;
color: rgba(255, 255, 255, 0.85);
font-weight: 600;
}
.tree-folder-header:hover {
background: rgba(255, 255, 255, 0.07);
}
.tree-folder-arrow {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
width: 12px;
}
.tree-folder-name {
color: rgba(255, 255, 255, 0.85);
}
.tree-folder-children {
padding-left: 18px;
border-left: 1px solid rgba(255, 255, 255, 0.08);
margin-left: 6px;
}
.tree-file {
margin-bottom: 1px;
}
.tree-file-label {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
border-radius: 4px;
cursor: pointer;
color: rgba(255, 255, 255, 0.7);
}
.tree-file-label:hover {
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.9);
}
.tree-file-label input[type="checkbox"] {
accent-color: #7c6fcd;
width: 14px;
height: 14px;
cursor: pointer;
flex-shrink: 0;
}
.tree-file-name {
font-size: 0.875rem;
}
.tree-loading,
.tree-error,
.tree-empty {
padding: 1rem;
color: rgba(255, 255, 255, 0.5);
font-size: 0.9rem;
text-align: center;
}
.tree-error {
color: #ff7b72;
}

View File

@@ -0,0 +1,82 @@
import { useState, useEffect } from 'preact/hooks';
import { MindforgeApiService, type FileTreeNode } from '../services/MindforgeApiService';
import './FileTreeComponent.css';
interface FileTreeComponentProps {
selectedPaths: string[];
onSelectionChange: (paths: string[]) => void;
}
interface FolderNodeProps {
node: FileTreeNode;
selectedPaths: string[];
onToggle: (path: string) => void;
}
function FolderNode({ node, selectedPaths, onToggle }: FolderNodeProps) {
const [expanded, setExpanded] = useState(true);
if (node.type === 'file') {
return (
<div className="tree-file">
<label className="tree-file-label">
<input
type="checkbox"
checked={selectedPaths.includes(node.path)}
onChange={() => onToggle(node.path)}
/>
<span className="tree-file-name">{node.name}</span>
</label>
</div>
);
}
return (
<div className="tree-folder">
<div className="tree-folder-header" onClick={() => setExpanded(e => !e)}>
<span className="tree-folder-arrow">{expanded ? '▾' : '▸'}</span>
<span className="tree-folder-name">{node.name}</span>
</div>
{expanded && node.children && (
<div className="tree-folder-children">
{node.children.map(child => (
<FolderNode key={child.path} node={child} selectedPaths={selectedPaths} onToggle={onToggle} />
))}
</div>
)}
</div>
);
}
export function FileTreeComponent({ selectedPaths, onSelectionChange }: FileTreeComponentProps) {
const [tree, setTree] = useState<FileTreeNode[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
MindforgeApiService.getRepositoryTree()
.then(setTree)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
const togglePath = (path: string) => {
if (selectedPaths.includes(path)) {
onSelectionChange(selectedPaths.filter(p => p !== path));
} else {
onSelectionChange([...selectedPaths, path]);
}
};
if (loading) return <div className="tree-loading">Carregando repositório...</div>;
if (error) return <div className="tree-error">Erro ao carregar repositório: {error}</div>;
if (tree.length === 0) return <div className="tree-empty">Nenhum arquivo encontrado no repositório.</div>;
return (
<div className="file-tree">
{tree.map(node => (
<FolderNode key={node.path} node={node} selectedPaths={selectedPaths} onToggle={togglePath} />
))}
</div>
);
}

View File

@@ -1,5 +1,8 @@
import { useState, useRef } from 'preact/hooks';
import { useState } from 'preact/hooks';
import { MindforgeApiService, type FlashcardMode } from '../services/MindforgeApiService';
import { FileTreeComponent } from './FileTreeComponent';
import { Button } from './Button';
import './FlashcardComponent.css';
// Mapping of flashcard mode to its maximum allowed amount
const modeMax: Record<FlashcardMode, number> = {
@@ -9,9 +12,6 @@ const modeMax: Record<FlashcardMode, number> = {
Hyper: 130,
};
import { Button } from './Button';
import './FlashcardComponent.css';
function utf8ToBase64(str: string): string {
const bytes = new TextEncoder().encode(str);
const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
@@ -19,8 +19,7 @@ function utf8ToBase64(str: string): string {
}
export function FlashcardComponent() {
const [text, setText] = useState('');
const [fileName, setFileName] = useState('manual_input.md');
const [selectedPaths, setSelectedPaths] = useState<string[]>([]);
const [amount, setAmount] = useState<number>(20);
const [mode, setMode] = useState<FlashcardMode>('Simple');
const [loading, setLoading] = useState(false);
@@ -28,30 +27,13 @@ export function FlashcardComponent() {
const [success, setSuccess] = useState<boolean>(false);
const handleModeChange = (newMode: FlashcardMode) => {
setMode(newMode); // set the mode
setAmount(20); // set the default amount
};
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileUpload = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
const file = target.files[0];
setFileName(file.name);
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
setText(event.target.result as string);
}
};
reader.readAsText(file);
}
setMode(newMode);
setAmount(20);
};
const handleGenerate = async () => {
if (!text.trim()) {
setError('Por favor, insira algum texto ou faça upload de um arquivo para gerar os flashcards.');
if (selectedPaths.length === 0) {
setError('Selecione pelo menos um arquivo do repositório para gerar os flashcards.');
return;
}
@@ -60,16 +42,25 @@ export function FlashcardComponent() {
setSuccess(false);
try {
const base64Content = utf8ToBase64(text);
// Fetch all selected files and merge their content
const fileContents = await Promise.all(
selectedPaths.map(path => MindforgeApiService.getFileContent(path))
);
const mergedContent = fileContents.map(f => f.content).join('\n\n---\n\n');
const mergedFileName = selectedPaths.length === 1
? (selectedPaths[0].split('/').pop() ?? 'merged.md')
: 'merged.md';
const base64Content = utf8ToBase64(mergedContent);
const res = await MindforgeApiService.generateFlashcards({
fileContent: base64Content,
fileName,
fileName: mergedFileName,
amount,
mode
mode,
});
const csvContent = res.result;
downloadCSV(csvContent);
downloadCSV(res.result);
setSuccess(true);
} catch (err: any) {
setError(err.message || 'Ocorreu um erro ao gerar os flashcards.');
@@ -79,7 +70,6 @@ export function FlashcardComponent() {
};
const downloadCSV = (content: string) => {
// Adicionar BOM do UTF-8 para o Excel reconhecer os caracteres corretamente
const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
const blob = new Blob([bom, content], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
@@ -95,38 +85,22 @@ export function FlashcardComponent() {
return (
<div className="flashcard-container">
<h2 className="title" style={{ fontSize: '2.5rem' }}>Gerador de Flashcards</h2>
<p className="subtitle">Crie flashcards baseados nos seus materiais de estudo rapidamente.</p>
<p className="subtitle">Selecione os arquivos do repositório para gerar flashcards. Múltiplos arquivos serão combinados.</p>
<div className="flashcard-form">
<div className="input-group">
<label>Texto (Markdown)</label>
<textarea
className="text-area"
value={text}
onInput={(e) => setText((e.target as HTMLTextAreaElement).value)}
placeholder="Cole seu texto de estudo aqui ou faça upload do material..."
/>
</div>
<div className="file-input-wrapper">
<input
type="file"
accept=".md,.txt,.html"
ref={fileInputRef}
onChange={handleFileUpload}
className="file-input"
id="flashcard-file"
/>
<label htmlFor="flashcard-file" className="file-input-label">
📁 Escolher Arquivo
</label>
<span style={{ fontSize: '0.9rem', color: 'rgba(255,255,255,0.6)' }}>
{fileName !== 'manual_input.md' ? fileName : 'Nenhum arquivo selecionado'}
</span>
<label>Arquivos do Repositório</label>
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
{selectedPaths.length > 0 && (
<div style={{ fontSize: '0.85rem', color: 'rgba(255,255,255,0.5)', marginTop: '0.4rem' }}>
{selectedPaths.length} arquivo{selectedPaths.length !== 1 ? 's' : ''} selecionado{selectedPaths.length !== 1 ? 's' : ''}
{selectedPaths.length > 1 ? ' — conteúdo será combinado' : ''}
</div>
)}
</div>
<div className="input-group">
<label>Quantidade Estimada de Flashcards (10 - 100)</label>
<label>Quantidade Estimada de Flashcards (10 - {modeMax[mode]})</label>
<div className="slider-wrapper">
<input
type="range"

View File

@@ -22,6 +22,31 @@
justify-content: center;
align-items: center;
width: 100%;
position: relative;
}
.header-repo {
position: absolute;
right: 24px;
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
padding: 4px 10px;
}
.header-repo-icon {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5);
}
.header-repo-name {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.65);
font-family: var(--font-main);
letter-spacing: 0.5px;
}
.header-title {

View File

@@ -1,3 +1,5 @@
import { useState, useEffect } from 'preact/hooks';
import { MindforgeApiService } from '../services/MindforgeApiService';
import './Header.css';
interface HeaderProps {
@@ -5,14 +7,27 @@ interface HeaderProps {
}
export function Header({ onGoHome }: HeaderProps) {
const [repoName, setRepoName] = useState<string | null>(null);
useEffect(() => {
MindforgeApiService.getRepositoryInfo()
.then(info => setRepoName(info.name))
.catch(() => setRepoName(null));
}, []);
return (
<header class="header">
<div class="header-content">
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={onGoHome}>
<img src="/assets/mindforge.png" alt="Mindforge" width="55" height="55" style={{ marginRight: '10px' }} />
<h1 class="header-title">Mindforge</h1>
</div>
{repoName && (
<div class="header-repo">
<span class="header-repo-icon"></span>
<span class="header-repo-name">{repoName}</span>
</div>
)}
</div>
</header>
);

View File

@@ -8,6 +8,24 @@
gap: 1.5rem;
}
.file-result-block {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 1rem;
margin-bottom: 1rem;
}
.file-result-title {
font-size: 0.9rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
font-family: monospace;
}
.verificador-form {
display: flex;
flex-direction: column;

View File

@@ -1,5 +1,6 @@
import { useState, useRef } from 'preact/hooks';
import { useState } from 'preact/hooks';
import { MindforgeApiService } from '../services/MindforgeApiService';
import { FileTreeComponent } from './FileTreeComponent';
import { Button } from './Button';
import * as diff from 'diff';
import { marked } from 'marked';
@@ -13,62 +14,66 @@ function utf8ToBase64(str: string): string {
type CheckTypeEnum = 'language' | 'content' | 'both';
export function VerificadorComponent() {
const [text, setText] = useState('');
const [fileName, setFileName] = useState('manual_input.md');
const [checkType, setCheckType] = useState<CheckTypeEnum>('language');
interface FileResult {
path: string;
fileName: string;
originalContent: string;
languageResult: string | null;
contentResult: string | null;
error: string | null;
}
export function VerificadorComponent() {
const [selectedPaths, setSelectedPaths] = useState<string[]>([]);
const [checkType, setCheckType] = useState<CheckTypeEnum>('language');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [languageResult, setLanguageResult] = useState<string | null>(null);
const [contentResult, setContentResult] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileUpload = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
const file = target.files[0];
setFileName(file.name);
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
setText(event.target.result as string);
}
};
reader.readAsText(file);
}
};
const [results, setResults] = useState<FileResult[]>([]);
const handleSubmit = async () => {
if (!text.trim()) {
setError('Por favor, insira algum texto ou faça upload de um arquivo.');
if (selectedPaths.length === 0) {
setError('Selecione pelo menos um arquivo do repositório.');
return;
}
setLoading(true);
setError(null);
setLanguageResult(null);
setContentResult(null);
const base64Content = utf8ToBase64(text);
setResults([]);
try {
if (checkType === 'both') {
const [langRes, contRes] = await Promise.all([
MindforgeApiService.checkFile({ fileContent: base64Content, fileName, checkType: 'language' }),
MindforgeApiService.checkFile({ fileContent: base64Content, fileName, checkType: 'content' })
]);
setLanguageResult(langRes.result);
setContentResult(contRes.result);
} else {
const res = await MindforgeApiService.checkFile({ fileContent: base64Content, fileName, checkType });
if (checkType === 'language') setLanguageResult(res.result);
else setContentResult(res.result);
}
const fileResults = await Promise.all(
selectedPaths.map(async (path): Promise<FileResult> => {
const fileName = path.split('/').pop() ?? path;
try {
const { content } = await MindforgeApiService.getFileContent(path);
const base64Content = utf8ToBase64(content);
let languageResult: string | null = null;
let contentResult: string | null = null;
if (checkType === 'both') {
const [langRes, contRes] = await Promise.all([
MindforgeApiService.checkFile({ fileContent: base64Content, fileName, checkType: 'language' }),
MindforgeApiService.checkFile({ fileContent: base64Content, fileName, checkType: 'content' }),
]);
languageResult = langRes.result;
contentResult = contRes.result;
} else {
const res = await MindforgeApiService.checkFile({ fileContent: base64Content, fileName, checkType });
if (checkType === 'language') languageResult = res.result;
else contentResult = res.result;
}
return { path, fileName, originalContent: content, languageResult, contentResult, error: null };
} catch (err: any) {
return { path, fileName, originalContent: '', languageResult: null, contentResult: null, error: err.message };
}
})
);
setResults(fileResults);
} catch (err: any) {
setError(err.message || 'Ocorreu um erro ao processar sua requisição.');
setError(err.message || 'Ocorreu um erro ao processar os arquivos.');
} finally {
setLoading(false);
}
@@ -93,34 +98,17 @@ export function VerificadorComponent() {
return (
<div className="verificador-container">
<h2 className="title" style={{ fontSize: '2.5rem' }}>Verificador de Arquivos</h2>
<p className="subtitle">Faça o upload do seu arquivo Markdown para validação de linguagem ou conteúdo.</p>
<p className="subtitle">Selecione os arquivos do repositório para validação de linguagem ou conteúdo.</p>
<div className="verificador-form">
<div className="input-group">
<label>Texto (Markdown)</label>
<textarea
className="text-area"
value={text}
onInput={(e) => setText((e.target as HTMLTextAreaElement).value)}
placeholder="Cole seu texto aqui ou faça upload de um arquivo..."
/>
</div>
<div className="file-input-wrapper">
<input
type="file"
accept=".md,.txt"
ref={fileInputRef}
onChange={handleFileUpload}
className="file-input"
id="verificador-file"
/>
<label htmlFor="verificador-file" className="file-input-label">
📁 Escolher Arquivo
</label>
<span style={{ fontSize: '0.9rem', color: 'rgba(255,255,255,0.6)' }}>
{fileName !== 'manual_input.md' ? fileName : 'Nenhum arquivo selecionado'}
</span>
<label>Arquivos do Repositório</label>
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
{selectedPaths.length > 0 && (
<div style={{ fontSize: '0.85rem', color: 'rgba(255,255,255,0.5)', marginTop: '0.4rem' }}>
{selectedPaths.length} arquivo{selectedPaths.length !== 1 ? 's' : ''} selecionado{selectedPaths.length !== 1 ? 's' : ''}
</div>
)}
</div>
<div className="input-group">
@@ -150,46 +138,55 @@ export function VerificadorComponent() {
</div>
)}
{/* Render Results */}
{!loading && (languageResult || contentResult) && (
{!loading && results.length > 0 && (
<div className="response-section">
{checkType === 'language' && languageResult && (
<div className="side-pane">
<div className="pane-title">Resultado - Linguagem (Diff)</div>
<div className="response-content">
{renderDiff(text, languageResult)}
</div>
</div>
)}
{results.map((fileResult) => (
<div key={fileResult.path} className="file-result-block">
<div className="file-result-title">{fileResult.fileName}</div>
{checkType === 'content' && contentResult && (
<div className="side-pane">
<div className="pane-title">Resultado - Conteúdo</div>
<div
className="response-content markdown-body"
dangerouslySetInnerHTML={{ __html: marked.parse(contentResult) as string }}
/>
</div>
)}
{fileResult.error && (
<div style={{ color: '#ff7b72', padding: '0.5rem' }}>{fileResult.error}</div>
)}
{checkType === 'both' && languageResult && contentResult && (
<div className="side-by-side">
<div className="side-pane">
<div className="pane-title">Linguagem (Diff)</div>
<div className="response-content" style={{ minHeight: '300px' }}>
{renderDiff(text, languageResult)}
{!fileResult.error && checkType === 'language' && fileResult.languageResult && (
<div className="side-pane">
<div className="pane-title">Linguagem (Diff)</div>
<div className="response-content">
{renderDiff(fileResult.originalContent, fileResult.languageResult)}
</div>
</div>
</div>
<div className="side-pane">
<div className="pane-title">Conteúdo</div>
<div
className="response-content markdown-body"
style={{ minHeight: '300px' }}
dangerouslySetInnerHTML={{ __html: marked.parse(contentResult) as string }}
/>
</div>
)}
{!fileResult.error && checkType === 'content' && fileResult.contentResult && (
<div className="side-pane">
<div className="pane-title">Conteúdo</div>
<div
className="response-content markdown-body"
dangerouslySetInnerHTML={{ __html: marked.parse(fileResult.contentResult) as string }}
/>
</div>
)}
{!fileResult.error && checkType === 'both' && fileResult.languageResult && fileResult.contentResult && (
<div className="side-by-side">
<div className="side-pane">
<div className="pane-title">Linguagem (Diff)</div>
<div className="response-content" style={{ minHeight: '200px' }}>
{renderDiff(fileResult.originalContent, fileResult.languageResult)}
</div>
</div>
<div className="side-pane">
<div className="pane-title">Conteúdo</div>
<div
className="response-content markdown-body"
style={{ minHeight: '200px' }}
dangerouslySetInnerHTML={{ __html: marked.parse(fileResult.contentResult) as string }}
/>
</div>
</div>
)}
</div>
)}
))}
</div>
)}
</div>

View File

@@ -1,5 +1,21 @@
const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5123';
export interface FileTreeNode {
name: string;
path: string;
type: 'file' | 'folder';
children?: FileTreeNode[];
}
export interface RepositoryInfo {
name: string;
}
export interface FileContentResponse {
path: string;
content: string;
}
export interface CheckFileRequest {
fileContent: string;
fileName: string;
@@ -52,5 +68,23 @@ export const MindforgeApiService = {
throw new Error(`Error generating flashcards: ${response.statusText}`);
}
return response.json();
}
},
async getRepositoryInfo(): Promise<RepositoryInfo> {
const response = await fetch(`${BASE_URL}/api/v1/repository/info`);
if (!response.ok) throw new Error(`Error fetching repository info: ${response.statusText}`);
return response.json();
},
async getRepositoryTree(): Promise<FileTreeNode[]> {
const response = await fetch(`${BASE_URL}/api/v1/repository/tree`);
if (!response.ok) throw new Error(`Error fetching repository tree: ${response.statusText}`);
return response.json();
},
async getFileContent(path: string): Promise<FileContentResponse> {
const response = await fetch(`${BASE_URL}/api/v1/repository/file?path=${encodeURIComponent(path)}`);
if (!response.ok) throw new Error(`Error fetching file ${path}: ${response.statusText}`);
return response.json();
},
};