diff --git a/CommentHandler.cs b/CommentHandler.cs index 8f7c106..a5694f7 100644 --- a/CommentHandler.cs +++ b/CommentHandler.cs @@ -42,11 +42,17 @@ public static async void Document_CommentsEventHandler(Word.Selection selection) Comment reply = c.Replies[i]; chatHistory.Add((i % 2 == 1) ? new UserChatMessage(reply.Range.Text) : new AssistantChatMessage(reply.Range.Text)); } - await AddComment( - c.Replies, - c.Range, - RAGControl.AskQuestion(Forge.CommentSystemPrompt, chatHistory, CommonUtils.GetActiveDocument().Range()) - ); + try + { + await AddComment( + c.Replies, + c.Range, + RAGControl.AskQuestion(Forge.CommentSystemPrompt, chatHistory, CommonUtils.GetActiveDocument().Range()) + ); + } catch (OperationCanceledException ex) + { + CommonUtils.DisplayWarning(ex); + } numComments++; _isDraftingComment = false; diff --git a/CommonUtils.cs b/CommonUtils.cs index 81791da..2002dcd 100644 --- a/CommonUtils.cs +++ b/CommonUtils.cs @@ -13,6 +13,22 @@ public static void DisplayError(Exception ex) MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error); } + public static void DisplayWarning(Exception ex) + { + MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + + public static void DisplayInformation(Exception ex) + { + MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + public static bool GetInternetAccessPermission(string url) + { + var result = MessageBox.Show($"Do you want to allow TextCraft to access the following internet resource?{Environment.NewLine}{url}", "Internet Access", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + return result == DialogResult.Yes; + } + public static Word.Application GetApplication() { return Globals.ThisAddIn.Application; diff --git a/Forge.cs b/Forge.cs index 52d8ba0..d055f87 100644 --- a/Forge.cs +++ b/Forge.cs @@ -2,7 +2,6 @@ using System.ClientModel; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -197,7 +196,13 @@ private static async Task ReviewButton_Click() if (Globals.ThisAddIn.Application.Selection.End - Globals.ThisAddIn.Application.Selection.Start > 0) { var selectionRange = CommonUtils.GetSelectionRange(); - await CommentHandler.AddComment(CommonUtils.GetComments(), selectionRange, Review(paragraphs, selectionRange, prompt)); + try + { + await CommentHandler.AddComment(CommonUtils.GetComments(), selectionRange, Review(paragraphs, selectionRange, prompt)); + } catch (OperationCanceledException ex) + { + CommonUtils.DisplayWarning(ex); + } hasCommented = true; } else @@ -235,14 +240,21 @@ private static async Task AnalyzeText(string systemPrompt, string userPrompt) var selectionRange = Globals.ThisAddIn.Application.Selection.Range; var range = (selectionRange.End - selectionRange.Start > 0) ? selectionRange : throw new InvalidRangeException("No text is selected for analysis!"); - ChatClient client = new ChatClient(ThisAddIn.Model, ThisAddIn.ApiKey, ThisAddIn.ClientOptions); + ChatClient client = new ChatClient(ThisAddIn.Model, new ApiKeyCredential(ThisAddIn.ApiKey), ThisAddIn.ClientOptions); var streamingAnswer = client.CompleteChatStreamingAsync( new List() { new SystemChatMessage(systemPrompt), new UserChatMessage(@$"{userPrompt}: {range.Text}") }, - new ChatCompletionOptions() { MaxTokens = ThisAddIn.ContextLength }, + new ChatCompletionOptions() { MaxOutputTokenCount = ThisAddIn.ContextLength }, ThisAddIn.CancellationTokenSource.Token ); + range.Delete(); - await AddStreamingContentToRange(streamingAnswer, range); + try + { + await AddStreamingContentToRange(streamingAnswer, range); + } catch (OperationCanceledException ex) + { + CommonUtils.DisplayWarning(ex); + } Globals.ThisAddIn.Application.Selection.SetRange(range.Start, range.End); } diff --git a/GenerateUserControl.cs b/GenerateUserControl.cs index 504100d..b9cb5eb 100644 --- a/GenerateUserControl.cs +++ b/GenerateUserControl.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Windows.Forms; using OpenAI.Chat; -using static System.Net.Mime.MediaTypeNames; namespace TextForge { @@ -27,7 +25,7 @@ private async void GenerateButton_Click(object sender, EventArgs e) { string textBoxContent = this.PromptTextBox.Text; if (textBoxContent.Length == 0) - throw new ArgumentException("The textbox is empty!"); + throw new TextBoxEmptyException("The textbox is empty!"); /* * So, If the user changes the selection carot in Word after clicking "generate" (bc it takes so long to generate text). @@ -48,13 +46,13 @@ private async void GenerateButton_Click(object sender, EventArgs e) await Forge.AddStreamingContentToRange(streamingAnswer, rangeBeforeChat); Globals.ThisAddIn.Application.Selection.SetRange(rangeBeforeChat.Start, rangeBeforeChat.End); } - catch (ArgumentException ex) + catch (TextBoxEmptyException ex) { - MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Information); + CommonUtils.DisplayInformation(ex); } catch (OperationCanceledException ex) { - MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Warning); + CommonUtils.DisplayWarning(ex); } catch (Exception ex) { @@ -112,6 +110,10 @@ private void PromptTextBox_KeyDown(object sender, KeyEventArgs e) CommonUtils.DisplayError(ex); } } + } + public class TextBoxEmptyException : ArgumentException + { + public TextBoxEmptyException(string message) : base(message) { } } } diff --git a/ModelProperties.cs b/ModelProperties.cs index 1d20c99..0c19070 100644 --- a/ModelProperties.cs +++ b/ModelProperties.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using OpenAI.Models; namespace TextForge @@ -10,6 +8,7 @@ namespace TextForge internal class ModelProperties { // Public + public const int BaselineContextWindowLength = 4096; // Change this if necessary public static List UniqueEmbedModels { get { return _embedModels; } } // Private @@ -140,6 +139,12 @@ internal class ModelProperties // new models { "minicpm-v", 32768 }, { "reader-lm", 256000 }, + { "mistral-small", 131072 }, + { "bespoke-minicheck", 32768 }, + { "qwen2.5", 32768 }, + { "nemotron-mini", 4096 }, + { "solar-pro", 4096 }, + { "qwen2.5-coder", 32768 } }; public static int GetContextLength(string modelName) @@ -151,7 +156,7 @@ public static int GetContextLength(string modelName) else if (modelName.Contains(':')) { string key = modelName.Split(':')[0]; - return ollamaModelsContextLength.ContainsKey(key) ? ollamaModelsContextLength[key] : ThisAddIn.BaselineContextWindowLength; + return ollamaModelsContextLength.ContainsKey(key) ? ollamaModelsContextLength[key] : BaselineContextWindowLength; } else if (modelName.StartsWith("o1")) { @@ -175,7 +180,7 @@ public static int GetContextLength(string modelName) } else { - return ThisAddIn.BaselineContextWindowLength; + return BaselineContextWindowLength; } } diff --git a/OfficeAddInSetup/OfficeAddInSetup.vdproj b/OfficeAddInSetup/OfficeAddInSetup.vdproj index c796a55..093257f 100644 --- a/OfficeAddInSetup/OfficeAddInSetup.vdproj +++ b/OfficeAddInSetup/OfficeAddInSetup.vdproj @@ -124,7 +124,7 @@ "Entry" { "MsmKey" = "8:_0D2B853494A572309174FECF1799A7D3" - "OwnerKey" = "8:_DA0B4CDB8FBB8A6765A2CF4AAD25D94D" + "OwnerKey" = "8:_7E6AB4C9D0EB589A734A6534537A2EF4" "MsmSig" = "8:_UNDEFINED" } "Entry" @@ -177,6 +177,18 @@ } "Entry" { + "MsmKey" = "8:_2D3AB0270E6A168063D8B06C28CC64BD" + "OwnerKey" = "8:_7E6AB4C9D0EB589A734A6534537A2EF4" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_2D3AB0270E6A168063D8B06C28CC64BD" + "OwnerKey" = "8:_5DA1C96D6C89410AA5F3D85C50EA4EE0" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { "MsmKey" = "8:_2F44424F9CD843DA80F8EDA29E0E1B79" "OwnerKey" = "8:_UNDEFINED" "MsmSig" = "8:_UNDEFINED" @@ -346,13 +358,13 @@ "Entry" { "MsmKey" = "8:_47419264F8E9C7478516E89FBAB483C3" - "OwnerKey" = "8:_891F0FCFF46B2004DA972770601FD360" + "OwnerKey" = "8:_2D3AB0270E6A168063D8B06C28CC64BD" "MsmSig" = "8:_UNDEFINED" } "Entry" { "MsmKey" = "8:_47419264F8E9C7478516E89FBAB483C3" - "OwnerKey" = "8:_FC0B70EA8C384379DE6F635A416EE180" + "OwnerKey" = "8:_C2CD9425FBD02E29232D14C86940816B" "MsmSig" = "8:_UNDEFINED" } "Entry" @@ -370,13 +382,13 @@ "Entry" { "MsmKey" = "8:_5C6B0E1DD3DDFEB1C509E3A5F41B8225" - "OwnerKey" = "8:_FC0B70EA8C384379DE6F635A416EE180" + "OwnerKey" = "8:_C2CD9425FBD02E29232D14C86940816B" "MsmSig" = "8:_UNDEFINED" } "Entry" { "MsmKey" = "8:_5C6B0E1DD3DDFEB1C509E3A5F41B8225" - "OwnerKey" = "8:_891F0FCFF46B2004DA972770601FD360" + "OwnerKey" = "8:_2D3AB0270E6A168063D8B06C28CC64BD" "MsmSig" = "8:_UNDEFINED" } "Entry" @@ -411,32 +423,32 @@ } "Entry" { - "MsmKey" = "8:_78CD14CCA6F9AEB890ADD4E21A2927EC" + "MsmKey" = "8:_67AEF62D067A6AEE307A7BDE5DE8EDA9" "OwnerKey" = "8:_5DA1C96D6C89410AA5F3D85C50EA4EE0" "MsmSig" = "8:_UNDEFINED" } "Entry" { - "MsmKey" = "8:_7F6D01269308525743C7EC1272A5B76F" - "OwnerKey" = "8:_5C6B0E1DD3DDFEB1C509E3A5F41B8225" + "MsmKey" = "8:_78CD14CCA6F9AEB890ADD4E21A2927EC" + "OwnerKey" = "8:_5DA1C96D6C89410AA5F3D85C50EA4EE0" "MsmSig" = "8:_UNDEFINED" } "Entry" { - "MsmKey" = "8:_891F0FCFF46B2004DA972770601FD360" - "OwnerKey" = "8:_DA0B4CDB8FBB8A6765A2CF4AAD25D94D" + "MsmKey" = "8:_7E6AB4C9D0EB589A734A6534537A2EF4" + "OwnerKey" = "8:_5DA1C96D6C89410AA5F3D85C50EA4EE0" "MsmSig" = "8:_UNDEFINED" } "Entry" { - "MsmKey" = "8:_891F0FCFF46B2004DA972770601FD360" - "OwnerKey" = "8:_5DA1C96D6C89410AA5F3D85C50EA4EE0" + "MsmKey" = "8:_7F6D01269308525743C7EC1272A5B76F" + "OwnerKey" = "8:_5C6B0E1DD3DDFEB1C509E3A5F41B8225" "MsmSig" = "8:_UNDEFINED" } "Entry" { "MsmKey" = "8:_8E27D187F943E65FFE9AD2F4C0143661" - "OwnerKey" = "8:_DA0B4CDB8FBB8A6765A2CF4AAD25D94D" + "OwnerKey" = "8:_7E6AB4C9D0EB589A734A6534537A2EF4" "MsmSig" = "8:_UNDEFINED" } "Entry" @@ -478,7 +490,7 @@ "Entry" { "MsmKey" = "8:_A5566B0A2E7D03762124FB7763FBA7DA" - "OwnerKey" = "8:_DA0B4CDB8FBB8A6765A2CF4AAD25D94D" + "OwnerKey" = "8:_7E6AB4C9D0EB589A734A6534537A2EF4" "MsmSig" = "8:_UNDEFINED" } "Entry" @@ -567,6 +579,24 @@ } "Entry" { + "MsmKey" = "8:_C2CD9425FBD02E29232D14C86940816B" + "OwnerKey" = "8:_2D3AB0270E6A168063D8B06C28CC64BD" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_C2CD9425FBD02E29232D14C86940816B" + "OwnerKey" = "8:_5DA1C96D6C89410AA5F3D85C50EA4EE0" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_C2CD9425FBD02E29232D14C86940816B" + "OwnerKey" = "8:_7E6AB4C9D0EB589A734A6534537A2EF4" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { "MsmKey" = "8:_C2F42BA801692489F910DED594B693EE" "OwnerKey" = "8:_8E27D187F943E65FFE9AD2F4C0143661" "MsmSig" = "8:_UNDEFINED" @@ -597,12 +627,6 @@ } "Entry" { - "MsmKey" = "8:_DA0B4CDB8FBB8A6765A2CF4AAD25D94D" - "OwnerKey" = "8:_5DA1C96D6C89410AA5F3D85C50EA4EE0" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { "MsmKey" = "8:_EB728693CAD8918A0EAB28C472D2B8D2" "OwnerKey" = "8:_5EB2A0F3EB10CBA67898753C34F87EBD" "MsmSig" = "8:_UNDEFINED" @@ -699,24 +723,6 @@ } "Entry" { - "MsmKey" = "8:_FC0B70EA8C384379DE6F635A416EE180" - "OwnerKey" = "8:_891F0FCFF46B2004DA972770601FD360" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_FC0B70EA8C384379DE6F635A416EE180" - "OwnerKey" = "8:_5DA1C96D6C89410AA5F3D85C50EA4EE0" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { - "MsmKey" = "8:_FC0B70EA8C384379DE6F635A416EE180" - "OwnerKey" = "8:_DA0B4CDB8FBB8A6765A2CF4AAD25D94D" - "MsmSig" = "8:_UNDEFINED" - } - "Entry" - { "MsmKey" = "8:_FEEE914780312C9ADC4FD9FB1EEC387F" "OwnerKey" = "8:_5DA1C96D6C89410AA5F3D85C50EA4EE0" "MsmSig" = "8:_UNDEFINED" @@ -832,7 +838,7 @@ "Entry" { "MsmKey" = "8:_UNDEFINED" - "OwnerKey" = "8:_DA0B4CDB8FBB8A6765A2CF4AAD25D94D" + "OwnerKey" = "8:_7E6AB4C9D0EB589A734A6534537A2EF4" "MsmSig" = "8:_UNDEFINED" } "Entry" @@ -916,6 +922,12 @@ "Entry" { "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_67AEF62D067A6AEE307A7BDE5DE8EDA9" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" "OwnerKey" = "8:_5C6B0E1DD3DDFEB1C509E3A5F41B8225" "MsmSig" = "8:_UNDEFINED" } @@ -1317,6 +1329,37 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_2D3AB0270E6A168063D8B06C28CC64BD" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:OpenAI, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b4187f3e65366280, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_2D3AB0270E6A168063D8B06C28CC64BD" + { + "Name" = "8:OpenAI.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:OpenAI.dll" + "TargetName" = "8:" + "Tag" = "8:" + "Folder" = "8:_AF016B7856C74F08A06EC9F3AA46C574" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:TRUE" + "IsolateTo" = "8:" + } "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_2F44424F9CD843DA80F8EDA29E0E1B79" { "SourcePath" = "8:Setup License File.rtf" @@ -1605,6 +1648,37 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_67AEF62D067A6AEE307A7BDE5DE8EDA9" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" + "ScatterAssemblies" + { + "_67AEF62D067A6AEE307A7BDE5DE8EDA9" + { + "Name" = "8:System.Net.Http.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:System.Net.Http.dll" + "TargetName" = "8:" + "Tag" = "8:" + "Folder" = "8:_AF016B7856C74F08A06EC9F3AA46C574" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:TRUE" + "IsolateTo" = "8:" + } "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_78CD14CCA6F9AEB890ADD4E21A2927EC" { "AssemblyRegister" = "3:1" @@ -1636,20 +1710,20 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_7F6D01269308525743C7EC1272A5B76F" + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_7E6AB4C9D0EB589A734A6534537A2EF4" { "AssemblyRegister" = "3:1" - "AssemblyIsInGAC" = "11:TRUE" - "AssemblyAsmDisplayName" = "8:System.Diagnostics.Tracing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:HyperVectorDB, Version=1.0.5.0, Culture=neutral, processorArchitecture=MSIL" "ScatterAssemblies" { - "_7F6D01269308525743C7EC1272A5B76F" + "_7E6AB4C9D0EB589A734A6534537A2EF4" { - "Name" = "8:System.Diagnostics.Tracing.dll" + "Name" = "8:HyperVectorDB.dll" "Attributes" = "3:512" } } - "SourcePath" = "8:System.Diagnostics.Tracing.dll" + "SourcePath" = "8:HyperVectorDB.dll" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_AF016B7856C74F08A06EC9F3AA46C574" @@ -1667,20 +1741,20 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_891F0FCFF46B2004DA972770601FD360" + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_7F6D01269308525743C7EC1272A5B76F" { "AssemblyRegister" = "3:1" - "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:OpenAI, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b4187f3e65366280, processorArchitecture=MSIL" + "AssemblyIsInGAC" = "11:TRUE" + "AssemblyAsmDisplayName" = "8:System.Diagnostics.Tracing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" "ScatterAssemblies" { - "_891F0FCFF46B2004DA972770601FD360" + "_7F6D01269308525743C7EC1272A5B76F" { - "Name" = "8:OpenAI.dll" + "Name" = "8:System.Diagnostics.Tracing.dll" "Attributes" = "3:512" } } - "SourcePath" = "8:OpenAI.dll" + "SourcePath" = "8:System.Diagnostics.Tracing.dll" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_AF016B7856C74F08A06EC9F3AA46C574" @@ -2068,20 +2142,20 @@ "IsDependency" = "11:FALSE" "IsolateTo" = "8:" } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_C2F42BA801692489F910DED594B693EE" + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_C2CD9425FBD02E29232D14C86940816B" { "AssemblyRegister" = "3:1" "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:Microsoft.NET.StringTools, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" + "AssemblyAsmDisplayName" = "8:System.ClientModel, Version=1.1.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8, processorArchitecture=MSIL" "ScatterAssemblies" { - "_C2F42BA801692489F910DED594B693EE" + "_C2CD9425FBD02E29232D14C86940816B" { - "Name" = "8:Microsoft.NET.StringTools.dll" + "Name" = "8:System.ClientModel.dll" "Attributes" = "3:512" } } - "SourcePath" = "8:Microsoft.NET.StringTools.dll" + "SourcePath" = "8:System.ClientModel.dll" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_AF016B7856C74F08A06EC9F3AA46C574" @@ -2099,20 +2173,20 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_CC0708ED1FCE2BF93BD1A209EAC99639" + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_C2F42BA801692489F910DED594B693EE" { "AssemblyRegister" = "3:1" "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL" + "AssemblyAsmDisplayName" = "8:Microsoft.NET.StringTools, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" "ScatterAssemblies" { - "_CC0708ED1FCE2BF93BD1A209EAC99639" + "_C2F42BA801692489F910DED594B693EE" { - "Name" = "8:System.ValueTuple.dll" + "Name" = "8:Microsoft.NET.StringTools.dll" "Attributes" = "3:512" } } - "SourcePath" = "8:System.ValueTuple.dll" + "SourcePath" = "8:Microsoft.NET.StringTools.dll" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_AF016B7856C74F08A06EC9F3AA46C574" @@ -2130,20 +2204,20 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_D6DCC622A4634ECBD4733DB016770C60" + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_CC0708ED1FCE2BF93BD1A209EAC99639" { "AssemblyRegister" = "3:1" "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:Microsoft.Office.Tools.v4.0.Framework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" + "AssemblyAsmDisplayName" = "8:System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL" "ScatterAssemblies" { - "_D6DCC622A4634ECBD4733DB016770C60" + "_CC0708ED1FCE2BF93BD1A209EAC99639" { - "Name" = "8:Microsoft.Office.Tools.v4.0.Framework.dll" + "Name" = "8:System.ValueTuple.dll" "Attributes" = "3:512" } } - "SourcePath" = "8:Microsoft.Office.Tools.v4.0.Framework.dll" + "SourcePath" = "8:System.ValueTuple.dll" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_AF016B7856C74F08A06EC9F3AA46C574" @@ -2161,20 +2235,20 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_DA0B4CDB8FBB8A6765A2CF4AAD25D94D" + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_D6DCC622A4634ECBD4733DB016770C60" { "AssemblyRegister" = "3:1" "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:HyperVectorDB, Version=1.0.3.0, Culture=neutral, processorArchitecture=MSIL" + "AssemblyAsmDisplayName" = "8:Microsoft.Office.Tools.v4.0.Framework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" "ScatterAssemblies" { - "_DA0B4CDB8FBB8A6765A2CF4AAD25D94D" + "_D6DCC622A4634ECBD4733DB016770C60" { - "Name" = "8:HyperVectorDB.dll" + "Name" = "8:Microsoft.Office.Tools.v4.0.Framework.dll" "Attributes" = "3:512" } } - "SourcePath" = "8:HyperVectorDB.dll" + "SourcePath" = "8:Microsoft.Office.Tools.v4.0.Framework.dll" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_AF016B7856C74F08A06EC9F3AA46C574" @@ -2316,37 +2390,6 @@ "IsDependency" = "11:TRUE" "IsolateTo" = "8:" } - "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_FC0B70EA8C384379DE6F635A416EE180" - { - "AssemblyRegister" = "3:1" - "AssemblyIsInGAC" = "11:FALSE" - "AssemblyAsmDisplayName" = "8:System.ClientModel, Version=1.1.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8, processorArchitecture=MSIL" - "ScatterAssemblies" - { - "_FC0B70EA8C384379DE6F635A416EE180" - { - "Name" = "8:System.ClientModel.dll" - "Attributes" = "3:512" - } - } - "SourcePath" = "8:System.ClientModel.dll" - "TargetName" = "8:" - "Tag" = "8:" - "Folder" = "8:_AF016B7856C74F08A06EC9F3AA46C574" - "Condition" = "8:" - "Transitive" = "11:FALSE" - "Vital" = "11:TRUE" - "ReadOnly" = "11:FALSE" - "Hidden" = "11:FALSE" - "System" = "11:FALSE" - "Permanent" = "11:FALSE" - "SharedLegacy" = "11:FALSE" - "PackageAs" = "3:1" - "Register" = "3:1" - "Exclude" = "11:FALSE" - "IsDependency" = "11:TRUE" - "IsolateTo" = "8:" - } "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_FEEE914780312C9ADC4FD9FB1EEC387F" { "AssemblyRegister" = "3:1" @@ -2457,15 +2500,15 @@ { "Name" = "8:Microsoft Visual Studio" "ProductName" = "8:TextCraft" - "ProductCode" = "8:{A666857E-AC1E-48A9-B14F-A92DFC253894}" - "PackageCode" = "8:{06D81216-51F4-476E-9ABB-18A72B67108F}" + "ProductCode" = "8:{15612664-6E52-4467-AB1D-A6A61912EB5F}" + "PackageCode" = "8:{F2373BEB-832C-43B4-B577-2F320EF122FD}" "UpgradeCode" = "8:{E36CBC33-F0C8-472A-99B5-D882A25CC883}" "AspNetVersion" = "8:" "RestartWWWService" = "11:FALSE" "RemovePreviousVersions" = "11:TRUE" "DetectNewerInstalledVersion" = "11:TRUE" "InstallAllUsers" = "11:FALSE" - "ProductVersion" = "8:1.0.2" + "ProductVersion" = "8:1.0.3" "Manufacturer" = "8:suncloudsmoon" "ARPHELPTELEPHONE" = "8:" "ARPHELPLINK" = "8:https://github.com/suncloudsmoon/TextCraft" diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 3bef2a2..e1d233f 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -33,6 +33,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.2.0")] -[assembly: AssemblyFileVersion("1.0.2.0")] +[assembly: AssemblyVersion("1.0.3.0")] +[assembly: AssemblyFileVersion("1.0.3.0")] diff --git a/RAGControl.Designer.cs b/RAGControl.Designer.cs index c46c147..2130383 100644 --- a/RAGControl.Designer.cs +++ b/RAGControl.Designer.cs @@ -49,6 +49,7 @@ private void InitializeComponent() this.FileListBox.Size = new System.Drawing.Size(330, 504); this.FileListBox.Sorted = true; this.FileListBox.TabIndex = 0; + this.FileListBox.MouseMove += new System.Windows.Forms.MouseEventHandler(this.FileListBox_MouseMove); // // RemoveButton // diff --git a/RAGControl.cs b/RAGControl.cs index 1bf1d0d..f5c22c8 100644 --- a/RAGControl.cs +++ b/RAGControl.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.IO; using System.Linq; -using System.Net; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; @@ -25,9 +24,10 @@ public partial class RAGControl : UserControl public static readonly int CHUNK_LEN = CommonUtils.TokensToCharCount(256); // Private + private ToolTip _fileToolTip = new ToolTip(); private Queue _removalQueue = new Queue(); private ConcurrentDictionary _indexFileCount = new ConcurrentDictionary(); - private BindingList _fileList; + private BindingList> _fileList; // Use KeyValuePair for label and filename private HyperVectorDB.HyperVectorDB _db; private bool _isIndexing; private readonly object progressBarLock = new object(); @@ -37,11 +37,20 @@ public RAGControl() try { InitializeComponent(); - _fileList = new BindingList(); + _fileList = new BindingList>(); FileListBox.DataSource = _fileList; + FileListBox.DisplayMember = "Key"; // Display the label (Key) + FileListBox.ValueMember = "Value"; // Internally use the filename (Value) + + //_fileToolTip.AutomaticDelay = 500; + _fileToolTip.ShowAlways = true; // Always show the tooltip + + // Attach MouseMove event to FileListBox to display the full path in the tooltip + FileListBox.MouseMove += FileListBox_MouseMove; _db = new HyperVectorDB.HyperVectorDB(ThisAddIn.Embedder, Path.GetTempPath()); - } catch (Exception ex) + } + catch (Exception ex) { CommonUtils.DisplayError(ex); } @@ -58,9 +67,9 @@ private async void AddButton_Click(object sender, EventArgs e) { foreach (string fileName in openFileDialog.FileNames) { - if (!_fileList.Contains(fileName)) + if (!_fileList.Any(file => file.Value == fileName)) { - _fileList.Add(fileName); + _fileList.Add(new KeyValuePair("📄 " + Path.GetFileName(fileName), fileName)); filesToIndex.Add(fileName); if (!RemoveButton.Enabled) { @@ -121,6 +130,25 @@ private void RemoveButton_Click(object sender, EventArgs e) } } + private void FileListBox_MouseMove(object sender, MouseEventArgs e) + { + // Get the index of the item under the mouse cursor + int index = FileListBox.IndexFromPoint(e.Location); + if (index != ListBox.NoMatches) + { + // Get the KeyValuePair (label, file path) for the item + var item = (KeyValuePair)FileListBox.Items[index]; + + // Show the file path in the tooltip + _fileToolTip.SetToolTip(FileListBox, item.Value); + } + else + { + // Clear the tooltip if not hovering over an item + _fileToolTip.SetToolTip(FileListBox, string.Empty); + } + } + private void AutoHideRemoveButton() { if (_fileList.Count == 0) @@ -137,7 +165,14 @@ private async Task IndexDocumentAsync(string filePath) { this.Invoke((MethodInvoker)delegate { - _fileList.Remove(filePath); + // Find and remove the file entry from _fileList based on the internal filename (filePath) + var fileEntry = _fileList.FirstOrDefault(file => file.Value == filePath); + if (fileEntry.Key != null) // If the file entry is found + { + _fileList.Remove(fileEntry); + } + + // Automatically hide the remove button if there are no more files in the list AutoHideRemoveButton(); }); throw; @@ -218,13 +253,20 @@ private int GetIndexFileCount() private void ProcessRemovalQueue() { int initialQueueCount = _removalQueue.Count; + for (int i = 0; i < initialQueueCount; i++) { string documentToRemove = _removalQueue.Dequeue(); - if (!_fileList.Contains(documentToRemove)) + + // Check if the document (by filename) exists in the _fileList + var fileEntry = _fileList.FirstOrDefault(file => file.Value == documentToRemove); + + // If the document is found, attempt to remove it + if (fileEntry.Key != null) { - if (!RemoveDocument(documentToRemove)) + if (!RemoveDocument(documentToRemove)) // Try removing the document { + // If removal fails, re-enqueue the document and stop processing _removalQueue.Enqueue(documentToRemove); break; } @@ -325,7 +367,7 @@ private static void IterateInnerPdfFile(ref PdfDocument doc, ref List ch public string GetRAGContext(string query, int maxTokens) { if (_fileList.Count == 0) return string.Empty; - var result = _db.QueryCosineSimilarity(query, _fileList.Count * 5); // 5 results per file + var result = _db.QueryCosineSimilarity(query, _fileList.Count * 10); // 10 results per file StringBuilder ragContext = new StringBuilder(); foreach (var document in result.Documents) ragContext.AppendLine(document.DocumentString); @@ -360,11 +402,11 @@ public static AsyncCollectionResult AskQuestion(S chatHistory.Add(new UserChatMessage($@"RAG Context: ""{ragQuery}""")); chatHistory.AddRange(messages); - ChatClient client = new ChatClient(ThisAddIn.Model, ThisAddIn.ApiKey, ThisAddIn.ClientOptions); + ChatClient client = new ChatClient(ThisAddIn.Model, new ApiKeyCredential(ThisAddIn.ApiKey), ThisAddIn.ClientOptions); // https://github.com/ollama/ollama/pull/6504 return client.CompleteChatStreamingAsync( chatHistory, - new ChatCompletionOptions() { MaxTokens = ThisAddIn.ContextLength }, + new ChatCompletionOptions() { MaxOutputTokenCount = ThisAddIn.ContextLength }, ThisAddIn.CancellationTokenSource.Token ); } diff --git a/README.md b/README.md index 29d0eb0..9612c1b 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,12 @@ To install this application, ensure your system meets the following requirements To install TextCraft, the Office® Add-In with integrated AI tools, follow these steps: 1. **Install** [Ollama™️](https://ollama.com/download). 2. **Pull** the language model of your choice, for example: - - `ollama pull qwen2:1.5b-instruct-q4_K_M` + - `ollama pull qwen2.5:1.5b` 4. **Pull** an embedding model of your choice, for example: - `ollama pull all-minilm` 6. **Download the appropriate setup file:** - - For a 32-bit system, download [`TextCraft_x32.zip`](https://github.com/suncloudsmoon/TextCraft/releases/download/v1.0.2/TextCraft_x32.zip). - - For a 64-bit system, download [`TextCraft_x64.zip`](https://github.com/suncloudsmoon/TextCraft/releases/download/v1.0.2/TextCraft_x64.zip). + - For a 32-bit system, download [`TextCraft_x32.zip`](https://github.com/suncloudsmoon/TextCraft/releases/download/v1.0.3/TextCraft_x32.zip). + - For a 64-bit system, download [`TextCraft_x64.zip`](https://github.com/suncloudsmoon/TextCraft/releases/download/v1.0.3/TextCraft_x64.zip). 7. **Extract the contents** of the downloaded zip file to a folder of your choice. 8. **Run** `setup.exe`: This will install any required dependencies for TextCraft, including .NET Framework® 4.8.1 and Visual Studio® 2010 Tools for Office Runtime. 9. **Run** `OfficeAddInSetup.msi` to install TextCraft. diff --git a/TextCraft.csproj b/TextCraft.csproj index b6a6a0e..97be0df 100644 --- a/TextCraft.csproj +++ b/TextCraft.csproj @@ -123,14 +123,15 @@ false $(DefineConstants);TRACE 4 + true - - packages\HyperVectorDB-APIFixes.1.0.3\lib\net481\HyperVectorDB.dll + + packages\HyperVectorDB-APIFixes.1.0.5\lib\net481\HyperVectorDB.dll packages\MessagePack.3.0.134-beta\lib\net472\MessagePack.dll @@ -148,14 +149,14 @@ packages\Microsoft.NET.StringTools.17.11.4\lib\net472\Microsoft.NET.StringTools.dll - packages\OpenAI.2.0.0-beta.11\lib\netstandard2.0\OpenAI.dll + packages\OpenAI.2.0.0-beta.12\lib\netstandard2.0\OpenAI.dll packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - packages\System.ClientModel.1.1.0-beta.7\lib\netstandard2.0\System.ClientModel.dll + packages\System.ClientModel.1.1.0\lib\netstandard2.0\System.ClientModel.dll packages\System.Collections.Immutable.9.0.0-rc.1.24431.7\lib\net462\System.Collections.Immutable.dll @@ -174,6 +175,7 @@ packages\System.Memory.Data.9.0.0-rc.1.24431.7\lib\net462\System.Memory.Data.dll + packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll diff --git a/ThisAddIn.cs b/ThisAddIn.cs index ac44f0b..2e55ff1 100644 --- a/ThisAddIn.cs +++ b/ThisAddIn.cs @@ -5,6 +5,7 @@ using HyperVectorDB.Embedder; using OpenAI; using OpenAI.Models; +using System.ClientModel; using Word = Microsoft.Office.Interop.Word; namespace TextForge @@ -12,7 +13,6 @@ namespace TextForge public partial class ThisAddIn { // Public - public const int BaselineContextWindowLength = 4096; // Change this if necessary public static string OpenAIEndpoint { get { return _openAIEndpoint; } } public static string ApiKey { get { return _apiKey; } } public static string Model { get { return _model; } set { _model = value; } } @@ -30,7 +30,7 @@ public partial class ThisAddIn private static string _apiKey = "dummy_key"; private static string _model = "gpt-4o"; private static string _embedModel = string.Empty; - private static int _contextLength = BaselineContextWindowLength; + private static int _contextLength = ModelProperties.BaselineContextWindowLength; private static OpenAIClientOptions _clientOptions; private static EmbedderOpenAI _embedder; private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); @@ -38,7 +38,6 @@ public partial class ThisAddIn private static bool _isAddinInitialized = false; private static IEnumerable _modelList; - private void ThisAddIn_Startup(object sender, System.EventArgs e) { try @@ -87,7 +86,7 @@ private static void InitializeEnvironmentVariables() ProjectId = "Operation Clippy", ApplicationId = "TextCraft" }; - ModelClient modelRetriever = new ModelClient(_apiKey, _clientOptions); + ModelClient modelRetriever = new ModelClient(new ApiKeyCredential(_apiKey), _clientOptions); _modelList = ModelProperties.GetModelList(modelRetriever); // caching the response string defaultModel = Properties.Settings.Default.DefaultModel; diff --git a/WordMarkdown.cs b/WordMarkdown.cs index 1b38cd3..8e419e4 100644 --- a/WordMarkdown.cs +++ b/WordMarkdown.cs @@ -1,12 +1,17 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Net.Http; using System.Text.RegularExpressions; +using Task = System.Threading.Tasks.Task; using Word = Microsoft.Office.Interop.Word; namespace TextForge { internal class WordMarkdown { + private static readonly HttpClient client = new HttpClient(); + private static readonly Dictionary Keywords = new Dictionary { ["python"] = new[] @@ -59,7 +64,43 @@ internal class WordMarkdown "instanceof", "int", "interface", "let", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typeof", "var", "void", "volatile", "while", "with", "yield" - } + }, + ["php"] = new[] + { + "abstract", "and", "array", "as", "break", "callable", "case", "catch", "class", "clone", "const", "continue", + "declare", "default", "do", "echo", "else", "elseif", "enddeclare", "endfor", "endforeach", "endif", "endswitch", + "endwhile", "extends", "final", "finally", "for", "foreach", "function", "global", "goto", "if", "implements", + "include", "include_once", "instanceof", "insteadof", "interface", "isset", "list", "namespace", "new", "or", + "print", "private", "protected", "public", "require", "require_once", "return", "static", "switch", "throw", + "trait", "try", "unset", "use", "var", "while", "xor", "yield" + }, + ["ruby"] = new[] + { + "BEGIN", "END", "alias", "and", "begin", "break", "case", "class", "def", "defined?", "do", "else", "elsif", + "end", "ensure", "false", "for", "if", "in", "module", "next", "nil", "not", "or", "redo", "rescue", "retry", + "return", "self", "super", "then", "true", "undef", "unless", "until", "when", "while", "yield" + }, + ["swift"] = new[] + { + "Any", "as", "associatedtype", "associativity", "break", "case", "catch", "class", "continue", "convenience", + "default", "defer", "deinit", "do", "dynamic", "else", "enum", "extension", "fallthrough", "false", "fileprivate", + "final", "for", "func", "get", "guard", "if", "import", "in", "indirect", "infix", "init", "inout", "internal", + "is", "lazy", "let", "mutating", "nil", "none", "nonmutating", "open", "operator", "optional", "override", "postfix", + "precedence", "prefix", "private", "protocol", "public", "repeat", "required", "rethrows", "return", "self", "set", + "some", "static", "struct", "subscript", "super", "switch", "throw", "throws", "true", "try", "typealias", "var", + "where", "while" + }, + ["go"] = new[] + { + "break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func", "go", + "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct", "switch", "type", + "var" + }, + ["r"] = new[] + { + "if", "else", "repeat", "while", "function", "for", "in", "next", "break", "TRUE", "FALSE", "NULL", "Inf", "NaN", + "NA", "NA_integer_", "NA_real_", "NA_complex_", "NA_character_" + }, }; private static string RemoveBoldMarkdownSyntax(string markdownText) => Regex.Replace(markdownText, RegexSyntaxFilter.Bold, "$1"); @@ -94,7 +135,11 @@ public static string RemoveMarkdownSyntax(string markdownText) // Replace Environment.NewLine with \n to handle line endings consistently markdownText = Regex.Replace(markdownText, Environment.NewLine, "\n"); - // List of functions to remove specific Markdown syntax elements + // Step 1: Mask code blocks (both inline and block-level) before applying removal functions + var codeBlockMask = new List(); + markdownText = MaskCodeBlocks(markdownText, codeBlockMask); + + // List of functions to remove specific Markdown syntax elements, excluding the code blocks var removalFunctions = new Func[] { RemoveBoldMarkdownSyntax, @@ -105,24 +150,62 @@ public static string RemoveMarkdownSyntax(string markdownText) RemoveHeadingMarkdownSyntax, RemoveUnorderedListMarkdownSyntax, RemoveHorizontalRuleMarkdownSyntax, - RemoveCodeBlockMarkdownSyntax, - RemoveInlineCodeMarkdownSyntax, RemoveImageMarkdownSyntax, RemoveAlternateHeadingMarkdownSyntax, RemoveLinkMarkdownSyntax }; - // Apply each removal function to the text + // Apply each removal function to the text, except for code blocks foreach (var removeFunction in removalFunctions) { markdownText = removeFunction(markdownText); } + // Step 4: After all markdown elements have been removed, unmask the code blocks + markdownText = UnmaskCodeBlocks(markdownText, codeBlockMask); + + // Step 3: Finally, call RemoveCodeBlockMarkdownSyntax to remove code block syntax + markdownText = RemoveCodeBlockMarkdownSyntax(markdownText); + markdownText = RemoveInlineCodeMarkdownSyntax(markdownText); + // Replace \n back with Environment.NewLine to restore original line endings markdownText = Regex.Replace(markdownText, "\n", Environment.NewLine); + return markdownText; } + private static string MaskCodeBlocks(string markdownText, List codeBlockMask) + { + // Inline code (mask this first) + markdownText = Regex.Replace(markdownText, RegexSyntaxFilter.InlineCode, match => + { + var mask = $"&&INLINE&CODE&MASK&{codeBlockMask.Count}&&"; + codeBlockMask.Add(match.Value); // Save original inline code block + return mask; + }, RegexOptions.Singleline); + + // Code blocks (mask these) + markdownText = Regex.Replace(markdownText, RegexSyntaxFilter.CodeBlock, match => + { + var mask = $"&&CODE&BLOCK&MASK&{codeBlockMask.Count}&&"; + codeBlockMask.Add(match.Value); // Save original code block + return mask; + }, RegexOptions.Singleline); + + return markdownText; + } + + private static string UnmaskCodeBlocks(string markdownText, List codeBlockMask) + { + for (int i = 0; i < codeBlockMask.Count; i++) + { + markdownText = markdownText.Replace($"&&INLINE&CODE&MASK&{i}&&", codeBlockMask[i]); + markdownText = markdownText.Replace($"&&CODE&BLOCK&MASK&{i}&&", codeBlockMask[i]); + } + return markdownText; + } + + private static string RemoveAllMarkdownSyntaxExcept(RegexSyntaxFilter.Number num, string markdownText) { // Dictionary mapping each Markdown syntax type to its corresponding removal function @@ -215,6 +298,8 @@ private static void ApplyMarkdownFormatting(Word.Range commentRange, string full string partialMarkdownText = RemoveAllMarkdownSyntaxExcept(formatType, fullMarkdownText); MatchCollection matches = regex.Matches(partialMarkdownText); + var codeBlockLocations = GetCodeBlockPoints(commentRange, RemoveAllMarkdownSyntaxExcept(RegexSyntaxFilter.Number.CodeBlock, fullMarkdownText)); + int searchIndex = 0; int offset = 0; foreach (Match match in matches) @@ -224,6 +309,13 @@ private static void ApplyMarkdownFormatting(Word.Range commentRange, string full searchIndex = commentRange.Start + partialMarkdownText.IndexOf(match.Value, searchIndex); int length = textToFormat.Length; + CodeBlockPoint codeBlockLoc; + if (formatType != RegexSyntaxFilter.Number.CodeBlock && IsLocatedWithinCodeBlock(codeBlockLocations, searchIndex, out codeBlockLoc)) + { + searchIndex += codeBlockLoc.End - searchIndex + 1; + continue; + } + Word.Range formatRange = commentRange.Duplicate; switch (formatType) { @@ -268,8 +360,12 @@ private static void ApplyMarkdownFormatting(Word.Range commentRange, string full ApplyFormatting(formatRange, searchIndex, offset, insideContent.Length, 10, ref offset, 6); break; case RegexSyntaxFilter.Number.Image: + int startIndex = searchIndex - offset; + formatRange.SetRange(startIndex, startIndex); + string imageUrl = match.Groups[2].Value; - ApplyImageFormatting(formatRange, imageUrl); + if (CommonUtils.GetInternetAccessPermission(imageUrl)) + ApplyImageFormatting(formatRange, imageUrl); offset += length; // Adjust the offset to account for the removed text break; case RegexSyntaxFilter.Number.AlternateHeading: @@ -280,6 +376,48 @@ private static void ApplyMarkdownFormatting(Word.Range commentRange, string full } } + private static bool IsLocatedWithinCodeBlock(List points, int startIndex, out CodeBlockPoint blockPoint) + { + foreach (var point in points) + if (startIndex >= point.Start && startIndex <= point.End) + { + blockPoint = point; + return true; + } + blockPoint = null; + return false; + } + + private static CodeBlockPoint GetCodeBlockAtIndex(List points, int startIndex) + { + foreach (var point in points) + if (startIndex >= point.Start && startIndex <= point.End) + return point; + throw new ApplicationException($"Could not find markdown code block at index #{startIndex}!"); + } + + private static List GetCodeBlockPoints(Word.Range commentRange, string partialMarkdownText) + { + List points = new List(); + Regex regex = new Regex(RegexSyntaxFilter.CodeBlock, RegexOptions.Singleline); + MatchCollection matches = regex.Matches(partialMarkdownText); + + int searchIndex = 0; + int offset = 0; + foreach (Match match in matches) + { + string textToFormat = match.Value; + string insideContent = match.Groups[1].Value; + searchIndex = commentRange.Start + partialMarkdownText.IndexOf(match.Value, searchIndex); + int length = textToFormat.Length; + + points.Add(new CodeBlockPoint(searchIndex - offset, searchIndex - offset + length - 1)); + offset += 6 + length; + } + + return points; + } + private static void ApplyFormatting(Word.Range formatRange, int searchIndex, int offset, int length, int formatType, ref int offsetIncrement, int offsetValue, int level = 1) { formatRange.SetRange(searchIndex - offset, searchIndex - offset + length); @@ -329,17 +467,35 @@ private static void ApplyAlternateHeadingFormatting(Word.Range formatRange, int private static void ApplyImageFormatting(Word.Range formatRange, string imageUrl) { - using (var webClient = new System.Net.WebClient()) + try { - byte[] imageBytes = webClient.DownloadData(imageUrl); + // Validate the URL + if (!Uri.IsWellFormedUriString(imageUrl, UriKind.Absolute)) + { + throw new ArgumentException("Invalid URL format", nameof(imageUrl)); + } + + // Download the image data + byte[] imageBytes = Task.Run(() => client.GetByteArrayAsync(imageUrl)).Result; + + // Create a temporary file string tempFilePath = System.IO.Path.GetTempFileName(); System.IO.File.WriteAllBytes(tempFilePath, imageBytes); + + // Add the picture to the document formatRange.InlineShapes.AddPicture(tempFilePath); - System.IO.File.Delete(tempFilePath); // Clean up the temporary file + + // Clean up the temporary file + System.IO.File.Delete(tempFilePath); + } + catch (Exception ex) + { + // Handle exceptions (e.g., logging) + Console.WriteLine($"An error occurred: {ex.Message}"); } } - private static void ApplyHeadingFormatting(Word.Range formatRange, int searchIndex, int offset, Match match, string insideContent, ref int offsetIncrement) + private static void ApplyHeadingFormatting(Word.Range formatRange, int searchIndex, int offset, Match match, string insideContent, ref int offsetIncrement) { formatRange.SetRange(searchIndex - offset, searchIndex - offset + match.Groups[2].Length); if (insideContent.Length < 1 || insideContent.Length > 6) @@ -404,9 +560,24 @@ private static void ApplySyntaxHighlighting(Word.Range range, string language, s keywordRange.Font.Color = Word.WdColor.wdColorBlue; } } + private static string[] GetKeywordsForLanguage(string language) { - return Keywords.TryGetValue(language.ToLower(), out var keywords) ? keywords : Array.Empty(); + return Keywords.TryGetValue(language, out var keywords) ? keywords : Array.Empty(); + } + } + + public class CodeBlockPoint + { + public int Start { get { return _start; } } + public int End { get { return _end; } } + + private int _start, _end; + + public CodeBlockPoint(int start, int end) + { + _start = start; + _end = end; } } diff --git a/packages.config b/packages.config index 2bb3d4a..ba6e123 100644 --- a/packages.config +++ b/packages.config @@ -1,25 +1,25 @@  - + - + - + - - + + - + - - + + - - + + \ No newline at end of file