diff --git a/APPdoc.csproj b/APPdoc.csproj new file mode 100644 index 0000000..da40cda --- /dev/null +++ b/APPdoc.csproj @@ -0,0 +1,152 @@ + + + + + Debug + AnyCPU + {AB18E4FD-5C49-46EF-BF2B-A8C7115EF346} + WinExe + APPdoc + APPdoc + v4.7.2 + 512 + true + true + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + packages\AngleSharp.1.2.0\lib\net472\AngleSharp.dll + + + packages\DocumentFormat.OpenXml.3.3.0\lib\net46\DocumentFormat.OpenXml.dll + + + packages\DocumentFormat.OpenXml.Framework.3.3.0\lib\net46\DocumentFormat.OpenXml.Framework.dll + + + packages\HtmlToOpenXml.dll.3.2.4\lib\net462\HtmlToOpenXml.dll + + + packages\Markdig.0.41.0\lib\net462\Markdig.dll + + + packages\Microsoft.Bcl.AsyncInterfaces.9.0.4\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + + packages\Microsoft.Extensions.Logging.Abstractions.6.0.0\lib\net461\Microsoft.Extensions.Logging.Abstractions.dll + + + + packages\System.Buffers.4.6.0\lib\net462\System.Buffers.dll + + + + + packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + True + True + + + packages\System.IO.Pipelines.9.0.4\lib\net462\System.IO.Pipelines.dll + + + packages\System.Memory.4.6.0\lib\net462\System.Memory.dll + + + + packages\System.Numerics.Vectors.4.6.0\lib\net462\System.Numerics.Vectors.dll + + + packages\System.Runtime.CompilerServices.Unsafe.6.1.0\lib\net462\System.Runtime.CompilerServices.Unsafe.dll + + + packages\System.Text.Encoding.CodePages.8.0.0\lib\net462\System.Text.Encoding.CodePages.dll + + + packages\System.Text.Encodings.Web.9.0.4\lib\net462\System.Text.Encodings.Web.dll + + + packages\System.Text.Json.9.0.4\lib\net462\System.Text.Json.dll + + + packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + + + + + + + + + + + + + + + Form + + + Form1.cs + + + + + Form1.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + + + Данный проект ссылается на пакеты NuGet, отсутствующие на этом компьютере. Используйте восстановление пакетов NuGet, чтобы скачать их. Дополнительную информацию см. по адресу: http://go.microsoft.com/fwlink/?LinkID=322105. Отсутствует следующий файл: {0}. + + + + \ No newline at end of file diff --git a/APPdoc.sln b/APPdoc.sln new file mode 100644 index 0000000..901d54b --- /dev/null +++ b/APPdoc.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35828.75 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "APPdoc", "APPdoc.csproj", "{AB18E4FD-5C49-46EF-BF2B-A8C7115EF346}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB18E4FD-5C49-46EF-BF2B-A8C7115EF346}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB18E4FD-5C49-46EF-BF2B-A8C7115EF346}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB18E4FD-5C49-46EF-BF2B-A8C7115EF346}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB18E4FD-5C49-46EF-BF2B-A8C7115EF346}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {45BF0B15-6273-4C05-A53E-06F937D82214} + EndGlobalSection +EndGlobal diff --git a/App.config b/App.config new file mode 100644 index 0000000..3aa25a2 --- /dev/null +++ b/App.config @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Form1.Designer.cs b/Form1.Designer.cs new file mode 100644 index 0000000..2bbb492 --- /dev/null +++ b/Form1.Designer.cs @@ -0,0 +1,169 @@ +namespace GiteaDocGenerator +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label1 = new System.Windows.Forms.Label(); + this.txtRepoUrl = new System.Windows.Forms.TextBox(); + this.label2 = new System.Windows.Forms.Label(); + this.txtSampleDocx = new System.Windows.Forms.TextBox(); + this.btnSelectSample = new System.Windows.Forms.Button(); + this.btnGenerate = new System.Windows.Forms.Button(); + this.lblStatus = new System.Windows.Forms.Label(); + this.label3 = new System.Windows.Forms.Label(); + this.txtApiKey = new System.Windows.Forms.TextBox(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 15); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(143, 13); + this.label1.TabIndex = 0; + this.label1.Text = "URL репозитория Gitea:"; + // + // txtRepoUrl + // + this.txtRepoUrl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtRepoUrl.Location = new System.Drawing.Point(161, 12); + this.txtRepoUrl.Name = "txtRepoUrl"; + this.txtRepoUrl.Size = new System.Drawing.Size(411, 20); + this.txtRepoUrl.TabIndex = 1; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 70); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(127, 13); + this.label2.TabIndex = 5; + this.label2.Text = "Файл-пример DOCX:"; + // + // txtSampleDocx + // + this.txtSampleDocx.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtSampleDocx.Location = new System.Drawing.Point(161, 67); + this.txtSampleDocx.Name = "txtSampleDocx"; + this.txtSampleDocx.ReadOnly = true; + this.txtSampleDocx.Size = new System.Drawing.Size(323, 20); + this.txtSampleDocx.TabIndex = 6; + // + // btnSelectSample + // + this.btnSelectSample.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnSelectSample.Location = new System.Drawing.Point(490, 65); + this.btnSelectSample.Name = "btnSelectSample"; + this.btnSelectSample.Size = new System.Drawing.Size(82, 23); + this.btnSelectSample.TabIndex = 7; + this.btnSelectSample.Text = "Выбрать..."; + this.btnSelectSample.UseVisualStyleBackColor = true; + this.btnSelectSample.Click += new System.EventHandler(this.btnSelectSample_Click); + // + // btnGenerate + // + this.btnGenerate.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.btnGenerate.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204))); + this.btnGenerate.Location = new System.Drawing.Point(15, 103); + this.btnGenerate.Name = "btnGenerate"; + this.btnGenerate.Size = new System.Drawing.Size(557, 35); + this.btnGenerate.TabIndex = 8; + this.btnGenerate.Text = "Сгенерировать"; + this.btnGenerate.UseVisualStyleBackColor = true; + this.btnGenerate.Click += new System.EventHandler(this.btnGenerate_Click); + // + // lblStatus + // + this.lblStatus.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.lblStatus.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; + this.lblStatus.Location = new System.Drawing.Point(12, 153); + this.lblStatus.Name = "lblStatus"; + this.lblStatus.Size = new System.Drawing.Size(560, 23); + this.lblStatus.TabIndex = 9; + this.lblStatus.Text = "Статус: Ожидание"; + this.lblStatus.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(12, 43); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(107, 13); + this.label3.TabIndex = 2; + this.label3.Text = "Google AI API Key:"; + // + // txtApiKey + // + this.txtApiKey.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtApiKey.Location = new System.Drawing.Point(161, 40); + this.txtApiKey.Name = "txtApiKey"; + this.txtApiKey.Size = new System.Drawing.Size(411, 20); + this.txtApiKey.TabIndex = 3; + //this.txtApiKey.UseSystemPasswordChar = true; // Можно раскомментировать, чтобы скрыть ключ звездочками + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(584, 188); + this.Controls.Add(this.txtApiKey); + this.Controls.Add(this.label3); + this.Controls.Add(this.lblStatus); + this.Controls.Add(this.btnGenerate); + this.Controls.Add(this.btnSelectSample); + this.Controls.Add(this.txtSampleDocx); + this.Controls.Add(this.label2); + this.Controls.Add(this.txtRepoUrl); + this.Controls.Add(this.label1); + this.MinimumSize = new System.Drawing.Size(450, 220); // Ограничение минимального размера + this.Name = "Form1"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Генератор Документации Gitea + Gemini"; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label label1; + private System.Windows.Forms.TextBox txtRepoUrl; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.TextBox txtSampleDocx; + private System.Windows.Forms.Button btnSelectSample; + private System.Windows.Forms.Button btnGenerate; + private System.Windows.Forms.Label lblStatus; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.TextBox txtApiKey; + } +} \ No newline at end of file diff --git a/Form1.cs b/Form1.cs new file mode 100644 index 0000000..f52b007 --- /dev/null +++ b/Form1.cs @@ -0,0 +1,433 @@ +using System; +using System.IO; +using System.IO.Compression; // Добавлено (или уже было) для ZipFile +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows.Forms; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using Markdig; +using HtmlToOpenXml; // Для конвертера HTML в DOCX +using System.Collections.Generic; // Для IList<> +using DocumentFormat.OpenXml; // Для OpenXmlCompositeElement +using System.Diagnostics; // <--- ДОБАВЛЕНО для Debug.WriteLine + +namespace GiteaDocGenerator +{ + public partial class Form1 : Form + { + private string _tempDir = string.Empty; // Для хранения скачанного кода + + public Form1() + { + InitializeComponent(); + } + + // --- Обработчики UI --- + + private void btnSelectSample_Click(object sender, EventArgs e) + { + using (OpenFileDialog ofd = new OpenFileDialog()) + { + ofd.Filter = "Word Documents (*.docx)|*.docx"; + ofd.Title = "Выберите файл-пример DOCX"; + if (ofd.ShowDialog() == DialogResult.OK) + { + txtSampleDocx.Text = ofd.FileName; + } + } + } + + private async void btnGenerate_Click(object sender, EventArgs e) + { + string repoUrl = txtRepoUrl.Text.Trim(); + string sampleDocxPath = txtSampleDocx.Text; + string apiKey = txtApiKey.Text.Trim(); // **НЕБЕЗОПАСНО!** + + if (string.IsNullOrWhiteSpace(repoUrl) || !Uri.IsWellFormedUriString(repoUrl, UriKind.Absolute)) + { + MessageBox.Show("Пожалуйста, введите корректный URL репозитория Gitea.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + if (string.IsNullOrWhiteSpace(sampleDocxPath) || !File.Exists(sampleDocxPath)) + { + MessageBox.Show("Пожалуйста, выберите существующий файл-пример DOCX.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + if (string.IsNullOrWhiteSpace(apiKey)) + { + MessageBox.Show("Пожалуйста, введите ваш Google AI API Key.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + // Блокируем UI на время работы + SetUiEnabled(false); + lblStatus.Text = "Статус: Скачивание репозитория..."; + + try + { + // 1. Скачивание и распаковка репозитория + _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDir); + string zipPath = await DownloadRepoAsync(repoUrl, _tempDir); + if (string.IsNullOrEmpty(zipPath)) return; // Ошибка уже показана в DownloadRepoAsync + + lblStatus.Text = "Статус: Распаковка архива..."; + string extractedPath = UnzipRepo(zipPath, _tempDir); + File.Delete(zipPath); // Удаляем zip после распаковки + + // Находим папку с кодом внутри (обычно имя_репо-ветка) + string repoSourcePath = Directory.GetDirectories(extractedPath).FirstOrDefault(); + if (repoSourcePath == null) + { + throw new DirectoryNotFoundException("Не удалось найти папку с исходным кодом после распаковки."); + } + + + // 2. Подготовка данных для ИИ + lblStatus.Text = "Статус: Чтение кода и структуры примера..."; + string codeContent = ReadCodeFiles(repoSourcePath); // Читаем код + string sampleStructure = ExtractStructureFromDocx(sampleDocxPath); // Читаем структуру DOCX + + if (string.IsNullOrWhiteSpace(codeContent)) + { + MessageBox.Show("Не удалось прочитать файлы кода из репозитория. Возможно, репозиторий пуст или содержит неподдерживаемые файлы.", "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + + // 3. Взаимодействие с Gemini API + lblStatus.Text = "Статус: Обращение к Google Gemini API..."; + string markdownResult = await CallGeminiApiAsync(apiKey, sampleStructure, codeContent); + + if (string.IsNullOrWhiteSpace(markdownResult)) + { + lblStatus.Text = "Статус: Ошибка API."; + MessageBox.Show("Получен пустой ответ от Gemini API.", "Ошибка API", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // 4. Создание DOCX из Markdown + lblStatus.Text = "Статус: Создание DOCX файла..."; + string outputDocxPath = ShowSaveDialog(); + if (!string.IsNullOrEmpty(outputDocxPath)) + { + await CreateDocxFromMarkdown(markdownResult, outputDocxPath, sampleDocxPath); + lblStatus.Text = $"Статус: Готово! Файл сохранен в {outputDocxPath}"; + MessageBox.Show($"Документация успешно сгенерирована и сохранена:\n{outputDocxPath}", "Успех", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + else + { + lblStatus.Text = "Статус: Генерация отменена."; + } + } + catch (Exception ex) + { + lblStatus.Text = "Статус: Ошибка!"; + string errorMessage = $"Произошла ошибка: {ex.Message}"; + if (ex.InnerException != null) + { + errorMessage += $"\n\nВнутренняя ошибка: {ex.InnerException.Message}\n\n{ex.InnerException.StackTrace}"; + } + errorMessage += $"\n\nStackTrace:\n{ex.StackTrace}"; + // Если в сообщении уже есть лог Markdown/HTML (добавленный в CreateDocxFromMarkdown), + // он будет включен здесь автоматически. + MessageBox.Show(errorMessage, "Критическая ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + SetUiEnabled(true); + CleanUpTempDir(); + } + } + + private void SetUiEnabled(bool enabled) + { + txtRepoUrl.Enabled = enabled; + txtSampleDocx.Enabled = enabled; + btnSelectSample.Enabled = enabled; + btnGenerate.Enabled = enabled; + txtApiKey.Enabled = enabled; + } + + private void CleanUpTempDir() + { + if (!string.IsNullOrEmpty(_tempDir) && Directory.Exists(_tempDir)) + { + try { Directory.Delete(_tempDir, true); } + catch (Exception ex) { Console.WriteLine($"Не удалось удалить временную папку {_tempDir}: {ex.Message}"); } + _tempDir = string.Empty; + } + } + + // --- Логика работы с Gitea --- + + private async Task DownloadRepoAsync(string repoUrl, string downloadDir) + { + Uri repoUri = new Uri(repoUrl); + string zipUrlMaster = $"{repoUri.GetLeftPart(UriPartial.Path).TrimEnd('/')}/archive/master.zip"; + string zipUrlMain = $"{repoUri.GetLeftPart(UriPartial.Path).TrimEnd('/')}/archive/main.zip"; + string zipPath = Path.Combine(downloadDir, "repo.zip"); + + using (HttpClient client = new HttpClient()) + { + client.DefaultRequestHeaders.Add("User-Agent", "GiteaDocGeneratorApp"); + HttpResponseMessage response = null; + try + { + response = await client.GetAsync(zipUrlMain); + if (!response.IsSuccessStatusCode) + { + lblStatus.Text = "Статус: Не удалось скачать main.zip, пробую master.zip..."; + response.Dispose(); + response = await client.GetAsync(zipUrlMaster); + } + response.EnsureSuccessStatusCode(); + using (var fs = new FileStream(zipPath, FileMode.CreateNew)) + { + await response.Content.CopyToAsync(fs); + } + return zipPath; + } + catch (HttpRequestException httpEx) + { + string message = $"Ошибка скачивания репозитория: {httpEx.Message}"; + if (response?.StatusCode == System.Net.HttpStatusCode.NotFound) + { + message += "\nУбедитесь, что URL верный, репозиторий публичный и содержит ветку 'main' или 'master'."; + } + else if (response != null) + { + message += $"\nКод ответа: {response.StatusCode}"; + } + MessageBox.Show(message, "Ошибка скачивания", MessageBoxButtons.OK, MessageBoxIcon.Error); + return null; + } + finally + { + response?.Dispose(); + } + } + } + + private string UnzipRepo(string zipPath, string extractPath) + { + ZipFile.ExtractToDirectory(zipPath, extractPath); + return extractPath; + } + + // --- Подготовка данных --- + + private string ReadCodeFiles(string sourcePath, int maxChars = 15000) + { + StringBuilder content = new StringBuilder(); + string[] extensions = { ".cs", ".md", ".txt", ".py", ".js", ".html", ".css", ".java", ".go", ".php", "Dockerfile", ".yaml", ".yml", ".json" }; + string basePathWithTrailingSlash = sourcePath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? sourcePath : sourcePath + Path.DirectorySeparatorChar; + Uri sourceUri = new Uri(basePathWithTrailingSlash); + + var readmeFiles = Directory.GetFiles(sourcePath, "README.md", SearchOption.AllDirectories).Concat(Directory.GetFiles(sourcePath, "README.txt", SearchOption.AllDirectories)); + foreach (var file in readmeFiles) + { + if (content.Length >= maxChars) break; + try + { + Uri fileUri = new Uri(file); + string relativePath = Uri.UnescapeDataString(sourceUri.MakeRelativeUri(fileUri).ToString()).Replace('/', Path.DirectorySeparatorChar); + content.AppendLine($"--- Файл: {relativePath} ---"); + content.AppendLine(File.ReadAllText(file)); + content.AppendLine("--- Конец файла ---"); + content.AppendLine(); + } + catch (Exception ex) { Console.WriteLine($"Ошибка чтения файла {file}: {ex.Message}"); } + } + + var codeFiles = Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories).Where(f => extensions.Contains(Path.GetExtension(f).ToLowerInvariant()) && !Path.GetFileName(f).StartsWith("README.", StringComparison.OrdinalIgnoreCase)); + foreach (var file in codeFiles) + { + if (content.Length >= maxChars) { content.AppendLine("\n[...Остальные файлы кода пропущены из-за ограничения размера...]"); break; } + try + { + string fileContent = File.ReadAllText(file); + if (!string.IsNullOrWhiteSpace(fileContent)) + { + Uri fileUri = new Uri(file); + string relativePath = Uri.UnescapeDataString(sourceUri.MakeRelativeUri(fileUri).ToString()).Replace('/', Path.DirectorySeparatorChar); + content.AppendLine($"--- Файл: {relativePath} ---"); + content.AppendLine(fileContent); + content.AppendLine("--- Конец файла ---"); + content.AppendLine(); + } + } + catch (Exception ex) { Console.WriteLine($"Ошибка чтения файла {file}: {ex.Message}"); } + } + return content.ToString(); + } + + private string ExtractStructureFromDocx(string docxPath) + { + StringBuilder structure = new StringBuilder(); + try + { + using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(docxPath, false)) + { + Body body = wordDoc.MainDocumentPart.Document.Body; + foreach (var element in body.Elements()) + { + string styleId = element.ParagraphProperties?.ParagraphStyleId?.Val?.Value; + bool isHeading = styleId != null && styleId.StartsWith("Heading", StringComparison.OrdinalIgnoreCase) || styleId == "Заголовок"; + if (isHeading) { structure.AppendLine($"{(isHeading ? "## " : "")}{element.InnerText}"); } + else if (!string.IsNullOrWhiteSpace(element.InnerText)) { if (structure.Length < 2000) { structure.AppendLine(element.InnerText); } } + } + if (structure.Length >= 2000) { structure.AppendLine("\n[...Структура примера обрезана...]"); } + } + } + catch (Exception ex) + { + MessageBox.Show($"Не удалось прочитать структуру из файла DOCX: {ex.Message}. Будет использован стандартный промпт.", "Ошибка чтения DOCX", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return "Заголовок 1\n\nВведение\n\nОсновная часть\n\nЗаключение"; + } + if (string.IsNullOrWhiteSpace(structure.ToString())) { return "Заголовок 1\n\nВведение\n\nОсновная часть\n\nЗаключение"; } + return structure.ToString(); + } + + // --- Взаимодействие с Gemini API --- + + private async Task CallGeminiApiAsync(string apiKey, string sampleStructure, string codeContent) + { + string apiUrl = $"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest:generateContent?key={apiKey}"; + string prompt = $@"Сгенерируй документацию для программного продукта, исходный код которого приведен ниже. +Документация должна строго следовать структуре, представленной в следующем примере (используй заголовки и порядок разделов как в примере): + +--- НАЧАЛО СТРУКТУРЫ ПРИМЕРА --- +{sampleStructure} +--- КОНЕЦ СТРУКТУРЫ ПРИМЕРА --- + +Содержимое документации должно описывать код продукта. Если кода нет, напиши общую документацию по указанной структуре. +ВЕСЬ ТВОЙ ОТВЕТ ДОЛЖЕН БЫТЬ ТОЛЬКО В ФОРМАТЕ MARKDOWN. Не добавляй никаких пояснений до или после markdown. + +--- НАЧАЛО КОДА ПРОДУКТА --- +{codeContent} +--- КОНЕЦ КОДА ПРОДУКТА --- + +Еще раз: ВЕСЬ ТВОЙ ОТВЕТ должен быть строго в формате Markdown."; + + var requestBody = new { contents = new[] { new { parts = new[] { new { text = prompt } } } }, generationConfig = new { temperature = 0.6 } }; + string jsonPayload = JsonSerializer.Serialize(requestBody); + + using (HttpClient client = new HttpClient()) + { + try + { + var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + HttpResponseMessage response = await client.PostAsync(apiUrl, content); + string responseBody = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) { string errorDetails = TryExtractErrorFromJson(responseBody); throw new Exception($"Ошибка API Gemini: {response.StatusCode}. {errorDetails}"); } + + using (JsonDocument document = JsonDocument.Parse(responseBody)) + { + if (document.RootElement.TryGetProperty("candidates", out JsonElement candidates) && candidates.GetArrayLength() > 0) + { + if (candidates[0].TryGetProperty("content", out JsonElement responseContent) && responseContent.TryGetProperty("parts", out JsonElement parts) && parts.GetArrayLength() > 0) + { + if (parts[0].TryGetProperty("text", out JsonElement textElement)) { return textElement.GetString(); } + } + if (candidates[0].TryGetProperty("finishReason", out JsonElement reason)) + { + string reasonStr = reason.GetString(); string safetyInfo = ""; + if (reasonStr == "SAFETY" && candidates[0].TryGetProperty("safetyRatings", out JsonElement ratings)) { safetyInfo = $" Safety Ratings: {ratings.ToString()}"; } + throw new Exception($"Ответ API Gemini не содержит текст. Причина завершения: {reasonStr}.{safetyInfo}"); + } + } + throw new Exception($"Не удалось извлечь текст из ответа API Gemini. Ответ:\n{responseBody}"); + } + } + catch (JsonException jsonEx) { throw new Exception($"Ошибка парсинга JSON ответа от API Gemini: {jsonEx.Message}. Ответ:\n{jsonPayload}", jsonEx); } + catch (Exception ex) { throw new Exception($"Ошибка при вызове Gemini API: {ex.Message}", ex); } + } + } + + private string TryExtractErrorFromJson(string jsonResponse) + { + if (string.IsNullOrWhiteSpace(jsonResponse)) return "(пустой ответ)"; + try { using (JsonDocument document = JsonDocument.Parse(jsonResponse)) { if (document.RootElement.TryGetProperty("error", out JsonElement errorElement)) { if (errorElement.TryGetProperty("message", out JsonElement messageElement)) { return messageElement.GetString(); } return errorElement.ToString(); } } } + catch (JsonException) { } + return jsonResponse; + } + + // --- Создание DOCX --- + + private string ShowSaveDialog() + { + using (SaveFileDialog sfd = new SaveFileDialog()) + { + sfd.Filter = "Word Document (*.docx)|*.docx"; sfd.Title = "Сохранить документацию как..."; sfd.FileName = "Generated_Documentation.docx"; + if (sfd.ShowDialog() == DialogResult.OK) { return sfd.FileName; } + return null; + } + } + + // Метод остается асинхронным (async Task) + // Метод остается асинхронным (async Task) + private async Task CreateDocxFromMarkdown(string markdown, string outputPath, string sampleDocxPath) + { + // --- Логирование Markdown --- + Debug.WriteLine("--- ИСХОДНЫЙ MARKDOWN ---"); + Debug.WriteLine(markdown ?? "[NULL]"); + Debug.WriteLine("--- КОНЕЦ MARKDOWN ---"); + // --------------------------- + + var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); + string html = "[NULL]"; + if (markdown != null) + { + html = Markdown.ToHtml(markdown, pipeline); + } + + // --- Логирование HTML --- + Debug.WriteLine("--- ПОЛУЧЕННЫЙ HTML ---"); + Debug.WriteLine(html); + Debug.WriteLine("--- КОНЕЦ HTML ---"); + // ------------------------ + + try + { + // Шаг 1: Клонирование шаблона. Шаблон будет закрыт сразу после клонирования. + using (WordprocessingDocument templateDoc = WordprocessingDocument.Open(sampleDocxPath, false)) + { + // Клонируем шаблон в ВЫХОДНОЙ файл и открываем для редактирования + using (WordprocessingDocument wordDoc = (WordprocessingDocument)templateDoc.Clone(outputPath, true)) + { + // Шаг 2: Работа с клонированным документом (wordDoc) + MainDocumentPart mainPart = wordDoc.MainDocumentPart; + Body body = mainPart.Document.Body; + + // !!! ОЧИЩАЕМ ТЕЛО ДОКУМЕНТА !!! + body.RemoveAllChildren(); + body.RemoveAllChildren(); + + // Шаг 3: Используем HtmlConverter для преобразования HTML и добавления в пустое тело + HtmlConverter converter = new HtmlConverter(mainPart); + await converter.ParseHtml(html); // ParseHtml добавляет элементы в mainPart.Document.Body + + // Шаг 4: Плейсхолдер НЕ НУЖЕН и его поиск/удаление НЕ НУЖНЫ + + // Сохранение происходит автоматически при выходе из using (wordDoc) + } // wordDoc закрывается и сохраняется здесь + } // templateDoc закрывается здесь + } + catch (IOException ioEx) + { + string logInfo = $"\n\n--- Markdown Log ---\n{markdown ?? "[NULL]"}\n--- HTML Log ---\n{html}"; + throw new IOException($"Ошибка доступа к файлу при клонировании/создании DOCX '{outputPath}': {ioEx.Message}. Убедитесь, что файл не открыт в другой программе.{logInfo}", ioEx); + } + catch (Exception ex) + { + // Убираем проверку на InvalidOperationException, так как плейсхолдера нет + string logInfo = $"\n\n--- Markdown Log ---\n{markdown ?? "[NULL]"}\n--- HTML Log ---\n{html}"; + throw new Exception($"Ошибка при создании DOCX файла из Markdown/HTML:\n{ex.Message}{logInfo}", ex); + } + } + } +} \ No newline at end of file diff --git a/Form1.resx b/Form1.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Form1.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..109f65b --- /dev/null +++ b/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using GiteaDocGenerator; // <--- ДОБАВЛЕННАЯ СТРОКА + +namespace APPdoc // Или другое имя корневого пространства имен вашего проекта +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); // Теперь компилятор знает, где искать Form1 + } + } +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d093cf5 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Общие сведения об этой сборке предоставляются следующим набором +// набора атрибутов. Измените значения этих атрибутов для изменения сведений, +// связанных со сборкой. +[assembly: AssemblyTitle("APPdoc")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("APPdoc")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми +// для компонентов COM. Если необходимо обратиться к типу в этой сборке через +// COM, следует установить атрибут ComVisible в TRUE для этого типа. +[assembly: ComVisible(false)] + +// Следующий GUID служит для идентификации библиотеки типов, если этот проект будет видимым для COM +[assembly: Guid("ab18e4fd-5c49-46ef-bf2b-a8c7115ef346")] + +// Сведения о версии сборки состоят из указанных ниже четырех значений: +// +// Основной номер версии +// Дополнительный номер версии +// Номер сборки +// Редакция +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs new file mode 100644 index 0000000..963d12c --- /dev/null +++ b/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// Этот код создан программным средством. +// Версия среды выполнения: 4.0.30319.42000 +// +// Изменения в этом файле могут привести к неправильному поведению и будут утрачены, если +// код создан повторно. +// +//------------------------------------------------------------------------------ + +namespace APPdoc.Properties +{ + + + /// + /// Класс ресурсов со строгим типом для поиска локализованных строк и пр. + /// + // Этот класс был автоматически создан при помощи StronglyTypedResourceBuilder + // класс с помощью таких средств, как ResGen или Visual Studio. + // Для добавления или удаления члена измените файл .ResX, а затем перезапустите ResGen + // с параметром /str или заново постройте свой VS-проект. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Возврат кэшированного экземпляра ResourceManager, используемого этим классом. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("APPdoc.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Переопределяет свойство CurrentUICulture текущего потока для всех + /// подстановки ресурсов с помощью этого класса ресурсов со строгим типом. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/Properties/Resources.resx b/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs new file mode 100644 index 0000000..5c2cf27 --- /dev/null +++ b/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace APPdoc.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/Properties/Settings.settings b/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..1aa5db7 --- /dev/null +++ b/packages.config @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file