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