diff --git a/.gitignore b/.gitignore index 24fe21d..937f2fc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ obj/ *.user *.suo *.vs/ +Red Ink for Word/Red Ink for Word_TemporaryKey.pfx +Red Ink for Excel/Red Ink for Excel_TemporaryKey.pfx +Red Ink for Outlook/Red Ink for Outlook_TemporaryKey.pfx diff --git a/Red Ink for Excel/Red Ink for Excel.vbproj b/Red Ink for Excel/Red Ink for Excel.vbproj index 8353b14..aaef44a 100644 --- a/Red Ink for Excel/Red Ink for Excel.vbproj +++ b/Red Ink for Excel/Red Ink for Excel.vbproj @@ -1,4 +1,4 @@ - + @@ -448,4 +452,7 @@ - \ No newline at end of file + + + + diff --git a/Red Ink for Outlook/Ribbon1.Designer.vb b/Red Ink for Outlook/Ribbon1.Designer.vb index a1d4b0d..196d398 100644 --- a/Red Ink for Outlook/Ribbon1.Designer.vb +++ b/Red Ink for Outlook/Ribbon1.Designer.vb @@ -112,6 +112,7 @@ Partial Class Ribbon1 Me.Menu1.Items.Add(Me.RI_Clipboard) Me.Menu1.Items.Add(Me.RI_DefineMyStyle) Me.Menu1.Items.Add(Me.RI_HelpMe) + Me.Menu1.Items.Add(Me.ModelsMenu) Me.Menu1.Items.Add(Me.Settings) Me.Menu1.KeyTip = "S" Me.Menu1.Label = "Task" @@ -380,6 +381,17 @@ Partial Class Ribbon1 Friend WithEvents RI_ApplyMyStyle As RibbonButton Friend WithEvents RI_DefineMyStyle As RibbonButton Friend WithEvents RI_HelpMe As RibbonButton + Friend WithEvents ModelsMenu As RibbonMenu + Friend WithEvents RI_Model1 As RibbonButton + Friend WithEvents RI_Model2 As RibbonButton + Friend WithEvents RI_Model3 As RibbonButton + Friend WithEvents RI_Model4 As RibbonButton + Friend WithEvents RI_Model5 As RibbonButton + Friend WithEvents RI_Model6 As RibbonButton + Friend WithEvents RI_Model7 As RibbonButton + Friend WithEvents RI_Model8 As RibbonButton + Friend WithEvents RI_Model9 As RibbonButton + Friend WithEvents RI_Model10 As RibbonButton End Class Partial Class ThisRibbonCollection @@ -459,6 +471,17 @@ Partial Class Ribbon2 Me.RI_Clipboard = Me.Factory.CreateRibbonButton Me.RI_DefineMyStyle = Me.Factory.CreateRibbonButton Me.RI_HelpMe = Me.Factory.CreateRibbonButton + Me.ModelsMenu = Me.Factory.CreateRibbonMenu + Me.RI_Model1 = Me.Factory.CreateRibbonButton + Me.RI_Model2 = Me.Factory.CreateRibbonButton + Me.RI_Model3 = Me.Factory.CreateRibbonButton + Me.RI_Model4 = Me.Factory.CreateRibbonButton + Me.RI_Model5 = Me.Factory.CreateRibbonButton + Me.RI_Model6 = Me.Factory.CreateRibbonButton + Me.RI_Model7 = Me.Factory.CreateRibbonButton + Me.RI_Model8 = Me.Factory.CreateRibbonButton + Me.RI_Model9 = Me.Factory.CreateRibbonButton + Me.RI_Model10 = Me.Factory.CreateRibbonButton Me.Settings = Me.Factory.CreateRibbonButton Me.Group2 = Me.Factory.CreateRibbonGroup Me.RI_PrimLang2 = Me.Factory.CreateRibbonButton @@ -502,6 +525,7 @@ Partial Class Ribbon2 Me.Menu1.Items.Add(Me.RI_Clipboard) Me.Menu1.Items.Add(Me.RI_DefineMyStyle) Me.Menu1.Items.Add(Me.RI_HelpMe) + Me.Menu1.Items.Add(Me.ModelsMenu) Me.Menu1.Items.Add(Me.Settings) Me.Menu1.KeyTip = "S" Me.Menu1.Label = "Task" @@ -509,6 +533,82 @@ Partial Class Ribbon2 Me.Menu1.ShowImage = True Me.Menu1.SuperTip = "Access other functions of Red Ink" ' + 'ModelsMenu + ' + Me.ModelsMenu.Label = "Models" + Me.ModelsMenu.Name = "ModelsMenu" + Me.ModelsMenu.ShowImage = True + Me.ModelsMenu.Items.Add(Me.RI_Model1) + Me.ModelsMenu.Items.Add(Me.RI_Model2) + Me.ModelsMenu.Items.Add(Me.RI_Model3) + Me.ModelsMenu.Items.Add(Me.RI_Model4) + Me.ModelsMenu.Items.Add(Me.RI_Model5) + Me.ModelsMenu.Items.Add(Me.RI_Model6) + Me.ModelsMenu.Items.Add(Me.RI_Model7) + Me.ModelsMenu.Items.Add(Me.RI_Model8) + Me.ModelsMenu.Items.Add(Me.RI_Model9) + Me.ModelsMenu.Items.Add(Me.RI_Model10) + ' + 'RI_Model1 + ' + Me.RI_Model1.Label = "Model 1" + Me.RI_Model1.Name = "RI_Model1" + Me.RI_Model1.Visible = False + ' + 'RI_Model2 + ' + Me.RI_Model2.Label = "Model 2" + Me.RI_Model2.Name = "RI_Model2" + Me.RI_Model2.Visible = False + ' + 'RI_Model3 + ' + Me.RI_Model3.Label = "Model 3" + Me.RI_Model3.Name = "RI_Model3" + Me.RI_Model3.Visible = False + ' + 'RI_Model4 + ' + Me.RI_Model4.Label = "Model 4" + Me.RI_Model4.Name = "RI_Model4" + Me.RI_Model4.Visible = False + ' + 'RI_Model5 + ' + Me.RI_Model5.Label = "Model 5" + Me.RI_Model5.Name = "RI_Model5" + Me.RI_Model5.Visible = False + ' + 'RI_Model6 + ' + Me.RI_Model6.Label = "Model 6" + Me.RI_Model6.Name = "RI_Model6" + Me.RI_Model6.Visible = False + ' + 'RI_Model7 + ' + Me.RI_Model7.Label = "Model 7" + Me.RI_Model7.Name = "RI_Model7" + Me.RI_Model7.Visible = False + ' + 'RI_Model8 + ' + Me.RI_Model8.Label = "Model 8" + Me.RI_Model8.Name = "RI_Model8" + Me.RI_Model8.Visible = False + ' + 'RI_Model9 + ' + Me.RI_Model9.Label = "Model 9" + Me.RI_Model9.Name = "RI_Model9" + Me.RI_Model9.Visible = False + ' + 'RI_Model10 + ' + Me.RI_Model10.Label = "Model 10" + Me.RI_Model10.Name = "RI_Model10" + Me.RI_Model10.Visible = False + ' 'RI_Primlang ' Me.RI_Primlang.KeyTip = "E" @@ -769,6 +869,17 @@ Partial Class Ribbon2 Friend WithEvents RI_ApplyMyStyle As RibbonButton Friend WithEvents RI_DefineMyStyle As RibbonButton Friend WithEvents RI_HelpMe As RibbonButton + Friend WithEvents ModelsMenu As RibbonMenu + Friend WithEvents RI_Model1 As RibbonButton + Friend WithEvents RI_Model2 As RibbonButton + Friend WithEvents RI_Model3 As RibbonButton + Friend WithEvents RI_Model4 As RibbonButton + Friend WithEvents RI_Model5 As RibbonButton + Friend WithEvents RI_Model6 As RibbonButton + Friend WithEvents RI_Model7 As RibbonButton + Friend WithEvents RI_Model8 As RibbonButton + Friend WithEvents RI_Model9 As RibbonButton + Friend WithEvents RI_Model10 As RibbonButton End Class Partial Class ThisRibbonCollection diff --git a/Red Ink for Outlook/Ribbon1.vb b/Red Ink for Outlook/Ribbon1.vb index 363666b..8a17bbe 100644 --- a/Red Ink for Outlook/Ribbon1.vb +++ b/Red Ink for Outlook/Ribbon1.vb @@ -3,6 +3,7 @@ Imports Microsoft.Office.Tools.Ribbon Imports Microsoft.Win32 +Imports SharedLibrary.SharedLibrary Public Class Ribbon1 Private Enum OfficeTheme @@ -160,6 +161,84 @@ Public Class Ribbon1 Globals.ThisAddIn.HelpMeInky() End Sub + Private Sub RI_Model1_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model1.Click + Globals.ThisAddIn.SelectModel(1) + End Sub + + Private Sub RI_Model2_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model2.Click + Globals.ThisAddIn.SelectModel(2) + End Sub + + Private Sub RI_Model3_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model3.Click + Globals.ThisAddIn.SelectModel(3) + End Sub + + Private Sub RI_Model4_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model4.Click + Globals.ThisAddIn.SelectModel(4) + End Sub + + Private Sub RI_Model5_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model5.Click + Globals.ThisAddIn.SelectModel(5) + End Sub + + Private Sub RI_Model6_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model6.Click + Globals.ThisAddIn.SelectModel(6) + End Sub + + Private Sub RI_Model7_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model7.Click + Globals.ThisAddIn.SelectModel(7) + End Sub + + Private Sub RI_Model8_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model8.Click + Globals.ThisAddIn.SelectModel(8) + End Sub + + Private Sub RI_Model9_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model9.Click + Globals.ThisAddIn.SelectModel(9) + End Sub + + Private Sub RI_Model10_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model10.Click + Globals.ThisAddIn.SelectModel(10) + End Sub + + Public Sub UpdateModelsMenu() + Try + Dim available = ModelConfigManager.GetAvailableModels() + Dim current = ModelConfigManager.GetCurrentModelNumber() + + For i = 1 To 10 + Dim btn = GetModelButton(i) + If btn Is Nothing Then Continue For + + If available.Contains(i) Then + btn.Visible = True + Dim label = ModelConfigManager.GetModelDisplayName(i) + btn.Label = If(i = current, label & " *", label) + Else + btn.Visible = False + End If + Next + Catch + ' non-critical + End Try + End Sub + + Private Function GetModelButton(i As Integer) As RibbonButton + Select Case i + Case 1 : Return RI_Model1 + Case 2 : Return RI_Model2 + Case 3 : Return RI_Model3 + Case 4 : Return RI_Model4 + Case 5 : Return RI_Model5 + Case 6 : Return RI_Model6 + Case 7 : Return RI_Model7 + Case 8 : Return RI_Model8 + Case 9 : Return RI_Model9 + Case 10 : Return RI_Model10 + End Select + Return Nothing + End Function + End Class Public Class Ribbon2 diff --git a/Red Ink for Outlook/ThisAddIn.vb b/Red Ink for Outlook/ThisAddIn.vb index c3cb2c7..c0fe161 100644 --- a/Red Ink for Outlook/ThisAddIn.vb +++ b/Red Ink for Outlook/ThisAddIn.vb @@ -48,6 +48,7 @@ Imports Microsoft.Office.Interop.Outlook Imports Microsoft.Office.Interop.PowerPoint Imports Microsoft.Office.Interop.Word Imports SharedLibrary.SharedLibrary +Imports SharedLibrary.SharedLibrary.SharedMethods Partial Public Class ThisAddIn @@ -576,6 +577,30 @@ Partial Public Class ThisAddIn _context.InitialConfigFailed = False _context.RDV = "Outlook (" & Version & ")" SharedMethods.InitializeConfig(_context, FirstTime, Reload) + Try + If Globals.Ribbons.Ribbon1 IsNot Nothing Then + Globals.Ribbons.Ribbon1.UpdateModelsMenu() + End If + Catch + ' Ribbon may not be ready yet + End Try + End Sub + Public Shared Sub SelectModel(modelNumber As Integer) + Try + If ModelConfigManager.SelectModel(_context, modelNumber) Then + Try + If Globals.Ribbons.Ribbon1 IsNot Nothing Then + Globals.Ribbons.Ribbon1.UpdateModelsMenu() + End If + Catch + ' non-critical + End Try + Else + SharedMethods.ShowCustomMessageBox($"Model {modelNumber} is not configured.") + End If + Catch ex As System.Exception + SharedMethods.ShowCustomMessageBox($"Error switching model: {ex.Message}") + End Try End Sub ''' diff --git a/Red Ink for Word/Red Ink for Word.vbproj b/Red Ink for Word/Red Ink for Word.vbproj index dd88f45..9fd61b9 100644 --- a/Red Ink for Word/Red Ink for Word.vbproj +++ b/Red Ink for Word/Red Ink for Word.vbproj @@ -1,4 +1,4 @@ - + @@ -48,21 +48,21 @@ 3 - 1.0.0.700 + 1.0.0.706 ..\clickonce\apps\develop\word\ https://redink.ai/apps/develop/word/ Red Ink for Word %28Develop%29 Red Ink for Word %28Develop%29 - 1.0.0.201 + 1.0.0.706 ..\clickonce\apps\preview\word\ https://redink.ai/apps/preview/word/ Red Ink for Word %28Preview%29 Red Ink for Word %28Preview%29 - 1.0.0.1 + 1.0.0.706 ..\clickonce\apps\ga\word\ https://redink.ai/apps/ga/word/ Red Ink for Word @@ -556,6 +556,7 @@ + @@ -628,6 +629,7 @@ + @@ -759,7 +761,7 @@ true - 50229D23A3CA721BCA66C2ECC258074E6C6D6456 + 5E40483CEA0314AAD471029E15CE26096544C430 http://timestamp.digicert.com @@ -784,6 +786,9 @@ pdbonly x64 + + Red Ink for Word_TemporaryKey.pfx + @@ -822,4 +827,9 @@ - \ No newline at end of file + + + + + + diff --git a/Red Ink for Word/Ribbon1.Designer.vb b/Red Ink for Word/Ribbon1.Designer.vb index ad54705..389f337 100644 --- a/Red Ink for Word/Ribbon1.Designer.vb +++ b/Red Ink for Word/Ribbon1.Designer.vb @@ -85,6 +85,7 @@ Partial Class Ribbon1 Me.RI_ArgueAgainst = Me.Factory.CreateRibbonButton Me.RI_SuggestTitles = Me.Factory.CreateRibbonButton Me.RI_RevisionsSummary = Me.Factory.CreateRibbonButton + Me.RI_RevisionsSummary = Me.Factory.CreateRibbonButton Me.RI_SpecialModel = Me.Factory.CreateRibbonButton Me.RI_DocCheck = Me.Factory.CreateRibbonButton Me.RI_FindClause = Me.Factory.CreateRibbonButton @@ -113,6 +114,17 @@ Partial Class Ribbon1 Me.RI_Transcriptor = Me.Factory.CreateRibbonButton Me.RI_DiscussInky = Me.Factory.CreateRibbonButton Me.RI_HelpMe = Me.Factory.CreateRibbonButton + Me.ModelsMenu = Me.Factory.CreateRibbonMenu + Me.RI_Model1 = Me.Factory.CreateRibbonButton + Me.RI_Model2 = Me.Factory.CreateRibbonButton + Me.RI_Model3 = Me.Factory.CreateRibbonButton + Me.RI_Model4 = Me.Factory.CreateRibbonButton + Me.RI_Model5 = Me.Factory.CreateRibbonButton + Me.RI_Model6 = Me.Factory.CreateRibbonButton + Me.RI_Model7 = Me.Factory.CreateRibbonButton + Me.RI_Model8 = Me.Factory.CreateRibbonButton + Me.RI_Model9 = Me.Factory.CreateRibbonButton + Me.RI_Model10 = Me.Factory.CreateRibbonButton Me.Settings = Me.Factory.CreateRibbonButton Me.Group2 = Me.Factory.CreateRibbonGroup Me.RI_PrimLang2 = Me.Factory.CreateRibbonButton @@ -162,6 +174,7 @@ Partial Class Ribbon1 Me.Menu1.Items.Add(Me.RI_Transcriptor) Me.Menu1.Items.Add(Me.RI_DiscussInky) Me.Menu1.Items.Add(Me.RI_HelpMe) + Me.Menu1.Items.Add(Me.ModelsMenu) Me.Menu1.Items.Add(Me.Settings) Me.Menu1.KeyTip = "RI" Me.Menu1.Label = "Task" @@ -423,15 +436,6 @@ Partial Class Ribbon1 Me.RI_Explain.ScreenTip = "Explain in simple terms what the selected text means" Me.RI_Explain.ShowImage = True ' - 'RI_ArgueAgainst - ' - Me.RI_ArgueAgainst.Label = "Argue Against" - Me.RI_ArgueAgainst.Name = "RI_ArgueAgainst" - Me.RI_ArgueAgainst.OfficeImageId = "SpeakStop" - Me.RI_ArgueAgainst.ScreenTip = "This will let the AI argue against the selected text with at least the number of " & - "words defined by you" - Me.RI_ArgueAgainst.ShowImage = True - ' 'RI_SuggestTitles ' Me.RI_SuggestTitles.Label = "Suggest Titles" @@ -687,6 +691,82 @@ Partial Class Ribbon1 Me.RI_HelpMe.ScreenTip = "This will call up a chatbot that answers your questions about Red Ink" Me.RI_HelpMe.ShowImage = True ' + 'ModelsMenu + ' + Me.ModelsMenu.Items.Add(Me.RI_Model1) + Me.ModelsMenu.Items.Add(Me.RI_Model2) + Me.ModelsMenu.Items.Add(Me.RI_Model3) + Me.ModelsMenu.Items.Add(Me.RI_Model4) + Me.ModelsMenu.Items.Add(Me.RI_Model5) + Me.ModelsMenu.Items.Add(Me.RI_Model6) + Me.ModelsMenu.Items.Add(Me.RI_Model7) + Me.ModelsMenu.Items.Add(Me.RI_Model8) + Me.ModelsMenu.Items.Add(Me.RI_Model9) + Me.ModelsMenu.Items.Add(Me.RI_Model10) + Me.ModelsMenu.Label = "Models" + Me.ModelsMenu.Name = "ModelsMenu" + Me.ModelsMenu.ShowImage = True + ' + 'RI_Model1 + ' + Me.RI_Model1.Label = "" + Me.RI_Model1.Name = "RI_Model1" + Me.RI_Model1.ShowImage = True + ' + 'RI_Model2 + ' + Me.RI_Model2.Label = "" + Me.RI_Model2.Name = "RI_Model2" + Me.RI_Model2.ShowImage = True + ' + 'RI_Model3 + ' + Me.RI_Model3.Label = "" + Me.RI_Model3.Name = "RI_Model3" + Me.RI_Model3.ShowImage = True + ' + 'RI_Model4 + ' + Me.RI_Model4.Label = "" + Me.RI_Model4.Name = "RI_Model4" + Me.RI_Model4.ShowImage = True + ' + 'RI_Model5 + ' + Me.RI_Model5.Label = "" + Me.RI_Model5.Name = "RI_Model5" + Me.RI_Model5.ShowImage = True + ' + 'RI_Model6 + ' + Me.RI_Model6.Label = "" + Me.RI_Model6.Name = "RI_Model6" + Me.RI_Model6.ShowImage = True + ' + 'RI_Model7 + ' + Me.RI_Model7.Label = "" + Me.RI_Model7.Name = "RI_Model7" + Me.RI_Model7.ShowImage = True + ' + 'RI_Model8 + ' + Me.RI_Model8.Label = "" + Me.RI_Model8.Name = "RI_Model8" + Me.RI_Model8.ShowImage = True + ' + 'RI_Model9 + ' + Me.RI_Model9.Label = "" + Me.RI_Model9.Name = "RI_Model9" + Me.RI_Model9.ShowImage = True + ' + 'RI_Model10 + ' + Me.RI_Model10.Label = "" + Me.RI_Model10.Name = "RI_Model10" + Me.RI_Model10.ShowImage = True + ' 'Settings ' Me.Settings.Label = "Settings" @@ -733,6 +813,15 @@ Partial Class Ribbon1 Me.RI_Chat.ScreenTip = "Will open a window where you can chat with the LLM" Me.RI_Chat.ShowImage = True ' + 'RI_ArgueAgainst + ' + Me.RI_ArgueAgainst.Label = "Argue Against" + Me.RI_ArgueAgainst.Name = "RI_ArgueAgainst" + Me.RI_ArgueAgainst.OfficeImageId = "SpeakStop" + Me.RI_ArgueAgainst.ScreenTip = "This will let the AI argue against the selected text with at least the number of " & + "words defined by you" + Me.RI_ArgueAgainst.ShowImage = True + ' 'Ribbon1 ' Me.Name = "Ribbon1" @@ -992,6 +1081,17 @@ Partial Class Ribbon1 Friend WithEvents RI_LiveCompare As RibbonButton Friend WithEvents RI_RevisionsSummary As RibbonButton Friend WithEvents RI_DiscussInky As RibbonButton + Friend WithEvents ModelsMenu As RibbonMenu + Friend WithEvents RI_Model1 As RibbonButton + Friend WithEvents RI_Model2 As RibbonButton + Friend WithEvents RI_Model3 As RibbonButton + Friend WithEvents RI_Model4 As RibbonButton + Friend WithEvents RI_Model5 As RibbonButton + Friend WithEvents RI_Model6 As RibbonButton + Friend WithEvents RI_Model7 As RibbonButton + Friend WithEvents RI_Model8 As RibbonButton + Friend WithEvents RI_Model9 As RibbonButton + Friend WithEvents RI_Model10 As RibbonButton End Class Partial Class ThisRibbonCollection diff --git a/Red Ink for Word/Ribbon1.vb b/Red Ink for Word/Ribbon1.vb index 85e0120..acd5f24 100644 --- a/Red Ink for Word/Ribbon1.vb +++ b/Red Ink for Word/Ribbon1.vb @@ -3,6 +3,7 @@ Imports Microsoft.Office.Tools.Ribbon Imports Microsoft.Win32 +Imports SharedLibrary.SharedLibrary Public Class Ribbon1 @@ -300,6 +301,84 @@ Public Class Ribbon1 Globals.ThisAddIn.ArgueAgainst() End Sub + Private Sub RI_Model1_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model1.Click + Globals.ThisAddIn.SelectModel(1) + End Sub + + Private Sub RI_Model2_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model2.Click + Globals.ThisAddIn.SelectModel(2) + End Sub + + Private Sub RI_Model3_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model3.Click + Globals.ThisAddIn.SelectModel(3) + End Sub + + Private Sub RI_Model4_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model4.Click + Globals.ThisAddIn.SelectModel(4) + End Sub + + Private Sub RI_Model5_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model5.Click + Globals.ThisAddIn.SelectModel(5) + End Sub + + Private Sub RI_Model6_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model6.Click + Globals.ThisAddIn.SelectModel(6) + End Sub + + Private Sub RI_Model7_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model7.Click + Globals.ThisAddIn.SelectModel(7) + End Sub + + Private Sub RI_Model8_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model8.Click + Globals.ThisAddIn.SelectModel(8) + End Sub + + Private Sub RI_Model9_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model9.Click + Globals.ThisAddIn.SelectModel(9) + End Sub + + Private Sub RI_Model10_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_Model10.Click + Globals.ThisAddIn.SelectModel(10) + End Sub + + Public Sub UpdateModelsMenu() + Try + Dim available = ModelConfigManager.GetAvailableModels() + Dim current = ModelConfigManager.GetCurrentModelNumber() + + For i = 1 To 10 + Dim btn = GetModelButton(i) + If btn Is Nothing Then Continue For + + If available.Contains(i) Then + btn.Visible = True + Dim label = ModelConfigManager.GetModelDisplayName(i) + btn.Label = If(i = current, $"{label} *", label) + Else + btn.Visible = False + End If + Next + Catch + ' non-critical + End Try + End Sub + + Private Function GetModelButton(i As Integer) As RibbonButton + Select Case i + Case 1 : Return RI_Model1 + Case 2 : Return RI_Model2 + Case 3 : Return RI_Model3 + Case 4 : Return RI_Model4 + Case 5 : Return RI_Model5 + Case 6 : Return RI_Model6 + Case 7 : Return RI_Model7 + Case 8 : Return RI_Model8 + Case 9 : Return RI_Model9 + Case 10 : Return RI_Model10 + End Select + Return Nothing + End Function + Private Sub RI_LiveCompare_Click(sender As Object, e As RibbonControlEventArgs) Handles RI_LiveCompare.Click Globals.ThisAddIn.CompareActiveDocWithOtherOpenDoc() End Sub diff --git a/Red Ink for Word/ThisAddIn.vb b/Red Ink for Word/ThisAddIn.vb index 36b868b..f530b1e 100644 --- a/Red Ink for Word/ThisAddIn.vb +++ b/Red Ink for Word/ThisAddIn.vb @@ -41,6 +41,7 @@ Imports System.Threading Imports System.Threading.Tasks Imports System.Windows.Forms Imports SharedLibrary.SharedLibrary +Imports SharedLibrary.SharedLibrary.SharedMethods Imports System.Globalization Partial Public Class ThisAddIn @@ -446,7 +447,39 @@ Partial Public Class ThisAddIn _context.InitialConfigFailed = False _context.RDV = "Word (" & Version & ")" SharedMethods.InitializeConfig(_context, FirstTime, Reload) + + ' Update the models menu if ribbon is loaded + Try + If Globals.Ribbons.Ribbon1 IsNot Nothing Then + Globals.Ribbons.Ribbon1.UpdateModelsMenu() + End If + Catch + ' Ribbon may not be loaded yet; ignore + End Try + End Sub + + ''' + ''' Selects a model by number and updates the primary configuration. + ''' Used by the ribbon \"Models\" menu. + ''' + Public Shared Sub SelectModel(modelNumber As Integer) + Try + If ModelConfigManager.SelectModel(_context, modelNumber) Then + Try + If Globals.Ribbons.Ribbon1 IsNot Nothing Then + Globals.Ribbons.Ribbon1.UpdateModelsMenu() + End If + Catch + ' Non-critical UI update failure + End Try + Else + SharedMethods.ShowCustomMessageBox($"Model {modelNumber} is not configured.") + End If + Catch ex As Exception + SharedMethods.ShowCustomMessageBox($"Error switching model: {ex.Message}") + End Try End Sub + Private Function INIValuesMissing() As Boolean Return SharedMethods.INIValuesMissing(_context) End Function diff --git a/SharedLibrary/Code/Core/Config/ModelConfigManager.vb b/SharedLibrary/Code/Core/Config/ModelConfigManager.vb new file mode 100644 index 0000000..67a13f8 --- /dev/null +++ b/SharedLibrary/Code/Core/Config/ModelConfigManager.vb @@ -0,0 +1,400 @@ +' Part of: Red Ink Shared Library by David Rosenthal +' 2025-12-06 Modelmanager implemented by Roman Kost + +Option Strict On +Option Explicit On + +Imports System.Collections.Generic +Imports System.Diagnostics +Imports System.Linq +Imports System.Windows.Forms +Imports SharedLibrary.SharedLibrary.SharedContext + +Namespace SharedLibrary + + ''' + ''' Manages multiple LLM model configurations loaded from redink.ini. + ''' - Detects numbered model variants (APIKey_1, Endpoint_1, Model_1, ...). + ''' - Stores them in memory. + ''' - Applies the selected model to the primary INI_* fields in the shared context. + ''' - Tracks the currently selected model number. + ''' + ''' This class only manipulates PRIMARY fields (INI_APIKey, INI_Model, ...). + ''' It does NOT touch the secondary API slot (INI_*_2) used by SecondAPI / alternate models. + ''' + Public Class ModelConfigManager + + Private Const MultiModelPrefix As String = "MultiModel_" + Private Const MaxModelNumber As Integer = 100 + ' Keep this aligned with SharedMethods.LoadConfig.INIValuesMissing to ensure both checks require the same core fields. + Private Shared ReadOnly RequiredModelKeys As String() = {"Endpoint", "Model", "APICall", "Response"} + + ' Storage for all detected model configurations (per model number) + Private Shared ReadOnly _availableModels As New Dictionary(Of Integer, Dictionary(Of String, String))() + + ' Currently selected model number (1-based), default 1 + Private Shared _currentModelNumber As Integer = 1 + + ' Display names for each model (e.g. the Model_N value) + Private Shared ReadOnly _modelDisplayNames As New Dictionary(Of Integer, String)() + + ''' + ''' Detects and stores all model configurations from the parsed INI dictionary. + ''' + Public Shared Sub DetectAndStoreModels(configDict As Dictionary(Of String, String)) + _availableModels.Clear() + _modelDisplayNames.Clear() + + ' Preferred format: MultiModel__ + Dim perModel = ExtractPrefixedModels(configDict) + + ' Validate each detected model + For Each n In perModel.Keys.OrderBy(Function(x) x) + Dim mc = perModel(n) + If ValidateModel(mc, $"#{n}") Then + _availableModels(n) = mc + _modelDisplayNames(n) = GetModelDisplayNameForConfig(mc, n) + End If + Next + + ' Backward compatibility: if no numbered models, extract a "primary" model as Model 1 + If _availableModels.Count = 0 Then + Dim primary = ExtractPrimaryModelConfig(configDict) + If ValidateModel(primary, "primary (no suffix)") Then + _availableModels(1) = primary + _modelDisplayNames(1) = GetModelDisplayNameForConfig(primary, 1) + End If + End If + + ' If previous current model is no longer available, fall back to 1 + If _availableModels.Count > 0 Then + If Not _availableModels.ContainsKey(_currentModelNumber) Then + _currentModelNumber = 1 + End If + Else + _currentModelNumber = 1 + End If + End Sub + + ''' + ''' Returns the list of available model numbers. + ''' + Public Shared Function GetAvailableModels() As List(Of Integer) + Return _availableModels.Keys.OrderBy(Function(n) n).ToList() + End Function + + ''' + ''' Returns a user-friendly display name for a model. + ''' + Public Shared Function GetModelDisplayName(modelNumber As Integer) As String + If _modelDisplayNames.ContainsKey(modelNumber) Then + Return _modelDisplayNames(modelNumber) + End If + Return $"Model {modelNumber}" + End Function + + ''' + ''' Returns the currently selected model number (1-based). + ''' + Public Shared Function GetCurrentModelNumber() As Integer + Return _currentModelNumber + End Function + + ''' + ''' Selects a model by copying its configuration into the primary INI_* fields. + ''' Returns False if the model number is not available or if an error occurs. + ''' + Public Shared Function SelectModel(context As ISharedContext, modelNumber As Integer) As Boolean + If context Is Nothing Then Return False + If Not _availableModels.ContainsKey(modelNumber) Then Return False + + Dim mc = _availableModels(modelNumber) + + Try + CopyPropertiesToContext(context, mc) + UpdateDecodedAPI(context) + + _currentModelNumber = modelNumber + + ' Persist selection to settings; ignore failures + Try + My.Settings.SelectedModelNumber = modelNumber + My.Settings.Save() + Catch + End Try + + Return True + Catch ex As Exception + Return False + End Try + End Function + + ''' + ''' Loads the saved model selection from settings. + ''' Returns 1 if not found or invalid. + ''' + Public Shared Function LoadSavedModelNumber() As Integer + Try + Dim n = My.Settings.SelectedModelNumber + If n > 0 Then Return n + Catch + End Try + Return 1 + End Function + + ' ----------------------- + ' Internal helpers + ' ----------------------- + + Private Shared Function ExtractPrefixedModels(configDict As Dictionary(Of String, String)) As Dictionary(Of Integer, Dictionary(Of String, String)) + Dim perModel As New Dictionary(Of Integer, Dictionary(Of String, String))() + + For Each kvp In configDict + Dim key = kvp.Key + If Not key.StartsWith(MultiModelPrefix, StringComparison.OrdinalIgnoreCase) Then Continue For + + Dim remainder = key.Substring(MultiModelPrefix.Length) + Dim idx = remainder.LastIndexOf("_"c) + If idx <= 0 OrElse idx = remainder.Length - 1 Then Continue For + + Dim baseKey = remainder.Substring(0, idx) + Dim suffix = remainder.Substring(idx + 1) + + Dim n As Integer + If Integer.TryParse(suffix, n) AndAlso n > 0 AndAlso n <= MaxModelNumber Then + If Not perModel.ContainsKey(n) Then + perModel(n) = New Dictionary(Of String, String)(StringComparer.OrdinalIgnoreCase) + End If + + perModel(n)(baseKey) = kvp.Value + End If + Next + + Return perModel + End Function + Private Shared Function ExtractPrimaryModelConfig(configDict As Dictionary(Of String, String)) As Dictionary(Of String, String) + Dim result As New Dictionary(Of String, String)(StringComparer.OrdinalIgnoreCase) + + For Each kvp In configDict + Dim key = kvp.Key + ' skip keys that clearly belong to numbered or secondary models + Dim idx = key.LastIndexOf("_"c) + Dim numericSuffix As Integer + If idx > 0 AndAlso Integer.TryParse(key.Substring(idx + 1), numericSuffix) AndAlso numericSuffix > 0 AndAlso numericSuffix <= MaxModelNumber Then + ' belongs to model N, handled elsewhere + Continue For + End If + result(key) = kvp.Value + Next + + Return result + End Function + + Private Shared Function ValidateModel(config As Dictionary(Of String, String), modelLabel As String) As Boolean + If config Is Nothing OrElse config.Count = 0 Then + Dim emptyMessage = BuildMissingKeyMessage(modelLabel, Nothing, True, config) + Debug.WriteLine($"[ModelConfigManager] {emptyMessage}") + MessageBox.Show(emptyMessage, + "Model configuration invalid", + MessageBoxButtons.OK, + MessageBoxIcon.Warning) + Return False + End If + + Dim missing As New List(Of String)() + + For Each key In RequiredModelKeys + If Not config.ContainsKey(key) OrElse String.IsNullOrWhiteSpace(config(key)) Then + missing.Add(key) + End If + Next + + If missing.Count > 0 Then + Dim detailedMsg = BuildMissingKeyMessage(modelLabel, missing, False, config) + Debug.WriteLine($"[ModelConfigManager] {detailedMsg}") + MessageBox.Show(detailedMsg, + "Model configuration invalid", + MessageBoxButtons.OK, + MessageBoxIcon.Warning) + Return False + End If + + Return True + End Function + + Private Shared Function BuildMissingKeyMessage(modelLabel As String, + missingKeys As List(Of String), + noEntries As Boolean, + modelConfig As Dictionary(Of String, String)) As String + Dim prefix As String + Dim fixHints As New List(Of String)() + + Dim labelNumber As Integer + Dim hasNumericLabel As Boolean = False + + If Not String.IsNullOrWhiteSpace(modelLabel) AndAlso modelLabel.StartsWith("#", StringComparison.Ordinal) Then + Dim suffix = modelLabel.TrimStart("#"c) + If Integer.TryParse(suffix, labelNumber) Then + hasNumericLabel = True + End If + End If + + If hasNumericLabel Then + Dim identifierSuffix As String = "" + prefix = $"Model #{labelNumber}" + If missingKeys IsNot Nothing Then + For Each key In missingKeys + fixHints.Add($"MultiModel_{key}_{labelNumber}") + Next + Else + fixHints.Add($"MultiModel__{labelNumber}") + End If + + Dim itemNameExists As Boolean = modelConfig IsNot Nothing AndAlso modelConfig.ContainsKey("ItemName") AndAlso Not String.IsNullOrWhiteSpace(modelConfig("ItemName")) + Dim modelNameExists As Boolean = modelConfig IsNot Nothing AndAlso modelConfig.ContainsKey("Model") AndAlso Not String.IsNullOrWhiteSpace(modelConfig("Model")) + + If itemNameExists Then + identifierSuffix = $"MultiModel_ItemName_{labelNumber} = '{modelConfig("ItemName")}'" + ElseIf modelNameExists Then + identifierSuffix = $"MultiModel_Model_{labelNumber} = '{modelConfig("Model")}'" + Else + identifierSuffix = $"MultiModel suffix #{labelNumber}" + End If + + prefix = $"{prefix} ({identifierSuffix})" + Else + prefix = $"Model {modelLabel}" + If missingKeys IsNot Nothing Then + fixHints.AddRange(missingKeys) + Else + fixHints.Add("base INI keys (e.g., Endpoint, Model, APICall, Response)") + End If + + If modelConfig IsNot Nothing Then + Dim primaryName As String = "" + If modelConfig.ContainsKey("ItemName") AndAlso Not String.IsNullOrWhiteSpace(modelConfig("ItemName")) Then + primaryName = $"ItemName = '{modelConfig("ItemName")}'" + ElseIf modelConfig.ContainsKey("Model") AndAlso Not String.IsNullOrWhiteSpace(modelConfig("Model")) Then + primaryName = $"Model = '{modelConfig("Model")}'" + End If + + If Not String.IsNullOrWhiteSpace(primaryName) Then + prefix = $"{prefix} ({primaryName})" + End If + End If + End If + + Dim baseMessage As String + If noEntries Then + baseMessage = $"{prefix} did not contain any configuration entries." + Else + baseMessage = $"{prefix} is missing required fields: {String.Join(", ", missingKeys)}." + End If + + Dim hintText = $"Please set the following INI keys: {String.Join(", ", fixHints.Distinct())}" + Return $"{baseMessage}{Environment.NewLine}{hintText}" + End Function + + Private Shared Function GetModelDisplayNameForConfig(config As Dictionary(Of String, String), n As Integer) As String + If config.ContainsKey("ItemName") AndAlso Not String.IsNullOrWhiteSpace(config("ItemName")) Then + Return config("ItemName") + End If + If config.ContainsKey("Model") AndAlso Not String.IsNullOrWhiteSpace(config("Model")) Then + Return config("Model") + End If + Return $"Model {n}" + End Function + + Private Shared Sub CopyPropertiesToContext(context As ISharedContext, + model As Dictionary(Of String, String)) + ' Strings + context.INI_APIKey = GetValue(model, "APIKey", "") + context.INI_APIKeyBack = context.INI_APIKey + context.INI_APIKeyPrefix = GetValue(model, "APIKeyPrefix", "") + context.INI_Endpoint = GetValue(model, "Endpoint", "") + context.INI_Model = GetValue(model, "Model", "") + context.INI_Temperature = GetValue(model, "Temperature", "") + context.INI_HeaderA = GetValue(model, "HeaderA", "") + context.INI_HeaderB = GetValue(model, "HeaderB", "") + context.INI_Response = GetValue(model, "Response", "") + context.INI_APICall = GetValue(model, "APICall", "") + context.INI_APICall_Object = GetValue(model, "APICall_Object", "") + context.INI_Anon = GetValue(model, "Anon", "") + context.INI_TokenCount = GetValue(model, "TokenCount", "") + context.INI_PreCorrection = GetValue(model, "PreCorrection", "") + context.INI_PostCorrection = GetValue(model, "PostCorrection", "") + context.INI_OAuth2ClientMail = GetValue(model, "OAuth2ClientMail", "") + context.INI_OAuth2Scopes = GetValue(model, "OAuth2Scopes", "") + context.INI_OAuth2Endpoint = GetValue(model, "OAuth2Endpoint", "") + + ' Numerics + context.INI_Timeout = GetLongValue(model, "Timeout", 0) + context.INI_MaxOutputToken = GetIntValue(model, "MaxOutputToken", 0) + context.INI_OAuth2ATExpiry = GetLongValue(model, "OAuth2ATExpiry", 3600) + + ' Booleans + context.INI_APIEncrypted = GetBoolValue(model, "APIKeyEncrypted", False) + context.INI_DoubleS = GetBoolValue(model, "DoubleS", False) + context.INI_Clean = GetBoolValue(model, "Clean", False) + context.INI_NoDash = GetBoolValue(model, "NoEmDash", False) + context.INI_OAuth2 = GetBoolValue(model, "OAuth2", False) + End Sub + + Private Shared Sub UpdateDecodedAPI(context As ISharedContext) + ' Mirrors logic from InitializeConfig for primary API + If context.INI_OAuth2 Then + context.INI_APIKey = Trim(SharedMethods.RealAPIKey(context.INI_APIKey, False, True, context)).Replace(vbLf, "").Replace(vbCr, "") + ' If something goes wrong, RealAPIKey will already have shown an error via SharedMethods + Else + context.DecodedAPI = SharedMethods.RealAPIKey(context.INI_APIKey, False, False, context) + End If + End Sub + + Private Shared Function GetValue(config As Dictionary(Of String, String), + key As String, + defaultValue As String) As String + If config.ContainsKey(key) Then + Return config(key) + End If + Return defaultValue + End Function + + Private Shared Function GetLongValue(config As Dictionary(Of String, String), + key As String, + defaultValue As Long) As Long + If config.ContainsKey(key) Then + Dim v As Long + If Long.TryParse(config(key), v) Then + Return v + End If + End If + Return defaultValue + End Function + + Private Shared Function GetIntValue(config As Dictionary(Of String, String), + key As String, + defaultValue As Integer) As Integer + If config.ContainsKey(key) Then + Dim v As Integer + If Integer.TryParse(config(key), v) Then + Return v + End If + End If + Return defaultValue + End Function + + Private Shared Function GetBoolValue(config As Dictionary(Of String, String), + key As String, + defaultValue As Boolean) As Boolean + If config.ContainsKey(key) Then + Dim raw = config(key).Trim().ToLowerInvariant() + Return raw = "yes" OrElse raw = "true" OrElse raw = "ja" OrElse raw = "wahr" OrElse raw = "1" + End If + Return defaultValue + End Function + + End Class + +End Namespace + + diff --git a/SharedLibrary/Code/Core/Config/SharedMethods.LoadConfig.vb b/SharedLibrary/Code/Core/Config/SharedMethods.LoadConfig.vb index 91d126d..26cb6fd 100644 --- a/SharedLibrary/Code/Core/Config/SharedMethods.LoadConfig.vb +++ b/SharedLibrary/Code/Core/Config/SharedMethods.LoadConfig.vb @@ -78,11 +78,24 @@ Namespace SharedLibrary If Not String.IsNullOrEmpty(trimmedLine) AndAlso Not trimmedLine.StartsWith(";") Then ' Skip comments and empty lines Dim keyValue = trimmedLine.Split(New Char() {"="c}, 2) If keyValue.Length = 2 Then - configDict(keyValue(0).Trim()) = keyValue(1).Trim() + Dim key = keyValue(0).Trim() + Dim value = keyValue(1).Trim() + + If Not configDict.ContainsKey(key) Then + configDict(key) = value + ElseIf Not String.IsNullOrWhiteSpace(value) Then + ' Prefer non-empty overrides; ignore blank defaults that would wipe earlier values + configDict(key) = value + End If End If End If Next + ' Detect available models using ModelConfigManager. + ' "Primary" = the original top-level INI_* keys (no suffix) that will be treated as Model 1 when no MultiModel_*_ keys exist. + ' New configs should prefer the MultiModel__ naming convention introduced by ModelConfigManager. + ModelConfigManager.DetectAndStoreModels(configDict) + ' Assign and validate configuration values context.INI_APIKey = If(configDict.ContainsKey("APIKey"), configDict("APIKey"), "") context.INI_Endpoint = If(configDict.ContainsKey("Endpoint"), configDict("Endpoint"), "") @@ -334,6 +347,16 @@ Namespace SharedLibrary Return End If + ' Apply selected model to primary fields if multi-model configuration is present. + Dim availableModels = ModelConfigManager.GetAvailableModels() + If availableModels.Count > 0 Then + Dim modelToSelect As Integer = ModelConfigManager.LoadSavedModelNumber() + If Not availableModels.Contains(modelToSelect) Then + modelToSelect = 1 + End If + ModelConfigManager.SelectModel(context, modelToSelect) + End If + ' Additional configurations for OAuth2 context.TokenExpiry = Microsoft.VisualBasic.DateAndTime.DateAdd(Microsoft.VisualBasic.DateInterval.Year, -1, DateTime.Now) context.DecodedAPI = "" diff --git a/SharedLibrary/My Project/Settings.Designer.vb b/SharedLibrary/My Project/Settings.Designer.vb index 4e4acf1..a245387 100644 --- a/SharedLibrary/My Project/Settings.Designer.vb +++ b/SharedLibrary/My Project/Settings.Designer.vb @@ -431,6 +431,18 @@ Namespace My Me("LicenseWarningStartCount") = value End Set End Property + + _ + Public Property SelectedModelNumber() As Integer + Get + Return CType(Me("SelectedModelNumber"),Integer) + End Get + Set + Me("SelectedModelNumber") = value + End Set + End Property End Class End Namespace diff --git a/SharedLibrary/My Project/Settings.settings b/SharedLibrary/My Project/Settings.settings index d151a5c..8c83c7f 100644 --- a/SharedLibrary/My Project/Settings.settings +++ b/SharedLibrary/My Project/Settings.settings @@ -98,5 +98,8 @@ 0 + + 1 + \ No newline at end of file diff --git a/SharedLibrary/SharedLibrary.vbproj b/SharedLibrary/SharedLibrary.vbproj index a8ae4d1..6e3d54e 100644 --- a/SharedLibrary/SharedLibrary.vbproj +++ b/SharedLibrary/SharedLibrary.vbproj @@ -333,6 +333,7 @@ +