Merge pull request 'adding gitea service' (#1) from claude/beautiful-joliot into main
Some checks failed
Mindforge API Build and Deploy / Build Mindforge API Image (push) Failing after 51s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Has been skipped
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 3m17s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 9s
Some checks failed
Mindforge API Build and Deploy / Build Mindforge API Image (push) Failing after 51s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Has been skipped
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 3m17s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 9s
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
.claude/
|
.claude/
|
||||||
|
_bmad*
|
||||||
40
Mindforge.API/Controllers/RepositoryController.cs
Normal file
40
Mindforge.API/Controllers/RepositoryController.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mindforge.API.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Mindforge.API.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/v1/repository")]
|
||||||
|
public class RepositoryController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IGiteaService _giteaService;
|
||||||
|
|
||||||
|
public RepositoryController(IGiteaService giteaService)
|
||||||
|
{
|
||||||
|
_giteaService = giteaService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("info")]
|
||||||
|
public IActionResult GetInfo()
|
||||||
|
{
|
||||||
|
return Ok(new { name = _giteaService.GetRepositoryName() });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("tree")]
|
||||||
|
public async Task<IActionResult> GetTree()
|
||||||
|
{
|
||||||
|
var tree = await _giteaService.GetFileTreeAsync();
|
||||||
|
return Ok(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("file")]
|
||||||
|
public async Task<IActionResult> GetFile([FromQuery] string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return BadRequest(new { error = "path query parameter is required." });
|
||||||
|
|
||||||
|
var content = await _giteaService.GetFileContentAsync(path);
|
||||||
|
return Ok(new { path, content });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Mindforge.API/Models/FileTreeNode.cs
Normal file
10
Mindforge.API/Models/FileTreeNode.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Mindforge.API.Models
|
||||||
|
{
|
||||||
|
public class FileTreeNode
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Path { get; set; } = "";
|
||||||
|
public string Type { get; set; } = ""; // "file" | "folder"
|
||||||
|
public List<FileTreeNode>? Children { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@ using Mindforge.API.Services.Interfaces;
|
|||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Ensure environment variables are loaded into IConfiguration
|
||||||
|
builder.Configuration.AddEnvironmentVariables();
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Logging.AddConsole();
|
builder.Logging.AddConsole();
|
||||||
@@ -37,6 +40,7 @@ builder.Services.AddScoped<ILlmApiProvider, GeminiApiProvider>();
|
|||||||
builder.Services.AddScoped<IAgentService, AgentService>();
|
builder.Services.AddScoped<IAgentService, AgentService>();
|
||||||
builder.Services.AddScoped<IFileService, FileService>();
|
builder.Services.AddScoped<IFileService, FileService>();
|
||||||
builder.Services.AddScoped<IFlashcardService, FlashcardService>();
|
builder.Services.AddScoped<IFlashcardService, FlashcardService>();
|
||||||
|
builder.Services.AddScoped<IGiteaService, GiteaService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -68,4 +72,17 @@ if (string.IsNullOrEmpty(geminiKey))
|
|||||||
app.Logger.LogWarning("GEMINI_API_KEY not found in configuration.");
|
app.Logger.LogWarning("GEMINI_API_KEY not found in configuration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var giteaRepoUrl = builder.Configuration["GITEA_REPO_URL"];
|
||||||
|
var giteaAccessToken = builder.Configuration["GITEA_ACCESS_TOKEN"];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(giteaRepoUrl))
|
||||||
|
{
|
||||||
|
app.Logger.LogWarning("GITEA_REPO_URL not found in configuration. Repository features will not work.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(giteaAccessToken))
|
||||||
|
{
|
||||||
|
app.Logger.LogWarning("GITEA_ACCESS_TOKEN not found in configuration. Repository features will not work.");
|
||||||
|
}
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
165
Mindforge.API/Services/GiteaService.cs
Normal file
165
Mindforge.API/Services/GiteaService.cs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Mindforge.API.Exceptions;
|
||||||
|
using Mindforge.API.Models;
|
||||||
|
using Mindforge.API.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Mindforge.API.Services
|
||||||
|
{
|
||||||
|
public class GiteaService : IGiteaService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
private readonly string _owner;
|
||||||
|
private readonly string _repo;
|
||||||
|
private readonly string _token;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public GiteaService(HttpClient httpClient, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
|
||||||
|
var repoUrl = configuration["GITEA_REPO_URL"];
|
||||||
|
if (string.IsNullOrEmpty(repoUrl))
|
||||||
|
throw new InvalidOperationException("GITEA_REPO_URL is not set in configuration.");
|
||||||
|
|
||||||
|
_token = configuration["GITEA_ACCESS_TOKEN"]
|
||||||
|
?? throw new InvalidOperationException("GITEA_ACCESS_TOKEN is not set in configuration.");
|
||||||
|
|
||||||
|
// Parse: https://host/owner/repo or https://host/owner/repo.git
|
||||||
|
var normalizedUrl = repoUrl.TrimEnd('/').TrimEnd(".git".ToCharArray());
|
||||||
|
var uri = new Uri(normalizedUrl);
|
||||||
|
_baseUrl = $"{uri.Scheme}://{uri.Host}{(uri.IsDefaultPort ? "" : $":{uri.Port}")}";
|
||||||
|
var parts = uri.AbsolutePath.Trim('/').Split('/');
|
||||||
|
_owner = parts[0];
|
||||||
|
_repo = parts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetRepositoryName() => _repo;
|
||||||
|
|
||||||
|
public async Task<List<FileTreeNode>> GetFileTreeAsync()
|
||||||
|
{
|
||||||
|
// Get the master branch to obtain the latest commit SHA
|
||||||
|
var branchJson = await GetApiAsync($"/api/v1/repos/{_owner}/{_repo}/branches/master");
|
||||||
|
var branch = JsonSerializer.Deserialize<GiteaBranch>(branchJson, JsonOptions)
|
||||||
|
?? throw new InvalidOperationException("Failed to parse branch response from Gitea.");
|
||||||
|
|
||||||
|
var treeSha = branch.Commit.Id;
|
||||||
|
|
||||||
|
// Fetch the full recursive tree
|
||||||
|
var treeJson = await GetApiAsync($"/api/v1/repos/{_owner}/{_repo}/git/trees/{treeSha}?recursive=true");
|
||||||
|
var treeResponse = JsonSerializer.Deserialize<GiteaTreeResponse>(treeJson, JsonOptions)
|
||||||
|
?? throw new InvalidOperationException("Failed to parse tree response from Gitea.");
|
||||||
|
|
||||||
|
return BuildTree(treeResponse.Tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetFileContentAsync(string path)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get,
|
||||||
|
$"{_baseUrl}/api/v1/repos/{_owner}/{_repo}/raw/{path}?ref=master");
|
||||||
|
request.Headers.Add("Authorization", $"token {_token}");
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
throw new UserException($"File not found in repository: {path}");
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetApiAsync(string endpoint)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}{endpoint}");
|
||||||
|
request.Headers.Add("Authorization", $"token {_token}");
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<FileTreeNode> BuildTree(List<GiteaTreeItem> items)
|
||||||
|
{
|
||||||
|
var root = new List<FileTreeNode>();
|
||||||
|
var folderMap = new Dictionary<string, FileTreeNode>();
|
||||||
|
|
||||||
|
// Only include .md files and their parent folders
|
||||||
|
var mdFiles = items.Where(i => i.Type == "blob" && i.Path.EndsWith(".md")).ToList();
|
||||||
|
var neededFolders = new HashSet<string>();
|
||||||
|
foreach (var file in mdFiles)
|
||||||
|
{
|
||||||
|
var dir = System.IO.Path.GetDirectoryName(file.Path)?.Replace('\\', '/');
|
||||||
|
while (!string.IsNullOrEmpty(dir))
|
||||||
|
{
|
||||||
|
neededFolders.Add(dir);
|
||||||
|
dir = System.IO.Path.GetDirectoryName(dir)?.Replace('\\', '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create folder nodes
|
||||||
|
foreach (var item in items.Where(i => i.Type == "tree" && neededFolders.Contains(i.Path)).OrderBy(i => i.Path))
|
||||||
|
{
|
||||||
|
var node = new FileTreeNode
|
||||||
|
{
|
||||||
|
Name = System.IO.Path.GetFileName(item.Path),
|
||||||
|
Path = item.Path,
|
||||||
|
Type = "folder",
|
||||||
|
Children = []
|
||||||
|
};
|
||||||
|
folderMap[item.Path] = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place file and folder nodes into parent
|
||||||
|
var allNodes = mdFiles.Select(i => new FileTreeNode
|
||||||
|
{
|
||||||
|
Name = System.IO.Path.GetFileName(i.Path),
|
||||||
|
Path = i.Path,
|
||||||
|
Type = "file"
|
||||||
|
}).Concat(folderMap.Values).OrderBy(n => n.Path);
|
||||||
|
|
||||||
|
foreach (var node in allNodes)
|
||||||
|
{
|
||||||
|
var parentPath = System.IO.Path.GetDirectoryName(node.Path)?.Replace('\\', '/');
|
||||||
|
if (string.IsNullOrEmpty(parentPath))
|
||||||
|
root.Add(node);
|
||||||
|
else if (folderMap.TryGetValue(parentPath, out var parent))
|
||||||
|
parent.Children!.Add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Gitea API DTOs ----
|
||||||
|
|
||||||
|
private class GiteaBranch
|
||||||
|
{
|
||||||
|
[JsonPropertyName("commit")]
|
||||||
|
public GiteaCommit Commit { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GiteaCommit
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GiteaTreeResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("tree")]
|
||||||
|
public List<GiteaTreeItem> Tree { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GiteaTreeItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("path")]
|
||||||
|
public string Path { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = ""; // "blob" | "tree"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Mindforge.API/Services/Interfaces/IGiteaService.cs
Normal file
11
Mindforge.API/Services/Interfaces/IGiteaService.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Mindforge.API.Models;
|
||||||
|
|
||||||
|
namespace Mindforge.API.Services.Interfaces
|
||||||
|
{
|
||||||
|
public interface IGiteaService
|
||||||
|
{
|
||||||
|
Task<List<FileTreeNode>> GetFileTreeAsync();
|
||||||
|
Task<string> GetFileContentAsync(string path);
|
||||||
|
string GetRepositoryName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,9 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"OPENAI_API_KEY": "",
|
||||||
|
"GEMINI_API_KEY": "",
|
||||||
|
"GITEA_REPO_URL": "",
|
||||||
|
"GITEA_ACCESS_TOKEN": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: mindforge-secrets
|
name: mindforge-secrets
|
||||||
key: GEMINI_API_KEY
|
key: GEMINI_API_KEY
|
||||||
|
- name: GITEA_REPO_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: mindforge-secrets
|
||||||
|
key: GITEA_REPO_URL
|
||||||
|
- name: GITEA_ACCESS_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: mindforge-secrets
|
||||||
|
key: GITEA_ACCESS_TOKEN
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "128Mi"
|
memory: "128Mi"
|
||||||
|
|||||||
89
Mindforge.Web/src/components/FileTreeComponent.css
Normal file
89
Mindforge.Web/src/components/FileTreeComponent.css
Normal 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;
|
||||||
|
}
|
||||||
82
Mindforge.Web/src/components/FileTreeComponent.tsx
Normal file
82
Mindforge.Web/src/components/FileTreeComponent.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { useState, useRef } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { MindforgeApiService, type FlashcardMode } from '../services/MindforgeApiService';
|
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
|
// Mapping of flashcard mode to its maximum allowed amount
|
||||||
const modeMax: Record<FlashcardMode, number> = {
|
const modeMax: Record<FlashcardMode, number> = {
|
||||||
@@ -9,9 +12,6 @@ const modeMax: Record<FlashcardMode, number> = {
|
|||||||
Hyper: 130,
|
Hyper: 130,
|
||||||
};
|
};
|
||||||
|
|
||||||
import { Button } from './Button';
|
|
||||||
import './FlashcardComponent.css';
|
|
||||||
|
|
||||||
function utf8ToBase64(str: string): string {
|
function utf8ToBase64(str: string): string {
|
||||||
const bytes = new TextEncoder().encode(str);
|
const bytes = new TextEncoder().encode(str);
|
||||||
const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
|
const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
|
||||||
@@ -19,8 +19,7 @@ function utf8ToBase64(str: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FlashcardComponent() {
|
export function FlashcardComponent() {
|
||||||
const [text, setText] = useState('');
|
const [selectedPaths, setSelectedPaths] = useState<string[]>([]);
|
||||||
const [fileName, setFileName] = useState('manual_input.md');
|
|
||||||
const [amount, setAmount] = useState<number>(20);
|
const [amount, setAmount] = useState<number>(20);
|
||||||
const [mode, setMode] = useState<FlashcardMode>('Simple');
|
const [mode, setMode] = useState<FlashcardMode>('Simple');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -28,30 +27,13 @@ export function FlashcardComponent() {
|
|||||||
const [success, setSuccess] = useState<boolean>(false);
|
const [success, setSuccess] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleModeChange = (newMode: FlashcardMode) => {
|
const handleModeChange = (newMode: FlashcardMode) => {
|
||||||
setMode(newMode); // set the mode
|
setMode(newMode);
|
||||||
setAmount(20); // set the default amount
|
setAmount(20);
|
||||||
};
|
|
||||||
|
|
||||||
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 handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!text.trim()) {
|
if (selectedPaths.length === 0) {
|
||||||
setError('Por favor, insira algum texto ou faça upload de um arquivo para gerar os flashcards.');
|
setError('Selecione pelo menos um arquivo do repositório para gerar os flashcards.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,16 +42,25 @@ export function FlashcardComponent() {
|
|||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
|
|
||||||
try {
|
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({
|
const res = await MindforgeApiService.generateFlashcards({
|
||||||
fileContent: base64Content,
|
fileContent: base64Content,
|
||||||
fileName,
|
fileName: mergedFileName,
|
||||||
amount,
|
amount,
|
||||||
mode
|
mode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const csvContent = res.result;
|
downloadCSV(res.result);
|
||||||
downloadCSV(csvContent);
|
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Ocorreu um erro ao gerar os flashcards.');
|
setError(err.message || 'Ocorreu um erro ao gerar os flashcards.');
|
||||||
@@ -79,7 +70,6 @@ export function FlashcardComponent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const downloadCSV = (content: string) => {
|
const downloadCSV = (content: string) => {
|
||||||
// Adicionar BOM do UTF-8 para o Excel reconhecer os caracteres corretamente
|
|
||||||
const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
|
const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
|
||||||
const blob = new Blob([bom, content], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([bom, content], { type: 'text/csv;charset=utf-8;' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -95,38 +85,22 @@ export function FlashcardComponent() {
|
|||||||
return (
|
return (
|
||||||
<div className="flashcard-container">
|
<div className="flashcard-container">
|
||||||
<h2 className="title" style={{ fontSize: '2.5rem' }}>Gerador de Flashcards</h2>
|
<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="flashcard-form">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<label>Texto (Markdown)</label>
|
<label>Arquivos do Repositório</label>
|
||||||
<textarea
|
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
|
||||||
className="text-area"
|
{selectedPaths.length > 0 && (
|
||||||
value={text}
|
<div style={{ fontSize: '0.85rem', color: 'rgba(255,255,255,0.5)', marginTop: '0.4rem' }}>
|
||||||
onInput={(e) => setText((e.target as HTMLTextAreaElement).value)}
|
{selectedPaths.length} arquivo{selectedPaths.length !== 1 ? 's' : ''} selecionado{selectedPaths.length !== 1 ? 's' : ''}
|
||||||
placeholder="Cole seu texto de estudo aqui ou faça upload do material..."
|
{selectedPaths.length > 1 ? ' — conteúdo será combinado' : ''}
|
||||||
/>
|
</div>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input-group">
|
<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">
|
<div className="slider-wrapper">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
|
|||||||
@@ -22,6 +22,31 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
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 {
|
.header-title {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
|
import { MindforgeApiService } from '../services/MindforgeApiService';
|
||||||
import './Header.css';
|
import './Header.css';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
@@ -5,14 +7,27 @@ interface HeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ onGoHome }: 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 (
|
return (
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={onGoHome}>
|
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={onGoHome}>
|
||||||
<img src="/assets/mindforge.png" alt="Mindforge" width="55" height="55" style={{ marginRight: '10px' }} />
|
<img src="/assets/mindforge.png" alt="Mindforge" width="55" height="55" style={{ marginRight: '10px' }} />
|
||||||
|
|
||||||
<h1 class="header-title">Mindforge</h1>
|
<h1 class="header-title">Mindforge</h1>
|
||||||
</div>
|
</div>
|
||||||
|
{repoName && (
|
||||||
|
<div class="header-repo">
|
||||||
|
<span class="header-repo-icon">⎇</span>
|
||||||
|
<span class="header-repo-name">{repoName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,24 @@
|
|||||||
gap: 1.5rem;
|
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 {
|
.verificador-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useRef } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { MindforgeApiService } from '../services/MindforgeApiService';
|
import { MindforgeApiService } from '../services/MindforgeApiService';
|
||||||
|
import { FileTreeComponent } from './FileTreeComponent';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import * as diff from 'diff';
|
import * as diff from 'diff';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
@@ -13,62 +14,66 @@ function utf8ToBase64(str: string): string {
|
|||||||
|
|
||||||
type CheckTypeEnum = 'language' | 'content' | 'both';
|
type CheckTypeEnum = 'language' | 'content' | 'both';
|
||||||
|
|
||||||
export function VerificadorComponent() {
|
interface FileResult {
|
||||||
const [text, setText] = useState('');
|
path: string;
|
||||||
const [fileName, setFileName] = useState('manual_input.md');
|
fileName: string;
|
||||||
const [checkType, setCheckType] = useState<CheckTypeEnum>('language');
|
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 [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [results, setResults] = useState<FileResult[]>([]);
|
||||||
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 handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!text.trim()) {
|
if (selectedPaths.length === 0) {
|
||||||
setError('Por favor, insira algum texto ou faça upload de um arquivo.');
|
setError('Selecione pelo menos um arquivo do repositório.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setLanguageResult(null);
|
setResults([]);
|
||||||
setContentResult(null);
|
|
||||||
|
|
||||||
const base64Content = utf8ToBase64(text);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (checkType === 'both') {
|
const fileResults = await Promise.all(
|
||||||
const [langRes, contRes] = await Promise.all([
|
selectedPaths.map(async (path): Promise<FileResult> => {
|
||||||
MindforgeApiService.checkFile({ fileContent: base64Content, fileName, checkType: 'language' }),
|
const fileName = path.split('/').pop() ?? path;
|
||||||
MindforgeApiService.checkFile({ fileContent: base64Content, fileName, checkType: 'content' })
|
try {
|
||||||
]);
|
const { content } = await MindforgeApiService.getFileContent(path);
|
||||||
setLanguageResult(langRes.result);
|
const base64Content = utf8ToBase64(content);
|
||||||
setContentResult(contRes.result);
|
|
||||||
} else {
|
let languageResult: string | null = null;
|
||||||
const res = await MindforgeApiService.checkFile({ fileContent: base64Content, fileName, checkType });
|
let contentResult: string | null = null;
|
||||||
if (checkType === 'language') setLanguageResult(res.result);
|
|
||||||
else setContentResult(res.result);
|
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) {
|
} 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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -93,34 +98,17 @@ export function VerificadorComponent() {
|
|||||||
return (
|
return (
|
||||||
<div className="verificador-container">
|
<div className="verificador-container">
|
||||||
<h2 className="title" style={{ fontSize: '2.5rem' }}>Verificador de Arquivos</h2>
|
<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="verificador-form">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<label>Texto (Markdown)</label>
|
<label>Arquivos do Repositório</label>
|
||||||
<textarea
|
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
|
||||||
className="text-area"
|
{selectedPaths.length > 0 && (
|
||||||
value={text}
|
<div style={{ fontSize: '0.85rem', color: 'rgba(255,255,255,0.5)', marginTop: '0.4rem' }}>
|
||||||
onInput={(e) => setText((e.target as HTMLTextAreaElement).value)}
|
{selectedPaths.length} arquivo{selectedPaths.length !== 1 ? 's' : ''} selecionado{selectedPaths.length !== 1 ? 's' : ''}
|
||||||
placeholder="Cole seu texto aqui ou faça upload de um arquivo..."
|
</div>
|
||||||
/>
|
)}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
@@ -150,46 +138,55 @@ export function VerificadorComponent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Render Results */}
|
{!loading && results.length > 0 && (
|
||||||
{!loading && (languageResult || contentResult) && (
|
|
||||||
<div className="response-section">
|
<div className="response-section">
|
||||||
{checkType === 'language' && languageResult && (
|
{results.map((fileResult) => (
|
||||||
<div className="side-pane">
|
<div key={fileResult.path} className="file-result-block">
|
||||||
<div className="pane-title">Resultado - Linguagem (Diff)</div>
|
<div className="file-result-title">{fileResult.fileName}</div>
|
||||||
<div className="response-content">
|
|
||||||
{renderDiff(text, languageResult)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{checkType === 'content' && contentResult && (
|
{fileResult.error && (
|
||||||
<div className="side-pane">
|
<div style={{ color: '#ff7b72', padding: '0.5rem' }}>{fileResult.error}</div>
|
||||||
<div className="pane-title">Resultado - Conteúdo</div>
|
)}
|
||||||
<div
|
|
||||||
className="response-content markdown-body"
|
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(contentResult) as string }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{checkType === 'both' && languageResult && contentResult && (
|
{!fileResult.error && checkType === 'language' && fileResult.languageResult && (
|
||||||
<div className="side-by-side">
|
<div className="side-pane">
|
||||||
<div className="side-pane">
|
<div className="pane-title">Linguagem (Diff)</div>
|
||||||
<div className="pane-title">Linguagem (Diff)</div>
|
<div className="response-content">
|
||||||
<div className="response-content" style={{ minHeight: '300px' }}>
|
{renderDiff(fileResult.originalContent, fileResult.languageResult)}
|
||||||
{renderDiff(text, languageResult)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="side-pane">
|
|
||||||
<div className="pane-title">Conteúdo</div>
|
{!fileResult.error && checkType === 'content' && fileResult.contentResult && (
|
||||||
<div
|
<div className="side-pane">
|
||||||
className="response-content markdown-body"
|
<div className="pane-title">Conteúdo</div>
|
||||||
style={{ minHeight: '300px' }}
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(contentResult) as string }}
|
className="response-content markdown-body"
|
||||||
/>
|
dangerouslySetInnerHTML={{ __html: marked.parse(fileResult.contentResult) as string }}
|
||||||
</div>
|
/>
|
||||||
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5123';
|
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 {
|
export interface CheckFileRequest {
|
||||||
fileContent: string;
|
fileContent: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
@@ -52,5 +68,23 @@ export const MindforgeApiService = {
|
|||||||
throw new Error(`Error generating flashcards: ${response.statusText}`);
|
throw new Error(`Error generating flashcards: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
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();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user