Parcourir la source

Ajoutez des fichiers projet.

master
Guillaume TOPENOT il y a 1 semaine
Parent
révision
4118512ec8
100 fichiers modifiés avec 162608 ajouts et 0 suppressions
  1. +68
    -0
      .gitignore
  2. +16
    -0
      CVService/CVService.csproj
  3. +316
    -0
      CVService/Services/GenererCVService.cs
  4. +346
    -0
      CVService/Services/RechercheCVService.cs
  5. +27
    -0
      CVService/Services/ReponseRechercheCV.cs
  6. +15
    -0
      ChatConversationStructure/ChatConversation.cs
  7. +9
    -0
      ChatConversationStructure/ChatConversationStructure.csproj
  8. +14
    -0
      ChatConversationStructure/ChatMessage.cs
  9. +14
    -0
      ChatService/ChatService.csproj
  10. +235
    -0
      ChatService/Services/ChatService.cs
  11. +52
    -0
      DocumentsEntreprisesServices/DocumentsEntreprisesService.csproj
  12. +245
    -0
      DocumentsEntreprisesServices/Services/FactureService.cs
  13. +157
    -0
      DocumentsEntreprisesServices/Services/TF_From_DSFService.cs
  14. +127
    -0
      DocumentsEntreprisesServices/Services/TranscriptionAndResumeService.cs
  15. +1
    -0
      DocumentsEntreprisesServices/ServicesExternes/ffmpeg/_A_LIRE.txt
  16. +21
    -0
      EmailService/EmailService.csproj
  17. +544
    -0
      EmailService/Services/EmailSendService.cs
  18. +1067
    -0
      EmailService/Services/EmailService.cs
  19. +36
    -0
      EmailService/Services/TaskService.cs
  20. +24
    -0
      EmailStructure/CompteMailUser.cs
  21. +123
    -0
      EmailStructure/EmailSendStructure.cs
  22. +179
    -0
      EmailStructure/EmailStructure.cs
  23. +13
    -0
      EmailStructure/EmailStructure.csproj
  24. +36
    -0
      FaceRecognition/FaceRecognition.csproj
  25. +529
    -0
      FaceRecognition/FaceRecognitionService.cs
  26. +5
    -0
      FaceRecognition/Models/_A LIRE.txt
  27. BIN
      FaceRecognition/Models/face_detection_yunet_2023mar.onnx
  28. +26161
    -0
      FaceRecognition/Models/haarcascade_frontalface_alt.xml
  29. +33314
    -0
      FaceRecognition/Models/haarcascade_frontalface_default.xml
  30. +94785
    -0
      FaceRecognition/Models/model_reconnaissance.yml
  31. +8
    -0
      FaceRecognition/ParametresFaceRecognitionService.cs
  32. +13
    -0
      FooocusService/FooocusService.csproj
  33. +241
    -0
      FooocusService/Services/FooocusRequest.cs
  34. +346
    -0
      FooocusService/Services/FooocusService.cs
  35. +14
    -0
      MaUI/App.xaml
  36. +11
    -0
      MaUI/App.xaml.cs
  37. +15
    -0
      MaUI/AppShell.xaml
  38. +9
    -0
      MaUI/AppShell.xaml.cs
  39. +84
    -0
      MaUI/MaUI.csproj
  40. +8
    -0
      MaUI/MaUI.csproj.user
  41. +15
    -0
      MaUI/MainPage.xaml
  42. +48
    -0
      MaUI/MainPage.xaml.cs
  43. +24
    -0
      MaUI/MauiProgram.cs
  44. +22
    -0
      MaUI/Model/base/LOGS.cs
  45. +0
    -0
      MaUI/Model/custom/_METTRE ICI LES AJOUTS CUSTOMS.txt
  46. +46
    -0
      MaUI/Pages/AuthLogin/Login.xaml
  47. +13
    -0
      MaUI/Pages/AuthLogin/Login.xaml.cs
  48. +83
    -0
      MaUI/Pages/ChatRoom/ChatRoom_LLM.xaml
  49. +18
    -0
      MaUI/Pages/ChatRoom/ChatRoom_LLM.xaml.cs
  50. +30
    -0
      MaUI/Pages/Mails/ListeEmails.xaml
  51. +15
    -0
      MaUI/Pages/Mails/ListeEmails.xaml.cs
  52. +6
    -0
      MaUI/Platforms/Android/AndroidManifest.xml
  53. +10
    -0
      MaUI/Platforms/Android/MainActivity.cs
  54. +15
    -0
      MaUI/Platforms/Android/MainApplication.cs
  55. +6
    -0
      MaUI/Platforms/Android/Resources/values/colors.xml
  56. +9
    -0
      MaUI/Platforms/MacCatalyst/AppDelegate.cs
  57. +14
    -0
      MaUI/Platforms/MacCatalyst/Entitlements.plist
  58. +38
    -0
      MaUI/Platforms/MacCatalyst/Info.plist
  59. +15
    -0
      MaUI/Platforms/MacCatalyst/Program.cs
  60. +16
    -0
      MaUI/Platforms/Tizen/Main.cs
  61. +15
    -0
      MaUI/Platforms/Tizen/tizen-manifest.xml
  62. +8
    -0
      MaUI/Platforms/Windows/App.xaml
  63. +24
    -0
      MaUI/Platforms/Windows/App.xaml.cs
  64. +46
    -0
      MaUI/Platforms/Windows/Package.appxmanifest
  65. +15
    -0
      MaUI/Platforms/Windows/app.manifest
  66. +9
    -0
      MaUI/Platforms/iOS/AppDelegate.cs
  67. +32
    -0
      MaUI/Platforms/iOS/Info.plist
  68. +15
    -0
      MaUI/Platforms/iOS/Program.cs
  69. +51
    -0
      MaUI/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
  70. +8
    -0
      MaUI/Properties/launchSettings.json
  71. +4
    -0
      MaUI/Resources/AppIcon/appicon.svg
  72. +8
    -0
      MaUI/Resources/AppIcon/appiconfg.svg
  73. BIN
      MaUI/Resources/Fonts/OpenSans-Regular.ttf
  74. BIN
      MaUI/Resources/Fonts/OpenSans-Semibold.ttf
  75. BIN
      MaUI/Resources/Images/dotnet_bot.png
  76. +15
    -0
      MaUI/Resources/Raw/AboutAssets.txt
  77. +8
    -0
      MaUI/Resources/Splash/splash.svg
  78. +45
    -0
      MaUI/Resources/Styles/Colors.xaml
  79. +427
    -0
      MaUI/Resources/Styles/Styles.xaml
  80. +180
    -0
      MaUI/Services/base/ApiService.cs
  81. +0
    -0
      MaUI/Services/custom/_METTRE ICI LES AJOUTS CUSTOMS.txt
  82. +122
    -0
      MaUI/ViewModel/AuthLogin/AuthLogin_VM.cs
  83. +160
    -0
      MaUI/ViewModel/base/ChatRoom/ChatRoom_VM.cs
  84. +167
    -0
      MaUI/ViewModel/base/Mails/ListeEmails_VM.cs
  85. +0
    -0
      MaUI/ViewModel/custom/_METTRE ICI LES AJOUTS CUSTOMS.txt
  86. +46
    -0
      OllamaModels/OllamaModels.cs
  87. +9
    -0
      OllamaModels/OllamaModels.csproj
  88. +35
    -0
      OllamaModels/OllamaRequest.cs
  89. +20
    -0
      OllamaService/OllamaService.csproj
  90. +56
    -0
      OllamaService/Services/EmbeddingService.cs
  91. +167
    -0
      OllamaService/Services/ModelSelector.cs
  92. +721
    -0
      OllamaService/Services/OllamaService.cs
  93. +11
    -0
      OllamaService/Services/OllamaTagsResponse.cs
  94. +17
    -0
      OllamaService/Services/ParametresOllamaService.cs
  95. BIN
      Prompt et longueur de prompt.docx
  96. +31
    -0
      RAGService/RAGService.csproj
  97. +8
    -0
      RAGService/Services/ParametresQdrantService.cs
  98. +8
    -0
      RAGService/Services/ParametresRAGService.cs
  99. +449
    -0
      RAGService/Services/QdrantService.cs
  100. +0
    -0
      RAGService/Services/RAGService.cs

+ 68
- 0
.gitignore Voir le fichier

@@ -0,0 +1,68 @@
## A streamlined .gitignore for modern .NET projects
## including temporary files, build results, and
## files generated by popular .NET tools. If you are
## developing with Visual Studio, the VS .gitignore
## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
## has more thorough IDE-specific entries.
##
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# ASP.NET Scaffolding
ScaffoldingReadMe.txt

# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg

# Others
~$*
*~
CodeCoverage/

# MSBuild Binary and Structured Log
*.binlog

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
/.vs
*.onnx_data
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/ffmpeg.exe
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/ffplay.exe
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/ffprobe.exe
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avcodec-62.dll
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avdevice-62.dll
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avfilter-11.dll
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avformat-62.dll
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avutil-60.dll
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/swresample-6.dll
/DocumentsEntreprisesServices/ServicesExternes/ffmpeg/swscale-9.dll
/RAGService/onnx/model.onnx
/RAGService/onnx/tokenizer.json

+ 16
- 0
CVService/CVService.csproj Voir le fichier

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\RAGService\RAGService.csproj" />
<ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" />
<ProjectReference Include="..\ToolsService\ToolsService.csproj" />
</ItemGroup>

</Project>

+ 316
- 0
CVService/Services/GenererCVService.cs Voir le fichier

@@ -0,0 +1,316 @@
using Services.ReActAgent;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using ToolsServices;

namespace Services
{
public static class GenererCVService
{
#region Méthodes publiques
public static async Task<bool> GenerateOneCV(string fullnameCV, string fullnameTemplateCV, string fullPathOutput, string titreCV, int nbrCompetencesMax, bool isCVAnonyme, bool isUsingIA)
{
LoggerService.LogInfo("GenererCVService.GenerateOneCV");
try
{
var reActRagAgent = new Services.ReActAgent.ReActAgent();

#region Logs + vérification de l'existence des fichiers
LoggerService.LogInfo($"Génération d'un CV");
LoggerService.LogInfo($"Fichier CV : {fullnameCV}");

if (!File.Exists(fullnameCV))
{
LoggerService.LogWarning($"Le fichier CV n'existe pas : {fullnameCV}.");
return false;
}

if (!File.Exists(fullnameTemplateCV))
{
LoggerService.LogWarning($"Le fichier Template de CV n'existe pas : {fullnameTemplateCV}.");
return false;
}

if (!Directory.Exists(fullPathOutput))
{
LoggerService.LogWarning($"Le dossier des exports des nouveaux CV n'existe pas : {fullPathOutput}.");
return false;
}
#endregion

string nameNewFileXML = "";
if (isUsingIA)
{
#region Vérification si injection de prompt
bool isPromptInjectionCV = false;
var cvText = reActRagAgent.CleanUserInput(FilesService.ExtractText(fullnameCV), out isPromptInjectionCV);
if (isPromptInjectionCV)
{
LoggerService.LogWarning("Attention, le texte CV contient des éléments suspects");
}
#endregion

#region Appel au LLM pour extraire les données et les adapter
/*
var prompt1 = $@"
Tu es un assistant RH.

Voici un CV brut :
<<<
{cvText}
>>>

Voici une offre d’emploi :
<<<
{ficheMissionText}
>>>

Ta mission :
- Réécris le CV de façon à ce qu’il corresponde au mieux à l’offre
- Mets en avant les compétences pertinentes
- Réorganise les expériences importantes
- Reformule le résumé professionnel avec les bons mots-clés
- Supprime les parties non utiles pour ce poste

Rends un CV **entièrement réécrit** au format texte brut. Ne commente pas.
";
*/
var prompt1 = PromptService.GetPrompt(PromptService.ePrompt.GenererCVService_GenerateOneCV_Extract, cvText);

LoggerService.LogInfo($"Génération du CV - phase A (extraction des infos) : {fullnameCV}");
var (reponse1, m1) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.CV_To_Xml, true, prompt1, "Génération de 1 CV - phase A");
var reponseCompleteClean1 = reponse1.Replace("\\n", "\n").Replace("\\r", "\r");
#endregion

#region Appel au LLM pour mettre au format XML
/*
var prompt2 = $@"
Voici un CV rédigé en texte. Reformate-le en XML structuré selon ce modèle :

<CV>
<Identite>
<Nom></Nom>
<Prenom></Prenom>
<Email></Email>
<Telephone></Telephone>
<Adresse></Adresse>
<DateDeNaissance></DateDeNaissance>
<Nationalite></Nationalite>
</Identite>
<Profil></Profil>
<Competences>
<Competence nom="" niveau="" />
...
</Competences>
<Langues>
<Langue nom="" niveau="" />
...
</Langues>
<Experiences>
<Experience>
<Poste></Poste>
<Entreprise></Entreprise>
<Lieu></Lieu>
<DateDebut></DateDebut>
<DateFin></DateFin>
<Description></Description>
</Experience>
...
</Experiences>
<Formations>
<Formation>
<Diplome></Diplome>
<Etablissement></Etablissement>
<DateDebut></DateDebut>
<DateFin></DateFin>
</Formation>
...
</Formations>
<Certifications>
<Certification>
<Nom></Nom>
<Organisme></Organisme>
<Date></Date>
</Certification>
...
</Certifications>
</CV>

Si une information est absente, laisse le champ vide. Ne commente pas.

CV :
<<<
{reponseCompleteClean1}
>>>

";
*/
var prompt2 = PromptService.GetPrompt(PromptService.ePrompt.GenererCVService_GenerateOneCV_Format, reponseCompleteClean1);

LoggerService.LogInfo($"Génération du CV - phase B (formattage en XML) : {fullnameCV}");
var (reponse,m2) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.CV_To_Xml, true, prompt2, "Génération de 1 CV - phase B");
var reponseCompleteClean = reponse.Replace("\\n", "\n").Replace("\\r", "\r");
reponseCompleteClean = reponseCompleteClean.Replace("&", " et ");
reponseCompleteClean = reponseCompleteClean.Replace("</start_of_turn>", "");
#endregion

#region Enregistrer le XML
// Création du XML
nameNewFileXML = $"{fullnameCV}_{DateTime.Now.ToString("yyyyMMdd_hhmmss")}.xml";
if (File.Exists(nameNewFileXML))
File.Delete(nameNewFileXML);
System.IO.File.WriteAllText(nameNewFileXML, reponseCompleteClean);
#endregion

}
else
{
LoggerService.LogInfo($"Pas de modèle IA utilisé");

#region Extraction du texte + générer un XML
var cv = FilesService.ExtractText(fullnameCV);
cv = Regex.Replace(cv, @"\s{2,}", " ").Trim();
var xml = GenerateCvXml(cv);
nameNewFileXML = $"{fullnameCV}_{DateTime.Now.ToString("yyyyMMdd_hhmmss")}.xml";
if (File.Exists(nameNewFileXML))
File.Delete(nameNewFileXML);
xml.Save(nameNewFileXML);
#endregion
}

#region Génération du CV sur la base du XML et d'un template

// Génération du Word
var t1 = System.IO.Path.GetFileNameWithoutExtension(fullnameCV);
var t2 = System.IO.Path.GetFileNameWithoutExtension(fullnameTemplateCV);
var fullnameNewCV = System.IO.Path.Combine(fullPathOutput, $"{t2}_{t1}.docx");
var b = DocxService.Genere_Docx(fullnameTemplateCV, nameNewFileXML, fullnameNewCV, titreCV, nbrCompetencesMax, isCVAnonyme);
if (File.Exists(nameNewFileXML))
File.Delete(nameNewFileXML);
#endregion

return b;
}
catch (Exception ex)
{
LoggerService.LogError($"Erreur dans la génération du CV (GenererCVService.GenerateOneCV) : {ex.Message}");
return false;
}
}
#endregion

#region Méthodes privées
private static XElement GenerateCvXml(string text)
{
LoggerService.LogInfo("GenererCVService.GenerateCvXml");

// === Nom / Prénom ===
var nameMatch = Regex.Match(text, @"\b([A-Z][a-zéèàêâïî\-']+)\s+([A-ZÉÈÊËÀÂÄÎÏÔÖÙÛÜÇ\-']{2,})\b");
string prenom = nameMatch.Success ? nameMatch.Groups[1].Value : "";
string nom = nameMatch.Success ? nameMatch.Groups[2].Value : "";

// === Email ===
var emailMatch = Regex.Match(text, @"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}");
string email = emailMatch.Success ? emailMatch.Value : "";

// === Téléphone ===
var telMatch = Regex.Match(text, @"0[1-9](?:[\s.-]?\d{2}){4}");
string telephone = telMatch.Success ? telMatch.Value : "";

// === Profil ===
string profil = ExtractSection(text, "PROFIL", "FORMATIONS");

// === Compétences ===
var competenceMatches = Regex.Matches(text, @"•\s?(.*?)(?=•|PROJECT MANAGER|EXPERIENCES CLES|PROFIL|$)");
var competences = new XElement("Competences");
foreach (Match match in competenceMatches)
{
string label = match.Groups[1].Value.Trim();
if (!string.IsNullOrWhiteSpace(label) && label.Contains(":"))
{
string domaine = label.Split(":")[0].Trim();
competences.Add(new XElement("Competence",
new XAttribute("nom", domaine),
new XAttribute("niveau", "Non précisé")
));
}
}

// === Langues ===
var langues = new XElement("Langues");
if (text.Contains("Anglais")) langues.Add(new XElement("Langue", new XAttribute("nom", "Anglais"), new XAttribute("niveau", "Professionnel (Bulats B1)")));
if (text.Contains("Espagnol")) langues.Add(new XElement("Langue", new XAttribute("nom", "Espagnol"), new XAttribute("niveau", "Scolaire")));

// === Expériences ===
var experiences = new XElement("Experiences");
var expMatches = Regex.Matches(text, @"(\d{4})\s*-\s*(.+?)\s+([A-Z].*?)\s+-\s+(.+?)\s+(.*?)\s(?=\d{4}\s*-|$)", RegexOptions.Singleline);
foreach (Match match in expMatches)
{
experiences.Add(new XElement("Experience",
new XElement("Poste", match.Groups[2].Value.Trim()),
new XElement("Entreprise", match.Groups[3].Value.Trim()),
new XElement("Lieu", match.Groups[4].Value.Trim()),
new XElement("DateDebut", match.Groups[1].Value.Trim()),
new XElement("DateFin", ""), // à adapter si fin détectable
new XElement("Description", match.Groups[5].Value.Trim())
));
}

// === Formations ===
var formations = new XElement("Formations");
var formationMatches = Regex.Matches(text, @"(\d{4})\s*–\s*(\d{4})\s*---\s*(.+?)\((\d{2})\)");
foreach (Match match in formationMatches)
{
formations.Add(new XElement("Formation",
new XElement("Diplome", "Non précisé"),
new XElement("Etablissement", match.Groups[3].Value.Trim()),
new XElement("DateDebut", match.Groups[1].Value.Trim()),
new XElement("DateFin", match.Groups[2].Value.Trim())
));
}

// Ajout manuel du Master si non capturé
if (!text.Contains("IAE de Montpellier"))
{
formations.Add(new XElement("Formation",
new XElement("Diplome", "Master 2 : Administration des Entreprises"),
new XElement("Etablissement", "IAE de Montpellier"),
new XElement("DateDebut", "2009"),
new XElement("DateFin", "2011")
));
}

return new XElement("CV",
new XElement("Identite",
new XElement("Nom", nom),
new XElement("Prenom", prenom),
new XElement("Email", email),
new XElement("Telephone", telephone),
new XElement("Adresse", ""),
new XElement("DateDeNaissance", ""),
new XElement("Nationalite", "")
),
new XElement("Profil", profil),
competences,
langues,
experiences,
formations,
new XElement("Certifications")
);
}

private static string ExtractSection(string text, string startLabel, string endLabel)
{
LoggerService.LogInfo("GenererCVService.ExtractSection");

int start = text.IndexOf(startLabel, StringComparison.OrdinalIgnoreCase);
int end = text.IndexOf(endLabel, start + startLabel.Length, StringComparison.OrdinalIgnoreCase);
if (start >= 0 && end > start)
{
return text.Substring(start + startLabel.Length, end - start - startLabel.Length).Trim();
}
return "";
}
#endregion
}
}

+ 346
- 0
CVService/Services/RechercheCVService.cs Voir le fichier

@@ -0,0 +1,346 @@
using ToolsServices;
using Services.ReActAgent;
using System.Collections.ObjectModel;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Services
{
public static class RechercheCVService
{
#region Variables
private static readonly string NomFichierRechercheCV = FichiersInternesService.ListeRechercheCV;// "listeRechercheCV.json";
private static readonly string NomFichierParametres = FichiersInternesService.ParamsRechercheCV;

#endregion

#region Méthodes publiques
public static (string, string) LoadParametres()
{
LoggerService.LogInfo("RechercheCVService.LoadParametres");
//ParametresOllamaService SelectedItem = new();

try
{
string FicheMission="";
string PathCV="";
if (File.Exists(NomFichierParametres))
{

string[] lignes = File.ReadAllLines(NomFichierParametres);

if (lignes.Length > 0)
FicheMission = lignes[0];

if (lignes.Length > 1)
PathCV = lignes[1];

}
return (FicheMission, PathCV);

}
catch
{
LoggerService.LogError($"Erreur lors du chargement des paramètres depuis {NomFichierParametres}");
return ("","");
}
}

public static bool SaveParametres(string FicheMission, string PathCV)
{
LoggerService.LogInfo("RechercheCVService.SaveParametres");
try
{
StringBuilder sb = new();
sb.AppendLine(FicheMission);
sb.AppendLine(PathCV);

File.WriteAllText(NomFichierParametres, sb.ToString());

return true;
}
catch
{
LoggerService.LogError($"Erreur lors de la sauvegarde des paramètres dans {NomFichierParametres}");
return false;
}

}

public static async Task<List<ReponseRechercheCV>?> RechercherCV(string ficheMission, string dossierCV, bool isGenererXML)
{
LoggerService.LogInfo("RechercheCVService.RechercheCV");

var reActRagAgent = new ReActAgent.ReActAgent();
var modeleIA = ReActAgent.ReActAgent.GetModeleIA(ModelsUseCases.TypeUseCase.AnalyseCVMission);
LoggerService.LogInfo($"Analyse des CV");
LoggerService.LogInfo($"Fiche mission : {ficheMission}");
LoggerService.LogInfo($"Dossier CV : {dossierCV}");
LoggerService.LogInfo($"Extraire les CV en XML : {isGenererXML}");
LoggerService.LogInfo($"Modèle utilisé : {modeleIA}");

if (!Directory.Exists(dossierCV))
{
LoggerService.LogWarning($"Le dossier {dossierCV} n'existe pas.");
return null;
}

if (!File.Exists(ficheMission))
{
LoggerService.LogWarning($"Le fichier {ficheMission} n'existe pas.");
return null;
}

var fichiers = Directory
.EnumerateFiles(dossierCV, "*.*", SearchOption.AllDirectories)
.Where(f => f.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase))
.ToArray();

if (fichiers.Length == 0)
{
LoggerService.LogWarning($"Aucun fichier trouvé dans le dossier spécifié : {dossierCV}");
return null;
}
string messagePromptInjection1 = "";
bool isPromptInjection = false;

var ficheMissionText = reActRagAgent.CleanUserInput(FilesService.ExtractText(ficheMission), out isPromptInjection);
if(isPromptInjection)
{
var msg = "Attention, le texte de la fiche de mission contient des éléments suspects";
messagePromptInjection1 = $"{msg}.\n";
LoggerService.LogWarning(msg);
}
List<ReponseRechercheCV> retours = new();
foreach (var fichier in fichiers)
{
var bIngest = await RAGService.IngestDocument(Domain.CV, true, false, FilesService.ExtractText(fichier), fichier);


}

foreach (var fichier in fichiers)
{

try
{
#region Analyse du CV
ReponseRechercheCV retour = new();
string messagePromptInjection2 = "";
var texteCV = reActRagAgent.CleanUserInput(FilesService.ExtractText(fichier), out isPromptInjection);

if(isPromptInjection)
{
var msg = "Attention, le texte de la fiche de mission contient des éléments suspects";
messagePromptInjection2 = $"{msg}.\n";
retour.PresenceSuspecte = true;
LoggerService.LogWarning($"{msg}: {fichier}");
}
/*
var prompt = $@"
Tu es un expert en recrutement. Ton rôle est d'évaluer la compatibilité entre une offre d'emploi et un CV.

Analyse comparative :

1. **Points forts** : liste les éléments du CV qui correspondent bien aux exigences de l'offre (compétences, expériences, formations, etc.).

2. **Points manquants ou faibles** : identifie les éléments de l'offre qui sont absents ou peu développés dans le CV.

3. **Note de compatibilité** : donne une note globale de correspondance entre le CV et l'offre, sur 10, avec une explication.

Voici les documents à comparer :

---
**Offre d’emploi** :
{ficheMissionText}

---
**CV du candidat** :
{texteCV}

---
Réponds en suivant la structure suivante :

**Points forts :**
- …

**Points manquants ou faibles :**
- …

**Note de compatibilité (sur 10) :**
X/10 – Explication : …

";
*/
var prompt = PromptService.GetPrompt(PromptService.ePrompt.RechercheCVService_RechercheCV_Analyse, ficheMissionText, texteCV);

LoggerService.LogInfo($"Analyse du CV : {fichier}");
var (reponse,m1) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseCVMission, true, prompt,"Analyse de CV");
var reponseComplete = $"Pour le CV {fichier} :\n{reponse}\n";
//var reponseClean = reponse.Replace("\\n", "\n").Replace("\\r", "\r");
var reponseCompleteClean = reponseComplete.Replace("\\n", "\n").Replace("\\r", "\r");

retour.FichierCV = fichier;
retour.Avis = messagePromptInjection1 + messagePromptInjection2 + reponseCompleteClean;

// Note : On peut ajouter une logique pour extraire la note de la réponse si nécessaire
if (reponse.Contains("Note de compatibilité"))
{
var noteMatch = System.Text.RegularExpressions.Regex.Match(reponse, @"(\d+)/10");
if (noteMatch.Success && int.TryParse(noteMatch.Groups[1].Value, out int note))
{
retour.Note = note;
}
}
#endregion

#region Renseigner le modèle IA
retour.ModeleIA = modeleIA;
#endregion

#region Génération XML
if (isGenererXML)
{
/*
var promptXML = $@"
Tu es un assistant spécialisé dans l'extraction d'informations depuis des CV.
Je vais te fournir le contenu brut d’un CV (texte brut sans mise en forme).
À partir de ce contenu, génère un document structuré au format XML en respectant la structure suivante :

<CV>
<Identite>
<Nom></Nom>
<Prenom></Prenom>
<Email></Email>
<Telephone></Telephone>
<Adresse></Adresse>
<DateDeNaissance></DateDeNaissance>
<Nationalite></Nationalite>
</Identite>
<Profil></Profil>
<Competences>
<Competence nom=""..."" niveau=""..."" />
...
</Competences>
<Langues>
<Langue nom=""..."" niveau=""..."" />
...
</Langues>
<Experiences>
<Experience>
<Poste></Poste>
<Entreprise></Entreprise>
<Lieu></Lieu>
<DateDebut></DateDebut>
<DateFin></DateFin>
<Description></Description>
</Experience>
...
</Experiences>
<Formations>
<Formation>
<Diplome></Diplome>
<Etablissement></Etablissement>
<DateDebut></DateDebut>
<DateFin></DateFin>
</Formation>
...
</Formations>
<Certifications>
<Certification>
<Nom></Nom>
<Organisme></Organisme>
<Date></Date>
</Certification>
...
</Certifications>
</CV>

Si une information est absente, laisse le champ vide. Ne fais aucun commentaire, retourne uniquement le XML.
N'oublie aucune expérience, aucune formation, aucune langue, ni aucune certification.
Formate le tout proprement au format XML. N'ajoute pas de texte explicatif. Commence directement par <document>.

Voici un contenu PDF extrait :

{texteCV}

";
*/
var promptXML = PromptService.GetPrompt(PromptService.ePrompt.RechercheCVService_RechercheCV_Generate, texteCV);
var (reponseXML,m2) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseCVMission, true, promptXML,"CV en XML");
var reponseCompleteCleanXML = reponseXML.Replace("\\n", "\n").Replace("\\r", "\r");

retour.VersionXML = reponseCompleteCleanXML;

}
#endregion

#region Vectorisation du CV
await RAGService.IngestDocument(Domain.CV,true, false, texteCV, fichier);
//reActRagAgent.IngestDocument(texteCV, fichier);
#endregion

retours.Add(retour);
}

catch (Exception ex)
{
LoggerService.LogError($"Erreur lors de l'extraction du texte de {fichier} : {ex.Message}");
}
}

LoggerService.LogInfo($"Analyse des CV terminée");
return retours;
}

public static async Task SauvegarderRechercheAsync(ObservableCollection<ReponseRechercheCV> ListeItems)
{
LoggerService.LogInfo("RechercheCVService.SauvegarderRechercheAsync");

if (ListeItems == null)
return;

var options = new JsonSerializerOptions
{
WriteIndented = true,
// Ignore les propriétés non sérialisables comme UniqueId si nécessaire
IgnoreReadOnlyProperties = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

using FileStream fs = File.Create(NomFichierRechercheCV);
await JsonSerializer.SerializeAsync(fs, ListeItems, options);
}

public static async Task<ObservableCollection<ReponseRechercheCV>> ChargerCVDepuisJsonAsync(ObservableCollection<ReponseRechercheCV> ListeItems)
{
LoggerService.LogInfo("RechercheCVService.ChargerCVDepuisJsonAsync");

if (!File.Exists(NomFichierRechercheCV))
return new ObservableCollection<ReponseRechercheCV>();

using FileStream fs = File.OpenRead(NomFichierRechercheCV);
var items = await JsonSerializer.DeserializeAsync<ObservableCollection<ReponseRechercheCV>>(fs);

if (items != null)
{
ListeItems = items;
}
return ListeItems;
}
public static async Task<string> SeekByKeyWord(string RechercheString)
{
LoggerService.LogInfo("RechercheCVService.SeekByKeyWord");
return await RAGService.Search(Domain.CV, RechercheString);
}
#endregion

#region Méthodes privées
#endregion
}
}

+ 27
- 0
CVService/Services/ReponseRechercheCV.cs Voir le fichier

@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;

namespace Services
{
public class ReponseRechercheCV
{
public string FichierCV { get; set; } = "";
public string Avis { get; set; } = "";
public int Note { get; set; } = 0;
public string ModeleIA { get; set; } = "";
public bool PresenceSuspecte { get; set; } = false;
public string VersionXML { get; set; } = "";

[JsonIgnore]
public string PresenceSuspecte_STR
{
get
{
if (PresenceSuspecte)
return "⚠️";
return "";
}
}

}

}

+ 15
- 0
ChatConversationStructure/ChatConversation.cs Voir le fichier

@@ -0,0 +1,15 @@
namespace Services
{
public class ChatConversation
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Type { get; set; } = "";
public string Model { get; set; } = "";
public string Title { get; set; } = "";
public string TitleLong { get; set; } = "";
public DateTime LastUse { get; set; }=DateTime.Now;

public List<ChatMessage> Messages { get; set; } = new();

}
}

+ 9
- 0
ChatConversationStructure/ChatConversationStructure.csproj Voir le fichier

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>

+ 14
- 0
ChatConversationStructure/ChatMessage.cs Voir le fichier

@@ -0,0 +1,14 @@
using System.Text.Json.Serialization;

namespace Services
{
public class ChatMessage
{
public string Role { get; set; } = "";// "user" ou "assistant"
public string Content { get; set; } = "";
public DateTime Timestamp { get; set; } = DateTime.Now;
public string Model { get; set; } = "";
}


}

+ 14
- 0
ChatService/ChatService.csproj Voir le fichier

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" />
<ProjectReference Include="..\ToolsService\ToolsService.csproj" />
</ItemGroup>

</Project>

+ 235
- 0
ChatService/Services/ChatService.cs Voir le fichier

@@ -0,0 +1,235 @@
using Services.Fooocus;
using Services.Models;
using Services.ReActAgent;
using System.Text.Json;
using ToolsServices;
using static Services.ReActAgent.ModelsUseCases;

namespace Services
{
public static class ChatService
{
#region Variables
private static string Folder => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Conversations");
private static Services.ReActAgent.ReActAgent _ReActAagent = new();
#endregion

#region Constructeur
static ChatService()
{
LoggerService.LogInfo($"ChatService");
try
{
if (!Directory.Exists(Folder))
{
LoggerService.LogDebug($"ChatService : tentative de création du dossier de conversations : {Folder}");
Directory.CreateDirectory(Folder);
LoggerService.LogInfo($"ChatService : dossier de conversations créé : {Folder}");
}
}
catch (Exception ex)
{
LoggerService.LogError($"ChatService : Erreur : {ex.Message}");
}
}
#endregion

#region Méthodes publiques
public static string GetModeleIA(ModelsUseCases.TypeUseCase useCase)
{
return ReActAgent.ReActAgent.GetModeleIA(useCase);
}
public static IEnumerable<string> GetModelesIA(ModelsUseCases.TypeUseCase useCase)
{
return ReActAgent.ReActAgent.GetModelesIA(useCase);
}
public static async Task<List<string>> GetFooocusStylesAsync()
{
return await _ReActAagent.GetFooocusStylesAsync();
}

public static (string, string, string) LoadParametresGenerateImg()
{
return _ReActAagent.LoadParametresGenerateImg();
}

public static bool SaveParametresGenerateImg(string url, string endpointV1, string endpointv2)
{
return _ReActAagent.SaveParametresGenerateImg(url, endpointV1, endpointv2);
}

public static FooocusRequest_Text_to_Image LoadParametresFooocus()
{
return _ReActAagent.LoadParametresFooocus();
}

public static bool SaveParametresFooocus(FooocusRequest_Text_to_Image param, List<string> ListeSelectedsStylesFooocus)
{
return _ReActAagent.SaveParametresFooocus(param, ListeSelectedsStylesFooocus);
}

public static void SaveConversation(ChatConversation conv)
{
LoggerService.LogInfo($"ChatService.Save");

Directory.CreateDirectory(Folder);
var path = Path.Combine(Folder, conv.Id + ".json");
File.WriteAllText(path, JsonSerializer.Serialize(conv, new JsonSerializerOptions { WriteIndented = true }));
}

public static void Delete(string conversationId)
{
LoggerService.LogInfo($"ChatService.Delete");
var path = Path.Combine(Folder, conversationId + ".json");
if (File.Exists(path)) File.Delete(path);
}
public static List<ChatConversation> LoadAll(string type)
{
LoggerService.LogInfo($"ChatService.LoadAll");

return Directory.GetFiles(Folder, "*.json")
.Select(file => JsonSerializer.Deserialize<ChatConversation>(File.ReadAllText(file)))
.Where(c => c != null)
.Where(c=>c.Type == type)
.Cast<ChatConversation>()
.ToList();
}

public static async Task<(bool, string, string)> SendMessageAsync(ModelsUseCases.TypeUseCase useCase, List<ChatMessage> messages, string model, bool isApiExterne=false)
{
LoggerService.LogInfo($"ChatService.SendMessageAsync");
var (s,m) = await _ReActAagent.AppelerLLMAsync(useCase, model, messages, isApiExterne);
return (true, s, m);
}
public static async Task<(bool, string, string)> SendMessageAsync(string message, string model, bool isWithAssistant, bool isGenerateParametres)
{
LoggerService.LogInfo($"ChatService.SendMessageAsync");
var (s,m) = await _ReActAagent.GenerateTextToImage(ModelsUseCases.TypeUseCase.PromptGenerationFooocus, message, model, isWithAssistant, isGenerateParametres);
return (true, s, m);
}

public static async Task<(bool, string, string)> SendMessageAsync(string message, string model, bool isWithAssistant, List<string> documentsFullFilename)
{
LoggerService.LogInfo($"ChatService.SendMessageAsync");
var (s,m) = await _ReActAagent.GenerateTextToImageWithIP(ModelsUseCases.TypeUseCase.PromptGenerationFooocus, message, model, isWithAssistant, documentsFullFilename);
return (true, s, m);
}

public static async Task<(bool, string, string)> SendMessageAsync(ModelsUseCases.TypeUseCase useCase, string prompt, string model, List<string> documentsFullFilename, bool isApiExterne = false)
{
if (useCase == TypeUseCase.LLM)
{
var (b1,s11,s12) = await SendMessageLLMAsync(prompt, model, documentsFullFilename, isApiExterne);
List<ChatMessage> messages = new();
prompt = prompt + "\n" + s11;
var userMessage = new ChatMessage { Role = "user", Content = prompt, Model = model };
messages.Add(userMessage);

var (b2, s21, s22) = await SendMessageAsync(ModelsUseCases.TypeUseCase.LLM, messages, model, isApiExterne);
return (b1 && b2, "Résumé\n------: \n" + s11 + "\n------\nFin du résumé\n\n" + s21, "Résumé : " + s12 + "\n" + "Traitement de la demande : " + s22);
}
else if (useCase == TypeUseCase.LLM_Coder)
{
return await SendMessageLLMCoderAsync(prompt, model, documentsFullFilename, isApiExterne);
}
else
{
return (false, "Erreur : Cas d'usage qui n'est ni LLM, ni LLM Coder", model);
}

}
#endregion

#region Méthodes privées

private static async Task<(bool, string, string)> SendMessageLLMAsync(string prompt, string model, List<string> documentsFullFilename, bool isApiExterne = false)
{
(List<string> lstImages, List<string> lstDocuments) = FilesService.GetListesFichiers(documentsFullFilename);

// Images mais pas de documents
if (lstImages.Count > 0 && lstDocuments.Count == 0)
{
LoggerService.LogInfo($"ChatService.SendMessageLLMAsync : {lstImages.Count} image(s) mais pas de documents");
var (s, m) = await _ReActAagent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.InterpretationImages, model, prompt, lstImages, isApiExterne);
return (true, s, m);
}
// Documents mais pas d'image
else if (lstImages.Count == 0 && lstDocuments.Count > 0)
{
LoggerService.LogInfo($"ChatService.SendMessageLLMAsync : {lstDocuments.Count} document(s) mais pas d'image");
var (resume, m) = await GetResume(lstDocuments);
return (true, resume.ToString(), m);
}
// Images + documents
else
{
LoggerService.LogInfo($"ChatService.SendMessageLLMAsync : {lstImages.Count} image(s) + {lstDocuments.Count} document(s)");
var resume = await GetResume(lstDocuments);
var (reponse, m) = await _ReActAagent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.InterpretationImages, model, prompt, lstImages, isApiExterne);
return (true, resume + "\n\n" + reponse, m);
}
}

private static async Task<(bool, string, string)> SendMessageLLMCoderAsync(string prompt, string model, List<string> documentsFullFilename, bool isApiExterne = false)
{
(List<string> lstImages, List<string> lstDocuments) = FilesService.GetListesFichiers(documentsFullFilename);

// Images non prises en compte dans ce contexte
if (lstImages.Count > 0 && lstDocuments.Count == 0)
{
LoggerService.LogDebug($"ChatService.SendMessageLLMCoderAsync : {lstImages.Count} image(s) mais pas de documents : pas pris en compte");
return (false, "Erreur : les images ne sont prises en compte dans ce contexte", model);
}
// Documents pris en compte
else if (lstDocuments.Count > 0)
{
LoggerService.LogInfo($"ChatService.SendMessageLLMCoderAsync : {lstDocuments.Count} document(s)");
var codeDocs = "";
foreach (var doc in lstDocuments)
{
var code = $"{Path.GetFileName(doc)} : \n" + TxtService.ExtractTextFromTxt(doc);
codeDocs += "\n" + code;
}
var (rep, m) = await _ReActAagent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.LLM_Coder, true, prompt + "\n\n" + codeDocs, "Analyse de code", model, isApiExterne);
return (true, rep, m);
}
// cas qui ne devrait pas arriver
else
{
LoggerService.LogInfo($"ChatService.SendMessageLLMCoderAsync : {lstImages.Count} image(s) + {lstDocuments.Count} document(s)");
return (true, "Cas qui ne devrait pas arriver", model);
}
}

private static async Task<(string, string)> GetResume(List<string> lstDocuments)
{
LoggerService.LogInfo($"ChatService.GetResume");
string resumes = "";
string model = "";
foreach (var doc in lstDocuments)
{
var filename = System.IO.Path.GetFileName(doc);
string content = FilesService.ExtractText(doc);
//agent.IngestDocument(content, doc);

var (resume, m) = await _ReActAagent.SummarizeIntegralLongDocAsync("ChatRoom", content);
resumes += "\n\n" + filename + " : \n" + resume;
model = m;
}
return (resumes, model);
}

public static async Task<List<OllamaModel>> GetInstalledModelsAsync()
{
var models = await _ReActAagent.GetInstalledModelsAsync(true);
return models;
}
#endregion
}
}

+ 52
- 0
DocumentsEntreprisesServices/DocumentsEntreprisesService.csproj Voir le fichier

@@ -0,0 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<None Remove="ServicesExternes\ffmpeg.zip" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\RAGService\RAGService.csproj" />
<ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" />
<ProjectReference Include="..\ToolsService\ToolsService.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="ServicesExternes\ffmpeg\avcodec-62.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ServicesExternes\ffmpeg\avdevice-62.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ServicesExternes\ffmpeg\avfilter-11.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ServicesExternes\ffmpeg\avformat-62.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ServicesExternes\ffmpeg\avutil-60.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ServicesExternes\ffmpeg\ffmpeg.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ServicesExternes\ffmpeg\ffplay.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ServicesExternes\ffmpeg\ffprobe.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ServicesExternes\ffmpeg\swresample-6.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ServicesExternes\ffmpeg\swscale-9.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>

+ 245
- 0
DocumentsEntreprisesServices/Services/FactureService.cs Voir le fichier

@@ -0,0 +1,245 @@
using Services.ReActAgent;
using System.Text;
using System.Text.RegularExpressions;
using ToolsServices;

namespace Services
{
public static class FactureService
{
#region Variables
private static readonly string NomFichierParametres = FichiersInternesService.ParamsFactures;
#endregion

#region Méthodes publiques
public static async Task<(bool, string)> Traitement(string inputPath, string outputPath, bool isApiExterne)
{
StringBuilder sb = new();
LoggerService.LogInfo($"FactureService.Traitement");

if (!Directory.Exists(inputPath))
{
LoggerService.LogWarning($"Le dossier {inputPath} n'existe pas.");
return (false, $"Le dossier {inputPath} n'existe pas.");
}

if (!Directory.Exists(outputPath))
{
LoggerService.LogWarning($"Le dossier {outputPath} n'existe pas.");
return (false, $"Le dossier {outputPath} n'existe pas.");
}

var fichiers = Directory
.EnumerateFiles(inputPath, "*.*", SearchOption.AllDirectories)
.Where(f => f.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
.ToArray();

if (fichiers.Length == 0)
{
LoggerService.LogWarning($"Aucun fichier trouvé dans le dossier spécifié : {inputPath}");
return (false, $"Aucun fichier trouvé dans le dossier spécifié : {inputPath}");
}

var reActRagAgent = new ReActAgent.ReActAgent();
var isOk = true;
foreach (var fichier in fichiers)
{

try
{
#region Analyse de la facture
var facturePDF = FilesService.ExtractText(fichier);
//facturePDF = CleanPdfText(facturePDF);
//facturePDF = SegmentInvoice(facturePDF);

var prompt = PromptService.GetPrompt(PromptService.ePrompt.FactureService_AnalyserFacture_Extract, facturePDF);

LoggerService.LogInfo($"Traitement de facture PDF to XML : {fichier}");
var (reponse,m) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.Facture_To_Xml, true, prompt, "Traitement de facture PDF to XML", "", isApiExterne);

if (!XmlService.IsXML(reponse))
{
sb.AppendLine($"Le résultat n'est pas au format XML pour le fichier : {fichier}");
LoggerService.LogWarning($"Le résultat n'est pas au format XML pour le fichier : {fichier}");

string fileNameXML = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + ".xml");
TxtService.CreateTextFile(fileNameXML, reponse);
}
else
{
#region Sauvegarde du résultat
LoggerService.LogDebug($"Le résultat est au format XML pour le fichier : {fichier}");
string fileNameXML = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + ".xml");
TxtService.CreateTextFile(fileNameXML, reponse);
#endregion
}
#endregion
}

catch (Exception ex)
{
isOk = false;
LoggerService.LogError($"Erreur lors de l'extraction du texte de {fichier} : {ex.Message}");
}
}

LoggerService.LogInfo($"Traitement des factures PDF to XML terminée");

return (isOk, sb.ToString());
}

public static (string, string) LoadParametres()
{
LoggerService.LogInfo("FactureService.LoadParametres");
//ParametresOllamaService SelectedItem = new();

try
{
string FicheMission = "";
string PathCV = "";
if (File.Exists(NomFichierParametres))
{

string[] lignes = File.ReadAllLines(NomFichierParametres);

if (lignes.Length > 0)
FicheMission = lignes[0];

if (lignes.Length > 1)
PathCV = lignes[1];

}
return (FicheMission, PathCV);

}
catch
{
LoggerService.LogError($"Erreur lors du chargement des paramètres depuis {NomFichierParametres}");
return ("", "");
}
}

public static bool SaveParametres(string inputPath, string outputPath)
{
LoggerService.LogInfo("FactureService.SaveParametres");
try
{
StringBuilder sb = new();
sb.AppendLine(inputPath);
sb.AppendLine(outputPath);

File.WriteAllText(NomFichierParametres, sb.ToString());

return true;
}
catch
{
LoggerService.LogError($"Erreur lors de la sauvegarde des paramètres dans {NomFichierParametres}");
return false;
}

}
#endregion

#region Méthodes privées
private static string CleanPdfText(string rawText)
{
var text = rawText;

// Normaliser les fins de ligne
text = text.Replace("\r", "\n");

// Supprimer les caractères de contrôle (sauf \n)
text = new string(text.Where(c => !char.IsControl(c) || c == '\n').ToArray());

// Supprimer les en-têtes et pieds de page typiques (ex: "Page 1/3", "Facture ACME", etc.)
text = Regex.Replace(text, @"Page\s+\d+(/\d+)?", "", RegexOptions.IgnoreCase);
text = Regex.Replace(text, @"Facture\s+n[°º]\s*\d+", "", RegexOptions.IgnoreCase);

// Uniformiser les espaces : tabulations → espace, et espaces multiples → 1 seul
text = Regex.Replace(text, @"[ \t]+", " ");

// Supprimer les lignes vides excessives
text = Regex.Replace(text, @"\n{2,}", "\n");

// Supprimer les traits de séparation (souvent des "-----" ou "=====")
text = Regex.Replace(text, @"[-=]{3,}", "");

// Corriger les montants : ex "1 234,56 €" → "1234.56 EUR"
text = Regex.Replace(text, @"(\d{1,3}(?:[ \u00A0]\d{3})*,\d{2}) ?€",
m => m.Groups[1].Value.Replace(" ", "").Replace("\u00A0", "").Replace(",", ".") + " EUR");

// Corriger les dates : ex "01-02-2025" → "2025-02-01"
text = Regex.Replace(text, @"\b(\d{2})[-/](\d{2})[-/](\d{4})\b", "$3-$2-$1");

// Fusionner les lignes coupées artificiellement (ex: "TOTAL\n123,45" → "TOTAL 123,45")
text = Regex.Replace(text, @"([A-Za-z])\n(\d)", "$1 $2");

// Supprimer les espaces en trop au début et fin
text = text.Trim();

return text;
}

private static string SegmentInvoice(string text)
{
var sb = new StringBuilder();

// Bloc fournisseur
var fournisseurMatch = Regex.Match(text, @"(?i)(?:fournisseur|vendor|seller).{0,50}\n(.+?)(\n|$)");
if (fournisseurMatch.Success)
{
sb.AppendLine("=== FOURNISSEUR ===");
sb.AppendLine(fournisseurMatch.Groups[1].Value.Trim());
}

// Bloc client
var clientMatch = Regex.Match(text, @"(?i)(?:client|acheteur|buyer|bill to|destinataire).{0,50}\n(.+?)(\n|$)");
if (clientMatch.Success)
{
sb.AppendLine("\n=== CLIENT ===");
sb.AppendLine(clientMatch.Groups[1].Value.Trim());
}

// Numéro de facture
var numeroMatch = Regex.Match(text, @"(?i)facture[^\d]*(\d+)");
if (numeroMatch.Success)
{
sb.AppendLine("\n=== NUMERO FACTURE ===");
sb.AppendLine(numeroMatch.Groups[1].Value.Trim());
}

// Date de facture
var dateMatch = Regex.Match(text, @"(?i)(?:date|issued|emission)[^\d]*(\d{4}-\d{2}-\d{2})");
if (dateMatch.Success)
{
sb.AppendLine("\n=== DATE FACTURE ===");
sb.AppendLine(dateMatch.Groups[1].Value.Trim());
}

// Bloc lignes de facture (simplifié : cherche un tableau prix/quantité)
var lignesMatch = Regex.Match(text, @"(?is)(description.*?total)", RegexOptions.IgnoreCase);
if (lignesMatch.Success)
{
sb.AppendLine("\n=== LIGNES FACTURE ===");
sb.AppendLine(lignesMatch.Groups[0].Value.Trim());
}

// Total général
var totalMatch = Regex.Match(text, @"(?i)(total (?:ttc|general|amount)).{0,10}([\d.,]+ ?eur)");
if (totalMatch.Success)
{
sb.AppendLine("\n=== TOTAL GENERAL ===");
sb.AppendLine(totalMatch.Groups[2].Value.Trim());
}

// Retour du texte segmenté
return sb.Length > 0 ? sb.ToString() : text;
}
#endregion

}



}

+ 157
- 0
DocumentsEntreprisesServices/Services/TF_From_DSFService.cs Voir le fichier

@@ -0,0 +1,157 @@
using Services.ReActAgent;
using System.Text;
using ToolsServices;

namespace Services
{
public class TF_From_DSFService
{
private static readonly string NomFichierParametres = FichiersInternesService.ParamsTF_From_DSF;

public static async Task<(bool, string)> Traitement(string inputPath, string outputPath, bool isApiExterne)
{
StringBuilder sb = new();
LoggerService.LogInfo($"TF_From_DSFService.Traitement");

if (!Directory.Exists(inputPath))
{
LoggerService.LogWarning($"Le dossier {inputPath} n'existe pas.");
return (false, $"Le dossier {inputPath} n'existe pas.");
}

if (!Directory.Exists(outputPath))
{
LoggerService.LogWarning($"Le dossier {outputPath} n'existe pas.");
return (false, $"Le dossier {outputPath} n'existe pas.");
}

var fichiers = Directory
.EnumerateFiles(inputPath, "*.*", SearchOption.AllDirectories)
.Where(f => f.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
.ToArray();

if (fichiers.Length == 0)
{
LoggerService.LogWarning($"Aucun fichier trouvé dans le dossier spécifié : {inputPath}");
return (false, $"Aucun fichier trouvé dans le dossier spécifié : {inputPath}");
}

var reActRagAgent = new ReActAgent.ReActAgent();
var model = ReActAgent.ReActAgent.GetModeleIA(ModelsUseCases.TypeUseCase.TF_From_DSF);

foreach (var fichier in fichiers)
{
try
{
#region Analyse du DSF
var allCsv = new StringBuilder();

var docDSF = FilesService.ExtractText(fichier);
var chunks = RAGService.ChunkText(docDSF, 2000);// Chunker.ChunkText(docDSF, 2000); //SplitText(docDSF, 3000);
var nbChuncks = chunks.Length;
int numChuck = 0;

foreach (var chunk in chunks)
{
numChuck++;
var prompt = PromptService.GetPrompt(PromptService.ePrompt.TF_From_DSFService_GenereTF, chunk);

LoggerService.LogDebug($"Traitement TF From DSF {numChuck}/{nbChuncks} : {fichier}");
var (reponse,m) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.TF_From_DSF, true, prompt, "Traitement de TF From DSF", model, isApiExterne);
allCsv.AppendLine(reponse);

}


#region Sauvegarde du résultat
/*
string horodate = DateTime.Now.ToString("hhmmss");
string fileNameCSV = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + $"_{model.Replace(":","-")}_{horodate}.csv");
*/
string fileNameCSV = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + $".csv");
TxtService.CreateTextFile(fileNameCSV, allCsv.ToString());
#endregion

#endregion
}

catch (Exception ex)
{
var msg = $"Erreur lors de l'extraction du texte de {fichier} : {ex.Message}";
LoggerService.LogError(msg);
return (false, msg);
}
}
//}

LoggerService.LogInfo($"Traitement TF From DSF terminée");

return (true, sb.ToString());
}

private static List<string> SplitText(string text, int chunkSize = 3000)
{
var chunks = new List<string>();
for (int i = 0; i < text.Length; i += chunkSize)
{
int length = Math.Min(chunkSize, text.Length - i);
chunks.Add(text.Substring(i, length));
}
return chunks;
}

public static (string, string) LoadParametres()
{
LoggerService.LogInfo("TF_From_DSFService.LoadParametres");
//ParametresOllamaService SelectedItem = new();

try
{
string FicheMission = "";
string PathCV = "";
if (File.Exists(NomFichierParametres))
{

string[] lignes = File.ReadAllLines(NomFichierParametres);

if (lignes.Length > 0)
FicheMission = lignes[0];

if (lignes.Length > 1)
PathCV = lignes[1];

}
return (FicheMission, PathCV);

}
catch
{
LoggerService.LogError($"Erreur lors du chargement des paramètres depuis {NomFichierParametres}");
return ("", "");
}
}

public static bool SaveParametres(string inputPath, string outputPath)
{
LoggerService.LogInfo("TF_From_DSFService.SaveParametres");
try
{
StringBuilder sb = new();
sb.AppendLine(inputPath);
sb.AppendLine(outputPath);

File.WriteAllText(NomFichierParametres, sb.ToString());

return true;
}
catch
{
LoggerService.LogError($"Erreur lors de la sauvegarde des paramètres dans {NomFichierParametres}");
return false;
}

}


}
}

+ 127
- 0
DocumentsEntreprisesServices/Services/TranscriptionAndResumeService.cs Voir le fichier

@@ -0,0 +1,127 @@
using Services.ReActAgent;
using System.Diagnostics;
using ToolsServices;

namespace Services
{
public static class TranscriptionAndResumeService
{
public static async Task<(bool, string)> TranscribeAudio(string fullFileName)
{
try
{
LoggerService.LogInfo($"TranscriptionAndResumeService.TranscribeAudio");
var bActif = await ReActAgent.ReActAgent.IsWhisperActif();
if (!bActif)
{
var msg = "Le service de transcription Whisper n'est pas actif. Veuillez le démarrer et réessayer.";
LoggerService.LogWarning(msg);
return (false, msg);
}
var _Agent = new ReActAgent.ReActAgent();
var (b, s) = await _Agent.TranscribeAudio(fullFileName);
return (b, s);
}
catch (Exception ex)
{
return (false, ex.Message);
}
}

public static async Task<(bool, string)> ResumeTranscribe(string transcribeText, string contexte)
{
try
{
LoggerService.LogInfo($"TranscriptionAndResumeService.ResumeTranscribe");

var bActif = await ReActAgent.ReActAgent.IsOllamaActif(true);
if (!bActif)
{
var msg = "Le service de résumé Ollama n'est pas actif. Veuillez le démarrer et réessayer.";
LoggerService.LogWarning(msg);
return (false, msg);
}

var _Agent = new ReActAgent.ReActAgent();
/*
var prompt = $"""
Tu es un expert en résumé de texte.
Résume le texte suivant en français de manière claire et concise, en utilisant des phrases complètes.
Il s'agit d'une réunion de travail pour faire le bilan du sprint 03 du projet GIEBOX
Texte à résumer :
{transcribeText}
""";
*/
var prompt = PromptService.GetPrompt(PromptService.ePrompt.TranscriptionAndResumeService_Resume, transcribeText, contexte);

var (s, m) = await _Agent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.ResumeDocuments, true, prompt, "Résumé d'une transcription", "", false);

return (true, s);
}
catch (Exception ex)
{
return (false, ex.Message);
}
}

public static bool ConvertVideoToAudio(string inputFile, string outputFile)
{
LoggerService.LogInfo($"TranscriptionAndResumeService.ConvertVideoToAudio");

try
{
if (System.IO.File.Exists(outputFile))
{
try
{
System.IO.File.Delete(outputFile);
}
catch (Exception ex)
{
LoggerService.LogError($"TranscriptionAndResumeService.ConvertVideoToAudio : {ex.Message}");
return false;
}
}

// Mono en 16KHz
//var ffmpegArgs = $"-i \"{inputFile}\" -ac 1 -ar 16000 \"{outputFile}\"";

// Stereo en 44.1KHz
var ffmpegArgs = $"-i \"{inputFile}\" -ac 2 -ar 44000 -c:a pcm_s24le \"{outputFile}\"";
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ServicesExternes", "ffmpeg", "ffmpeg.exe");
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = path, // ffmpeg.exe n'a pas besoin d'être le PATH (variables environnement)
Arguments = ffmpegArgs,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};

process.Start();

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();
if (process.ExitCode != 0)
{
LoggerService.LogError($"TranscriptionAndResumeService.ConvertVideoToAudio : {error}");
throw new Exception($"Erreur ffmpeg : {error}");

}
return true;
}
catch (Exception ex)
{
LoggerService.LogError($"TranscriptionAndResumeService.ConvertVideoToAudio : {ex.Message}");
return false;
}
}
}
}

+ 1
- 0
DocumentsEntreprisesServices/ServicesExternes/ffmpeg/_A_LIRE.txt Voir le fichier

@@ -0,0 +1 @@
Mettre ici les exe et dll de ffmpeg

+ 21
- 0
EmailService/EmailService.csproj Voir le fichier

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MailKit" Version="4.14.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\EmailStructure\EmailStructure.csproj" />
<ProjectReference Include="..\RAGService\RAGService.csproj" />
<ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" />
<ProjectReference Include="..\ToolsService\ToolsService.csproj" />
</ItemGroup>

</Project>

+ 544
- 0
EmailService/Services/EmailSendService.cs Voir le fichier

@@ -0,0 +1,544 @@
using ClosedXML;
using Google.Protobuf.WellKnownTypes;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Search;
using MailKit.Security;
using MimeKit;
using Org.BouncyCastle.Security;
using Services.ReActAgent;
using System.Text.Json;
using ToolsServices;

namespace Services
{
#region Classe static annexe JsonDb
public static class JsonDb
{
static string FullFileName = FichiersInternesService.ListeMailsSend;

public static ThreadStore Load()
{
if (!File.Exists(FullFileName)) return new ThreadStore();
var json = File.ReadAllText(FullFileName);
return JsonSerializer.Deserialize<ThreadStore>(json) ?? new ThreadStore();
}

public static bool Save(ThreadItem thread)
{
ThreadStore data;

// Charger fichier existant
if (File.Exists(FullFileName))
{
string json = File.ReadAllText(FullFileName);
data = JsonSerializer.Deserialize<ThreadStore>(json, new JsonSerializerOptions { WriteIndented = true }) ?? new ThreadStore();
}
else
{
data = new ThreadStore();
}

// Chercher si le thread existe déjà
var existing = data.Threads.FirstOrDefault(t => t.Id == thread.Id);
if (existing != null)
{
int index = data.Threads.IndexOf(existing);
data.Threads[index] = thread; // remplacement
}
else
{
data.Threads.Add(thread); // ajout
}

// Sauvegarder
string updatedJson = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(FullFileName, updatedJson);
return true;
}

public static void Save(ThreadStore store)
{
Directory.CreateDirectory(Path.GetDirectoryName(FullFileName)!);
var json = JsonSerializer.Serialize(store, new JsonSerializerOptions { WriteIndented = true });
var tmp = FullFileName + ".tmp";
File.WriteAllText(tmp, json);
File.Move(tmp, FullFileName, true);
}
}
#endregion

#region Classe static EmailSendService
public static class EmailSendService
{
#region Variables
private static ReActAgent.ReActAgent _ReActAgent = new();
private static List<IMailFolder> ListInboxFolders = new List<IMailFolder>();
private static CompteMailUser ParametreUser = new();
#endregion

#region Méthodes publiques
public static int NbRelancesEnAttente()
{
var lstReponsesEnAttente = LstRelancesEnAttente();
return lstReponsesEnAttente.Count;
}

public static ThreadStore ChargerDonnees()
{
var fileMailsSend = FichiersInternesService.ListeMailsSend;
ThreadStore store = new ThreadStore();
if (File.Exists(fileMailsSend))
{
var json = System.IO.File.ReadAllText(fileMailsSend);
store = JsonSerializer.Deserialize<ThreadStore>(json) ?? new ThreadStore();
}
return store;
}

public static List<ThreadItem> LstRelancesEnAttente()
{
var store = JsonDb.Load();
var lstReponsesEnAttente = store.Threads
.Where(t => !t.SubjectClosed) // Sujet non fermé
.Where(t => t.IsOverdue) // En retard
.ToList();
return lstReponsesEnAttente;
}

private class MessageDossier
{
public UniqueId uid { get; set; }
public MimeMessage mimeMessage { get; set; } =new();
public IMailFolder? dossier { get; set; }
public string dossierFullname { get; set; } = "";
public string direction { get; set; } = "";
}
public static async Task<(bool, string, int)> RunOnce(CompteMailUser parametreUser)
{
var nbMsgSentSaved = 0;
var nbMsgInboxSaved = 0;
var nbRelancesCreated = 0;
var nbConvReOuvertes = 0;

ParametreUser = parametreUser;
try
{
LoggerService.LogInfo("EmailSendService.RunOnce");
var dateFilter = DateTime.UtcNow.AddDays(-1 * ParametreUser.DelaySentRecup);

var parametresMail = EmailService.LoadParametres();
if(parametresMail == null)
{
return (false, "Paramètres email introuvable",0);
}
var store = JsonDb.Load();

using var client = new ImapClient();
await client.ConnectAsync(parametresMail.ServeurImap, parametresMail.ServeurImapPort, SecureSocketOptions.SslOnConnect);
await client.AuthenticateAsync(parametresMail.UserAdresse, parametresMail.UserMotPasse);

ListInboxFolders.Clear();
var personal = client.GetFolder(client.PersonalNamespaces[0]);
await ProcessFolder(personal);

// Récupérer le dossier "Sent"
var sentFolder = await personal.GetSubfolderAsync("Sent");

// Si ça ne marche pas (par exemple dossier en français "Envoyés") :
if (sentFolder == null || !sentFolder.Exists)
{
var allFolders = await personal.GetSubfoldersAsync(false);
sentFolder = allFolders.FirstOrDefault(f => f.Name.Equals("Sent", StringComparison.OrdinalIgnoreCase)
|| f.Name.Equals("Envoyés", StringComparison.OrdinalIgnoreCase));
}

// Si on a bien trouvé un dossier "Sent" ou équivalent
if (sentFolder == null)
{
return (false, "Dossier mails envoyés introuvables",0);
}
var sent = sentFolder;



await sent.OpenAsync(MailKit.FolderAccess.ReadOnly);
//var uidsSent = await sent.SearchAsync(SearchQuery.All);



var uidsSent = await sent.SearchAsync(SearchQuery.DeliveredAfter(dateFilter));

List<MessageDossier> lst = new();
foreach (var uid in uidsSent)
{
var msg = await sent.GetMessageAsync(uid);
MessageDossier nouveau = new();
nouveau.uid = uid;
nouveau.mimeMessage = msg;
nouveau.dossier = sent;
nouveau.dossierFullname = sentFolder.FullName;
nouveau.direction = "out";
lst.Add(nouveau);
}

foreach (var oneFolder in ListInboxFolders)
{
var inbox = oneFolder!;// client.Inbox;
await inbox.OpenAsync(MailKit.FolderAccess.ReadWrite);
//var uidsInbox = await inbox.SearchAsync(SearchQuery.All);
var uidsInbox = await inbox.SearchAsync(SearchQuery.DeliveredAfter(dateFilter));
foreach (var uid in uidsInbox)
{
var msg = await inbox.GetMessageAsync(uid);
MessageDossier nouveau = new();
nouveau.uid = uid;
nouveau.mimeMessage = msg;
nouveau.dossier = inbox;
nouveau.dossierFullname = inbox.FullName;
nouveau.direction = "in";

lst.Add(nouveau);
}
}
lst = lst.OrderBy(e => e.mimeMessage.Date).ToList();
foreach (var message in lst)
{
var bCreateMsg = await AddMessage(message.uid, store, message.mimeMessage, message.direction,message.dossierFullname, parametreUser.OverdueDaysSent);
if (bCreateMsg)
nbMsgSentSaved++;
JsonDb.Save(store);
}



/*
foreach (var uid in uidsSent)
{
var msg = await sent.GetMessageAsync(uid);

var bCreateMsg = await AddMessage(uid, store, msg, "out", sentFolder.FullName, parametreUser.OverdueDaysSent);
if (bCreateMsg)
nbMsgSentSaved++;
JsonDb.Save(store);
}

// Tous les dossiers Inbox

foreach (var oneFolder in ListInboxFolders)
{
var inbox = oneFolder!;// client.Inbox;
await inbox.OpenAsync(MailKit.FolderAccess.ReadWrite);
//var uidsInbox = await inbox.SearchAsync(SearchQuery.All);
var uidsInbox = await inbox.SearchAsync(SearchQuery.DeliveredAfter(dateFilter));
foreach (var uid in uidsInbox)
{
var msg = await inbox.GetMessageAsync(uid);

var bCreateMsg = await AddMessage(uid, store, msg, "in", oneFolder.FullName, parametreUser.OverdueDaysSent);
if (bCreateMsg)
nbMsgInboxSaved++;
JsonDb.Save(store);
}
}
*/

await client.DisconnectAsync(true);

// Analyse IA pour déterminer les relances

foreach (var thread in store.Threads)
{
// On prend le dernier mail sortant
var lastOut = thread.Messages
.FindLast(m => m.Direction == "out");

var theLast = thread.Messages.FindLast(m => m.Direction == "out" || m.Direction == "in");
thread.DateLastMessage = theLast!.Date;

if (lastOut != null && lastOut.RequiresResponse)
{
// On ne régénère pas une relance si on l'a déjà faite pour ce mail
bool alreadyFollowedUp = (thread.LastFollowUpForMessageId == lastOut.Id);
if (!alreadyFollowedUp)
{
// Dernier mail entrant
var lastIn = thread.Messages
.Where(m => m.Direction == "in")
.OrderBy(m => m.Date.ToUniversalTime())
.LastOrDefault();

// relance si
bool needsFollowUp = (
lastIn == null // pas de réponse
|| (lastIn.Date.ToUniversalTime() < lastOut.Date.ToUniversalTime()) // pas de réponse APRES le dernier message
|| (lastIn.Date.ToUniversalTime() >= lastOut.Date.ToUniversalTime() && !lastIn.CoversAllPoints) // réponse APRES le dernier message MAIS tous les points ne sont pas couverts
);

if (needsFollowUp)
{
//if(thread.FollowUpDraft == null || thread.FollowUpDraft == "")
thread.FollowUpDraft = await GenerateFollowUp(thread);
// Même si cloturée, on ré-ouvre la conversation
if(thread.SubjectClosed == true)
{
nbConvReOuvertes++;
thread.SubjectClosed = false;
}

thread.LastFollowUpForMessageId = lastOut.Id;
nbRelancesCreated++;
}
}
}
JsonDb.Save(store);
}

JsonDb.Save(store);
LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages envoyés sauvegardés : {nbMsgSentSaved}");
LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages reçus sauvegardés : {nbMsgInboxSaved}");
LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre relances générées : {nbRelancesCreated}");
LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre coversation réouvertes : {nbConvReOuvertes}");
LoggerService.LogDebug("EmailSendService.RunOnce : terminé");
return (true, "", nbConvReOuvertes);
}
catch(Exception ex)
{
LoggerService.LogError($"EmailSendService.RunOnce : {ex.Message}");
LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages envoyés sauvegardés : {nbMsgSentSaved}");
LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages reçus sauvegardés : {nbMsgInboxSaved}");
LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre relances générées : {nbRelancesCreated}");
LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre coversation réouvertes : {nbConvReOuvertes}");

return (false, $"EmailSendService.RunOnce : {ex.Message}", nbConvReOuvertes);
}
}

public static bool Save(ThreadItem thread)
{
LoggerService.LogInfo("EmailSendService.Save");
return JsonDb.Save(thread);
}

public async static Task<bool> ArchiverRestaurerItem(ThreadItemMini thread)
{
await Task.Delay(1);
var store = ChargerDonnees();
var tts = store.Threads;
ThreadItem? t = tts!.Where(e => e.Id == thread.Id)!.FirstOrDefault();
if (t == null)
{
return false;
}
t.SubjectClosed = !t.SubjectClosed;
Save(t);
return true;
}

public async static Task<bool> SaveSendRelanceItem(ThreadItemMini thread)
{
await Task.Delay(1);
var store = ChargerDonnees();
var tts = store.Threads;
ThreadItem? t = tts!.Where(e=>e.Id == thread.Id)!.FirstOrDefault();
if (t == null)
{
return false;
}
t.FollowUpDraft = thread.FollowUpDraft;
Save(t);
return true;
}
public async static Task<bool> SaveAsync(ThreadItem thread)
{
await Task.Delay(1);
LoggerService.LogInfo("EmailSendService.Save");
return JsonDb.Save(thread);
}

public static void AllConversationsChangeStatus(bool isClosed)
{
var store = JsonDb.Load();
foreach(var conversation in store.Threads)
{
conversation.SubjectClosed = isClosed;
}
JsonDb.Save(store);

}
#endregion

#region Méthodes privées
private static async Task ProcessFolder(IMailFolder folder)
{
var excludedFolders = new[] { "Junk", "spam", "Sent", "Trash", "Drafts" };
if (Array.Exists(excludedFolders, f => string.Equals(f, folder.Name, StringComparison.OrdinalIgnoreCase)))
return;


if (folder.Attributes.HasFlag(FolderAttributes.NoSelect))
{
LoggerService.LogDebug($"📂 {folder.FullName} (conteneur uniquement, pas de mails)");
}
else
{
ListInboxFolders.Add(folder);
LoggerService.LogDebug($"📂 {folder.FullName}");
}

// Parcours récursif des sous-dossiers
foreach (var sub in folder.GetSubfolders())
{
await ProcessFolder(sub);
}
}

private static async Task<bool> AddMessage(UniqueId uid, ThreadStore store, MimeMessage msg, string direction, string folder, int overdueDays)
{
//var folder = (direction == "out" ? "Sent" : "Inbox");
var subject = msg.Subject ?? "(sans objet)";
var body = msg.TextBody ?? msg.HtmlBody ?? "";
var messageId = msg.MessageId ?? Guid.NewGuid().ToString();
var references = msg.References?.ToArray() ?? Array.Empty<string>();
var inReplyTo = msg.InReplyTo;

// Trouve un thread existant basé sur Reply-To
var thread = (ThreadItem?)null;

if (messageId != null)
{
// On cherche le message par son ID (pour ne pas créer un doublon)
thread = store.Threads.Find(t =>
t.Messages.Exists(m => m.Id == messageId)
);
}
if (thread == null && inReplyTo != null)
{
// On cherche si une reponse existe déjà pour mettre ce message dans le même thread
thread = store.Threads.Find(t =>
t.Messages.Exists(m => m.Id == inReplyTo)
);
}
if (thread == null)
{
thread = store.Threads.Find(t =>
t.Messages.Exists(m => m.InReplyTo == messageId)
);
}
// Vérifie si déjà importé via UID
/*
if (thread != null && thread.Messages.Exists(m => m.Folder == folder && m.Uid == uid))
return false;
*/
if (thread != null)
{
// Si le message existe déjà dans le thread, il a peut être été déplacé de dossier
if (thread.Messages.Exists(m => m.Id == msg.MessageId))
{
if (thread.Messages.Exists(m => m.Folder == folder && m.Id == msg.MessageId))
{
// existe et n'a pas été déplacé : on arrête
return false;
}
else
{
// a été déplacé --> mettre à jour le folder et uid
var msgExist = thread.Messages.Find(m => m.Id == msg.MessageId);
if (msgExist != null)
{
msgExist.Folder = folder;
//msgExist.Uid = uid;
return true;
}
else
{
// cas impossible ou msgExist est null
return false;
}
}
}
}

if (thread == null)
{
thread = new ThreadItem(overdueDays) { Subject = subject, DateMessage = msg.Date };
store.Threads.Add(thread);
}

var analysis = await AnalyzeMessage(body, direction);

thread.Messages.Add(new MailMessageItem
{
Id = msg.MessageId!,
InReplyTo = msg.InReplyTo,
//UidString = uid.ToString(),
Folder = folder,
Subject = subject,
From = msg.From.Mailboxes.FirstOrDefault()?.Address ?? "",
To = string.Join("; ", msg.To.Mailboxes.Select(m => m.Address)),
Direction = direction,
Date = msg.Date,
Body = body,
RequiresResponse = analysis.requiresResponse,
CoversAllPoints = analysis.coversAllPoints
});
return true;
}

private static async Task<(bool requiresResponse, bool coversAllPoints)> AnalyzeMessage(string body, string direction)
{
try
{
var prompt = "";

if (direction == "out")
{
//prompt = $"Réponds uniquement par oui ou non. Une réponse est-elle nécessaire à ce mail\nDébut du mail:\n{body} \n- fin du mail";
prompt = PromptService.GetPrompt(PromptService.ePrompt.EmailSendService_AnalyserMail_BesoinReponse, body);
}
else
{
//prompt = $"Réponds uniquement par oui ou non. Tous les points de ce mail ont-ils été traités\nDébut du mail:\n{body} \n- fin du mail";
prompt = PromptService.GetPrompt(PromptService.ePrompt.EmailSendService_AnalyserMail_AllPointsChecked, body);
}

var (reponseOllama, m) = await _ReActAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseMails, false, prompt, "Analyse mails sortants");

if(reponseOllama == null || reponseOllama.ToString().Trim().Length ==0)
{
return (true, true);
}
if (direction == "out")
{
var rep = (reponseOllama.ToString().ToLower().Contains("oui"));
return (rep, false);
}
else
{
var rep = (reponseOllama.ToString().ToLower().Contains("oui"));
return (false, rep);
}
}
catch
{
return (true, true);
}
}

private static async Task<string> GenerateFollowUp(ThreadItem thread)
{
var conversation = string.Join("\n\n", thread.Messages.ConvertAll(m => $"[{m.Direction}] {m.Body}"));
//var prompt = $"Génère un brouillon de mail en langue française de relance poli basé sur cette conversation :\n{conversation}";
var prompt = PromptService.GetPrompt(PromptService.ePrompt.EmailSendService_AnalyserMail_Relance, thread.Subject, conversation, ParametreUser.UserNomPrenom, ParametreUser.UserRole, ParametreUser.UserEntreprise);

var (reponseOllama, _) = await _ReActAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseMails, false, prompt, "Génération mail de relance");
return reponseOllama;
}
#endregion
}
#endregion
}

+ 1067
- 0
EmailService/Services/EmailService.cs
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 36
- 0
EmailService/Services/TaskService.cs Voir le fichier

@@ -0,0 +1,36 @@
using System.Text;

namespace EmailService.Services
{
public static class TaskService
{
public static void CreateThunderbirdTask(string subject, DateTime dtStart, int DureeMinutes)
{
var dtEnd = dtStart.AddMinutes(DureeMinutes);

string icsContent = $@"
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//MonApplication//Évènement Thunderbird//FR
BEGIN:VEVENT
SUMMARY:{subject}
DTSTART:{dtStart:yyyyMMddTHHmmssZ}
DTEND:{dtEnd:yyyyMMddTHHmmssZ}
PRIORITY:5
STATUS:CONFIRMED
END:VEVENT
END:VCALENDAR
";

string filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.ics");
File.WriteAllText(filePath, icsContent, Encoding.UTF8);

// Ouvre le fichier pour import dans Thunderbird
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = filePath,
UseShellExecute = true
});
}
}
}

+ 24
- 0
EmailStructure/CompteMailUser.cs Voir le fichier

@@ -0,0 +1,24 @@
namespace Services
{
public class CompteMailUser
{
public string UserAdresse { get; set; } = "prenom.nom@gmail.com";
public string UserMotPasse { get; set; } = "motdepasse!!!";
public string ServeurImap { get; set; } = "imap.gmail.com";
public int ServeurImapPort { get; set; } = 993;
public string ServeurSmtp { get; set; } = "smtp.gmail.com";
public int ServeurSmtpPort { get; set; } = 587;
public bool IsOk { get; set; } = false;

public string UserNomPrenom { get; set; } = "Votre nom et votre prénom";
public string UserRole { get; set; } = "Votre rôle";
public string UserEntreprise { get; set; } = "Votre entreprise";

public int DelayRefresh { get; set; } = 5; // en minutes, par défaut 5 minutes

public int DelaySentRefresh { get; set; } = 5; // en minutes, par défaut 5 minutes
public int DelaySentRecup { get; set; } = 30; // en jours, par défaut 30 jours
public int OverdueDaysSent { get; set; } = 7; // en jours, par défaut 7 jours

}
}

+ 123
- 0
EmailStructure/EmailSendStructure.cs Voir le fichier

@@ -0,0 +1,123 @@
using MailKit;
using System.Text.Json.Serialization;

namespace Services
{
public class ThreadItemMini
{
public string Id { get; set; } = "";
public string? FollowUpDraft { get; set; }
}
public class ThreadStore
{
public List<ThreadItem> Threads { get; set; } = new();
}

public class ThreadItem
{
#region Constructeurs
//Necessaire pour la déserialisation en json
public ThreadItem()
{
OverdueDays = 7;
}

//Necessaire pour passer le paramètre lors de la création d'objets
public ThreadItem(int overdueDays=7)
{
OverdueDays = overdueDays;
}
#endregion

#region Méthode privée
private bool IsOverDueThread()
{
var t = this;
if (t.LastFollowUpForMessageId == null || t.FollowUpDraft == null)
{
return false;
}
var m = t.Messages.Where(m => m.Id == LastFollowUpForMessageId).FirstOrDefault();
if (m == null)
{
return false;
}
if (m.Date < DateTimeOffset.UtcNow.AddDays(-1 * OverdueDays))
return true;
else
return false;
}
#endregion

#region Variable
[JsonIgnore]
public int OverdueDays { get; set; } = 7;
#endregion

#region Propriétés
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Subject { get; set; } = "";
public List<MailMessageItem> Messages { get; set; } = new();
public string? FollowUpDraft { get; set; }
public string? LastFollowUpForMessageId { get; set; }
public bool SubjectClosed { get; set; } = false;
public DateTimeOffset DateMessage { get; set; }
public DateTimeOffset DateLastMessage { get; set; }
[JsonIgnore]
public string DateMessageSTR { get => DateMessage.ToString("dd/MM/yyyy HH:mm"); }
[JsonIgnore]
public string DateLastMessageSTR { get => DateLastMessage.ToString("dd/MM/yyyy HH:mm"); }
[JsonIgnore]
public IEnumerable<MailMessageItem> MessagesSorted => Messages.OrderBy(m => m.Date.UtcDateTime);
[JsonIgnore]
public bool IsOverdue
{
get
{
return IsOverDueThread();
}
}
#endregion
}

public class MailMessageItem
{
public string Id { get; set; } = "";
public string InReplyTo { get; set; } = "";
/*[JsonIgnore]
public string UidString
{
get
{
return Uid.ToString();
}
set
{
if (value != "0")
Uid = UniqueId.Parse(value);
}
}*/
public string Folder { get; set; } = "";
public string Subject { get; set; } = "";
public string From { get; set; } = "";
public string To { get; set; } = "";
public string Direction { get; set; } = "out"; // "in" ou "out"
public DateTimeOffset Date { get; set; }
public string Body { get; set; } = "";
public bool RequiresResponse { get; set; }
public bool CoversAllPoints { get; set; }
//[JsonIgnore]
//public UniqueId Uid { get; set; }
[JsonIgnore]
public string DateSTR { get => $"Le : {Date.DateTime.ToString("dd/MM/yyyy HH:mm")}"; }
[JsonIgnore]
public string FromSTR { get => $"De : {From}"; }
[JsonIgnore]
public string ToSTR { get => $"Pour : {To}"; }
[JsonIgnore]
public string FolderSTR { get => $"Dossier : {Folder}"; }
[JsonIgnore]
public string SubjectSTR { get => $"Sujet : {Subject}"; }
}

}

+ 179
- 0
EmailStructure/EmailStructure.cs Voir le fichier

@@ -0,0 +1,179 @@
using MailKit;
using System.Text.Json.Serialization;
using System.Xml;

namespace Services
{
public class EmailMessage
{
[JsonIgnore]
public MailKit.UniqueId? Uid { get; set; } // Pour charger le corps plus tard

[JsonPropertyName("UidString")]
public string? UidString
{
get => Uid != null ? Uid?.ToString() : null;
set
{
if (!string.IsNullOrWhiteSpace(value))
{
if (MailKit.UniqueId.TryParse(value, out var parsed))
Uid = parsed;
else
Uid = null; // ou log une erreur
}
else
{
Uid = null;
}
}
}
/*
public string UidString
{
get => Uid.ToString();
set => Uid = UniqueId.Parse(value);
}
*/
public string Id { get; set; } = "";
public string To { get; set; } = "";
public string Cc { get; set; } = "";
public bool IsDestinatairePrincipal { get; set; }=false;
public string From { get; set; } = "";
public string FromName { get; set; } = "";
public string Subject { get; set; } = "";
public string InReplyTo { get; set; } = "";
public DateTime Date { get; set; }
public string DateSTR { get => Date.ToString("dd/MM/yyyy HH:mm"); }

public string Analyse { get; set; } = "";

public string Preview { get; set; } = "";
public string TextBody { get; set; } = "";
public string TextBodyHTML { get; set; } = "";

[JsonIgnore]
public string TextBody_PJ
{
get
{
if (ContentPJ == null || ContentPJ.Count == 0)
return TextBody;

var joined = string.Join("\n---\n", ContentPJ);
return $"{TextBody}\n\n--- Contenu des pièces jointes ---\n{joined}";
}
}

[JsonIgnore]
public string ContentPJ_STR
{
get
{
if (ContentPJ == null || ContentPJ.Count == 0)
return "";

var joined = string.Join("\n---\n", ContentPJ);
return $"--- Contenu des pièces jointes ---\n{joined}";
}
}

public string Resume { get; set; } = "";
public string Categorie { get; set; } = "";
public string Strategie { get; set; } = "";
public string Reponse { get; set; } = "";
public bool IsImportant { get; set; } = false;
public bool IsUrgent { get; set; } = false;
public int ImportanceScore { get; set; } = 0; // de 0 à 5
public int UrgenceScore { get; set; } = 0; // de 0 à 5

public List<string>? ContentPJ { get; set; } = new();
public int PJ_NBR
{
get
{
if (ContentPJ == null || ContentPJ.Count == 0)
return 0;
return ContentPJ.Count;
}
}

[JsonIgnore]
public string PJ_NBR_STR
{
get
{
if (PJ_NBR > 0)
return "📎";
return "";
}
}

public bool IsPJ { get => ContentPJ != null && ContentPJ.Count > 0; }

public string Priorite { get => $"Important : {(IsImportant ? "✔️" : "❌")} - Urgent : {(IsUrgent ? "⚠️" : "❌")}"; }
public string ImportanceSTR => $"{ImportanceScore}/5";
public string UrgenceSTR => $"{UrgenceScore}/5";

public string ModeleIA { get; set; } = "";

[JsonIgnore]
public bool ToRemove { get; set; } = false;

public bool PresenceSuspecte { get; set; } = false;

[JsonIgnore]
public string PresenceSuspecte_STR
{
get
{
if (PresenceSuspecte)
return "⚠️";
return "";
}
}

[JsonIgnore]
public string IsDestinatairePrincipal_STR
{
get
{
if (IsDestinatairePrincipal)
return "📩";
else
return "👥";
}
}
[JsonIgnore]
public string IsDestinatairePrincipal_STR_TXT
{
get
{
if (IsDestinatairePrincipal)
return "Vous êtes destinataire principal de ce mail";
else
return "Vous êtes en copie de ce mail";
}
}
}

#region Classe MessageToSend
public class MessageToSend
{

public string Object { get; set; } = "";
public string DestinataireA { get; set; } = "";
public string DestinataireCC { get; set; } = "";

public string Type { get; set; } = "Professionnel";
public string Ton { get; set; } = "";
public string Posture { get; set; } = "";
public string Couleur { get; set; } = "";
public string NiveauDetail{ get; set; } = "";
public bool VOUS_TU { get; set; } = true;// "true : 'Vous'" ou "false : 'Tu'"

public string Query { get; set; } = "";
public string Body { get; set; } = "...";
}
#endregion
}

+ 13
- 0
EmailStructure/EmailStructure.csproj Voir le fichier

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MailKit" Version="4.14.0" />
</ItemGroup>

</Project>

+ 36
- 0
FaceRecognition/FaceRecognition.csproj Voir le fichier

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Emgu.CV.runtime.windows.cuda" Version="4.8.0.5324" />
</ItemGroup>

<ItemGroup>
<Folder Include="Models\" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ToolsService\ToolsService.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Models\face_detection_yunet_2023mar.onnx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Models\haarcascade_frontalface_alt.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Models\haarcascade_frontalface_default.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Models\model_reconnaissance.yml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>

+ 529
- 0
FaceRecognition/FaceRecognitionService.cs Voir le fichier

@@ -0,0 +1,529 @@
using Emgu.CV;
using Emgu.CV.Dnn;
using Emgu.CV.Face;
using Emgu.CV.Structure;
using System.Drawing;
using System.Text;
using ToolsServices;
using static System.Net.Mime.MediaTypeNames;

namespace FaceRecognition
{
public class FaceRecognitionService
{
#region Variables privées statiques
private static string NomFichierData = FichiersInternesService.ParamsFaceRecognition;
#endregion

#region Variables privées
private readonly string _PathDataTrainSet = @"D:\_TrainingData\FaceRecognition\TrainSet";
private readonly string _PathModel = @"Models";

private readonly string _NomModelDetect = @"face_detection_yunet_2023mar.onnx";
private readonly string _NomModel = @"model_reconnaissance.yml";
private readonly string _FaceHaarXML = @"haarcascade_frontalface_alt.xml";

private readonly int _ReSizeFaceImg = 300;
private readonly int _ReSizeImage = 400;
private readonly float _ScaleFactor = 0.9f;
private readonly System.Drawing.Size _MinSizeDetect = new(30, 30);

private readonly int _DistanceMini = 40;

private readonly MCvScalar _MCvScalarColorGreen = new(0, 255, 0);
private readonly MCvScalar _MCvScalarColorRed = new(0, 0, 255);
private readonly int _SizeBorder = 2;

private readonly LBPHFaceRecognizer _FaceRecognizer = new();
private readonly FaceDetectorYN _FaceNet;

private List<int> _LstLabels = new();
private List<string> _LstLabelsString = new();

private readonly System.Drawing.Size _InputSize = new(300, 300);
#endregion

#region Constructeur
public FaceRecognitionService(bool isCuda = false)
{
var parametres = LoadParametres();
_PathDataTrainSet = parametres.Path_TrainSet;

// Créer un objet CascadeClassifier pour détecter les visages
if (isCuda)
{
_FaceNet = new FaceDetectorYN(TrtFindFileFaceModelDetect(), "", _InputSize, backendId: Emgu.CV.Dnn.Backend.Cuda, targetId: Target.Cuda);
}
else
{
_FaceNet = new FaceDetectorYN(TrtFindFileFaceModelDetect(), "", _InputSize, backendId: Emgu.CV.Dnn.Backend.Default, targetId: Target.Cpu);
}
}
#endregion

#region Gestion des paramètres TrainSet et TestSet
public static ParametresFaceRecognitionService LoadParametres()
{
LoggerService.LogInfo("ParametresFaceRecognitionService.LoadParametres");

ParametresFaceRecognitionService SelectedItem = new();

try
{
if (File.Exists(NomFichierData))
{

string[] lignes = File.ReadAllLines(NomFichierData);

if (lignes.Length > 0)
SelectedItem.Path_TrainSet = lignes[0];

if (lignes.Length > 1)
SelectedItem.Path_TestSet = lignes[1];

}
return SelectedItem;

}
catch
{
return new();
}
}

public static bool SaveParametres(ParametresFaceRecognitionService selectedItem)
{
LoggerService.LogInfo("ParametresFaceRecognitionService.SaveParametres");

try
{
StringBuilder sb = new();
sb.AppendLine(selectedItem.Path_TrainSet);
sb.AppendLine(selectedItem.Path_TestSet);

File.WriteAllText(NomFichierData, sb.ToString());

return true;
}
catch
{
return false;
}

}
#endregion

#region Méthodes publiques
public bool CapturePhoto(string filename)
{
try
{
var inputImage = new Mat(filename);
var inputClone = inputImage.Clone();
var lstRetour = new List<string>();

if (!File.Exists(filename))
{
throw new Exception($"Fichier manquant : {filename}.");
}
_FaceRecognizer.Read(TrtFindFileModel());

// Détecter les visages dans l'image
var w = (int)(_ReSizeImage * 3 * _ScaleFactor);
var h = (int)(_ReSizeImage * 3 * inputImage.Height / inputImage.Width * _ScaleFactor);
CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h));
var faces = DetectMultiFaces(inputImage);

if (faces.Length == 0)
{
inputImage = inputClone.Clone();
w = (int)(_ReSizeImage * 2 * _ScaleFactor);
h = (int)(_ReSizeImage * 2 * inputImage.Height / inputImage.Width * _ScaleFactor);
CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h));
faces = DetectMultiFaces(inputImage);
}

if (faces.Length == 0)
{
inputImage = inputClone.Clone();
w = (int)(_ReSizeImage * _ScaleFactor);
h = (int)(_ReSizeImage * inputImage.Height / inputImage.Width * _ScaleFactor);
CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h));
faces = DetectMultiFaces(inputImage);
}

// Parcourir chaque visage détecté
foreach (var face in faces)
{
// Extraire la région du visage de l'image
var faceImage = new Mat(inputImage, face);

if (System.IO.File.Exists(filename))
System.IO.File.Delete(filename);

CvInvoke.Imwrite(filename, faceImage);

}

return true;
}
catch
{
return false;

}
}

public (bool, string) Training(bool isPhotoVisible)
{
return TrtTraining(isPhotoVisible);
}

public List<string> Predict(string filename)
{
(var lst, _) = TrtPredict(new(filename));
return lst;
}

public List<string> Predict(Mat inputImage)
{
(var lst, _) = TrtPredict(inputImage);
return lst;
}

public List<string> PredictLiveNoShow()
{
List<string> lst = new();
var videoCapture = new Emgu.CV.VideoCapture(0);
try
{
int i = 0;
while (true)
{
try
{
i++;
Mat mat = new();
videoCapture.Read(mat);

// Préparation de l'affichage de l'image
(lst, _) = TrtPredict(mat, false, true);

if (i > 4)
break;
}
catch (Exception ex)
{
Console.WriteLine($"Erreur : {ex.Message}");
}
}
}
catch
{

}
return lst;
}

public void PredictLive()
{
var videoCapture = new Emgu.CV.VideoCapture(0);
try
{
while (true)
{
try
{
Mat mat = new();
Mat matOutput;

videoCapture.Read(mat);

// Préparation de l'affichage de l'image
(_, matOutput) = TrtPredict(mat, false, true);
CvInvoke.Imshow("frame", matOutput);

// affichage et saisie d'un code clavier (Q ou ECHAP)
if (CvInvoke.WaitKey(1) == (int)ConsoleKey.Q || CvInvoke.WaitKey(1) == (int)ConsoleKey.Escape)
break;
}
catch (Exception ex)
{
Console.WriteLine($"Erreur : {ex.Message}");

}
}
}
catch
{

}
finally
{
// Ne pas oublier de fermer le flux et la fenetre
CvInvoke.WaitKey(0);
CvInvoke.DestroyAllWindows();
}
}
#endregion

#region Méthodes privées
private (bool, string) TrtTraining(bool isPhotoVisible)
{
bool isReturnOK = true;
string msgReturn = "";

try
{
// Créer des listes pour stocker les images de formation et les étiquettes correspondantes
var trainingImages = TrtFindImagesAndLabels(true, isPhotoVisible);

// Entraîner le reconnaiseur avec les images de formation et les étiquettes
_FaceRecognizer.Train(trainingImages.ToArray(), _LstLabels.ToArray());
for (int i = 0; i < _LstLabels.Count; i++)
{
_FaceRecognizer.SetLabelInfo(_LstLabels[i], _LstLabelsString[i]);
}


// Enregistrer le modèle entraîné
_FaceRecognizer.Write(Path.Combine(_PathModel, _NomModel));
}

catch (Exception ex)
{
isReturnOK = false;
msgReturn = ex.Message;
}

return (isReturnOK, msgReturn);
}

private (List<string>, Mat) TrtPredict(Mat inputImage, bool isAffichage = true, bool isOnlyFirstName=true)
{
var inputClone = inputImage.Clone();
var lstRetour = new List<string>();
string filename = TrtFindFileModel();
if (!File.Exists(filename))
{
throw new Exception($"Fichier manquant : {filename}.");
}
_FaceRecognizer.Read(TrtFindFileModel());

// Convertir l'image en niveaux de gris pour faciliter la détection des visages
var grayImage = new Mat();


// Détecter les visages dans l'image
var w = (int)(_ReSizeImage * 3 * _ScaleFactor);
var h = (int)(_ReSizeImage * 3 * inputImage.Height / inputImage.Width * _ScaleFactor);
CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h));
var faces = DetectMultiFaces(inputImage);

if (faces.Length == 0)
{
inputImage = inputClone.Clone();
w = (int)(_ReSizeImage * 2 * _ScaleFactor);
h = (int)(_ReSizeImage * 2 * inputImage.Height / inputImage.Width * _ScaleFactor);
CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h));
faces = DetectMultiFaces(inputImage);
}

if (faces.Length == 0)
{
inputImage = inputClone.Clone();
w = (int)(_ReSizeImage * _ScaleFactor);
h = (int)(_ReSizeImage * inputImage.Height / inputImage.Width * _ScaleFactor);
CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h));
faces = DetectMultiFaces(inputImage);
}

CvInvoke.CvtColor(inputImage, grayImage, Emgu.CV.CvEnum.ColorConversion.Bgr2Gray);

// Parcourir chaque visage détecté
foreach (var face in faces)
{
// Extraire la région du visage de l'image en niveaux de gris
var faceImage = new Mat(grayImage, face);
CvInvoke.Resize(faceImage, faceImage, new System.Drawing.Size(_ReSizeFaceImg, _ReSizeFaceImg));

// Reconnaître le visage à partir de l'image redimensionnée
var predict = _FaceRecognizer.Predict(faceImage);

if (predict.Distance < _DistanceMini)
{
var nameComplet = _FaceRecognizer.GetLabelInfo(predict.Label);
var name = "";
if (isOnlyFirstName)
{
name = nameComplet.Split('_')[0];
}
else
{
name = nameComplet.Replace("_", " ");
}
lstRetour.Add(name);

CvInvoke.Rectangle(inputImage, face, _MCvScalarColorGreen, _SizeBorder);
//CvInvoke.PutText(inputImage, name + " - " + predict.Distance.ToString("0.0"), new System.Drawing.Point(face.X, face.Y), Emgu.CV.CvEnum.FontFace.HersheySimplex, 1.0, _MCvScalarColor, _SizeBorder);
CvInvoke.PutText(inputImage, name, new System.Drawing.Point(face.X, face.Y), Emgu.CV.CvEnum.FontFace.HersheySimplex, 1.0, _MCvScalarColorGreen, _SizeBorder);
CvInvoke.PutText(inputImage, predict.Distance.ToString("0.0"), new System.Drawing.Point(face.X, face.Y + 30), Emgu.CV.CvEnum.FontFace.HersheySimplex, 0.75, _MCvScalarColorGreen, _SizeBorder);
}
else
{
//var name = "???";
//lstRetour.Add(name);
CvInvoke.Rectangle(inputImage, face, _MCvScalarColorRed, _SizeBorder);
CvInvoke.PutText(inputImage, predict.Distance.ToString("0.0"), new System.Drawing.Point(face.X, face.Y), Emgu.CV.CvEnum.FontFace.HersheySimplex, 1.0, _MCvScalarColorRed, _SizeBorder);
}
}

if (isAffichage)
{
CvInvoke.Imshow("reconnaissance", inputImage);
CvInvoke.WaitKey(0);
CvInvoke.DestroyAllWindows();
}
return (lstRetour, inputImage);
}

private List<Mat> TrtFindImagesAndLabels(bool isTraining, bool isPhotoVisible)
{
// Créer des listes pour stocker les images de formation et les étiquettes correspondantes
var lstTrainingImages = new List<Mat>();

if(System.IO.Directory.Exists(_PathDataTrainSet) == false)
{
throw new Exception($"Le dossier n'existe pas : {_PathDataTrainSet}");
}

try
{
_LstLabels = new();
_LstLabelsString = new();

var dirs = Directory.GetDirectories(_PathDataTrainSet);
int numDir = 1;
foreach (var dir in dirs)
{
var files = Directory.GetFiles(dir);

foreach (var file in files)
{
var dernierDir = dir.Split(@"\");
var nomDernierDir = dernierDir[^1];

if (isTraining)
{
var image = new Mat(file);
var grayImage = new Mat();


//var w = (int)(image.Width * _ScaleFactor);
//var h = (int)(image.Height * _ScaleFactor);
var w = (int)(_ReSizeImage * _ScaleFactor);
var h = (int)(_ReSizeImage * image.Height / image.Width * _ScaleFactor);
CvInvoke.Resize(image, image, new System.Drawing.Size(w, h));
var faces = DetectMultiFaces(image);

CvInvoke.CvtColor(image, grayImage, Emgu.CV.CvEnum.ColorConversion.Bgr2Gray);
if (faces.Length > 0)
{
var face = new Mat(grayImage, faces[0]);
CvInvoke.Resize(face, face, new System.Drawing.Size(_ReSizeFaceImg, _ReSizeFaceImg));
lstTrainingImages.Add(face);

_LstLabels.Add(numDir);
_LstLabelsString.Add(nomDernierDir);

if (isPhotoVisible)
{
CvInvoke.PutText(image, nomDernierDir, new System.Drawing.Point(faces[0].X, faces[0].Y), Emgu.CV.CvEnum.FontFace.HersheySimplex, 1.0, _MCvScalarColorGreen, _SizeBorder);
CvInvoke.Rectangle(image, faces[0], _MCvScalarColorGreen, _SizeBorder);
}
}
else
{
Console.WriteLine("Visage non détecté");
}
if (isPhotoVisible)
{
CvInvoke.Imshow("reconnaissance", image);
CvInvoke.WaitKey(0);
CvInvoke.DestroyAllWindows();
}
}
else
{
_LstLabels.Add(numDir);
_LstLabelsString.Add(nomDernierDir);
}

}
numDir++;
}

}


catch (Exception ex)
{
throw new Exception(ex.Message);
}

return lstTrainingImages;
}

public Rectangle[] DetectMultiFaces(Mat img)
{
var lstRect = new List<Rectangle>();

_FaceNet.InputSize = new System.Drawing.Size(img.Width, img.Height);

var outputFaces = new Mat();

_FaceNet.Detect(img, outputFaces);

var detectionArray = outputFaces.GetData();
if (detectionArray is null)
{
return new Rectangle[0];
}

var max = detectionArray.GetLength(0);

Parallel.For(0, max, i =>
{
var confidence = (float)((Single)detectionArray.GetValue(i, 14)!);
if (confidence > 0.5)
{
// Coordonnées 2 points qui tracent un rectangle englobe le visage
var x1 = (int)((Single)detectionArray.GetValue(i, 0)!);
var y1 = (int)((Single)detectionArray.GetValue(i, 1)!);
var x2 = (int)((Single)detectionArray.GetValue(i, 2)!);
var y2 = (int)((Single)detectionArray.GetValue(i, 3)!);

lstRect.Add(new System.Drawing.Rectangle(x1, y1, x2, y2));
}
});

return lstRect.ToArray();
}

private string TrtFindFileFaceHaarXML()
{
return Path.Combine(_PathModel, _FaceHaarXML);
}

private string TrtFindFileFaceModelDetect()
{
return Path.Combine(_PathModel, _NomModelDetect);
}

private string TrtFindFileModel()
{
return Path.Combine(_PathModel, _NomModel);
}
#endregion
}
}

+ 5
- 0
FaceRecognition/Models/_A LIRE.txt Voir le fichier

@@ -0,0 +1,5 @@
Fichiers a mettre dans ce répertoire :
- face_detection_yunet_2023mar.onnx
- haarcascade_frontalface_alt.xml
- haarcascade_frontalface_default.xml
- model_reconnaissance.yml

BIN
FaceRecognition/Models/face_detection_yunet_2023mar.onnx Voir le fichier


+ 26161
- 0
FaceRecognition/Models/haarcascade_frontalface_alt.xml
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 33314
- 0
FaceRecognition/Models/haarcascade_frontalface_default.xml
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 94785
- 0
FaceRecognition/Models/model_reconnaissance.yml
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 8
- 0
FaceRecognition/ParametresFaceRecognitionService.cs Voir le fichier

@@ -0,0 +1,8 @@
namespace FaceRecognition
{
public class ParametresFaceRecognitionService
{
public string Path_TrainSet { get; set; } = "";
public string Path_TestSet { get; set; } = "";
}
}

+ 13
- 0
FooocusService/FooocusService.csproj Voir le fichier

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\ToolsService\ToolsService.csproj" />
</ItemGroup>

</Project>

+ 241
- 0
FooocusService/Services/FooocusRequest.cs Voir le fichier

@@ -0,0 +1,241 @@
namespace Services.Fooocus
{
public class Lora
{
public bool enabled { get; set; } = false;
public string model_name { get; set; } = "sd_xl_offset_example-lora_1.0.safetensors";
public double weight { get; set; } = 0.5;
}

public class FooocusRequest_Text_to_Image
{
public string prompt { get; set; } = "";
public string negative_prompt { get; set; } = "blurry, low quality, distorted, watermark, text, extra limbs, cropped";
public string[] style_selections { get; set; } = new string[0];
public string performance_selection { get; set; } = "Speed";//Speed, Quality, Extreme Speed
public string aspect_ratios_selection { get; set; } = "1152*896";
public int image_number { get; set; } = 1;
public int image_seed { get; set; } = -1;

public double sharpness { get; set; } = 2.0; //0-30
public double guidance_scale { get; set; } = 4.0;//1-30

public string base_model_name { get; set; }= "juggernautXL_v8Rundiffusion.safetensors";
public string refiner_model_name { get; set; } = "None";
public double refiner_switch { get; set; } = 0.5;

public bool require_base64 { get; set; } = false;
public bool async_process { get; set; } = false;

public Lora[] loras { get; set; } = new Lora[0];
}

public class FooocusRequest
{
// --- PARAMÈTRES DE BASE ---

/// <summary>
/// Description textuelle de l’image à générer.
/// Exemple : "un chat steampunk sur un toit".
/// Obligatoire.
/// </summary>
public string Prompt { get; set; } = "";

/// <summary>
/// Liste de ce que l’on veut éviter (qualité faible, flou...).
/// Exemple : "blurry, low quality".
/// Recommandé pour améliorer la sortie.
/// </summary>
public string NegativePrompt { get; set; } = "blurry, low quality, distorted, watermark, text, extra limbs, cropped";

/// <summary>
/// Largeur de l’image en pixels.
/// Valeurs courantes : 512, 768, 1024.
/// Défaut = 768.
/// </summary>
public int Width { get; set; } = 768;

/// <summary>
/// Hauteur de l’image en pixels.
/// Valeurs courantes : 512, 768, 1024.
/// Défaut = 768.
/// </summary>
public int Height { get; set; } = 768;

/// <summary>
/// Nombre d’itérations de diffusion (steps).
/// Plus c’est haut, plus c’est détaillé mais lent.
/// Recommandé : 20–40.
/// Défaut = 30.
/// </summary>
public int Steps { get; set; } = 40;

/// <summary>
/// Influence du prompt sur le rendu (CFG scale).
/// Valeurs typiques : 5–12.
/// Recommandé = 7.5.
/// </summary>
public double GuidanceScale { get; set; } = 9.0;


// --- STYLE & PERFORMANCE ---

/// <summary>
/// Liste des styles appliqués (ex. "Fooocus V2", "Fooocus Enhance").
/// Peut rester vide.
/// </summary>
public string[] StyleSelections { get; set; } = new string[0];

/// <summary>
/// Mode de performance.
/// Options : "Speed", "Quality", "Extreme Speed".
/// Défaut = "Speed".
/// </summary>
public string PerformanceSelection { get; set; } = "Speed";

/// <summary>
/// À quel moment le modèle "refiner" intervient (0 = jamais, 1 = fin).
/// Défaut = 0.5.
/// </summary>
public double RefinerSwitch { get; set; } = 0.0;

/// <summary>
/// Netteté appliquée à l’image finale.
/// Valeurs : 1.0 (douce) à 3.0 (très nette).
/// Défaut = 2.0.
/// </summary>
public double Sharpness { get; set; } = 2.0;


// --- SEED & BATCH ---

/// <summary>
/// Seed de génération.
/// -1 = aléatoire.
/// Utiliser une seed fixe pour reproductibilité.
/// </summary>
public int ImageSeed { get; set; } = -1;

/// <summary>
/// Nombre d’images à générer en une requête.
/// Défaut = 1.
/// </summary>
public int ImageNumber { get; set; } = 1;

/// <summary>
/// Ratio prédéfini de l’image (format).
/// Exemple : "768*768", "1152*896".
/// Défaut = "768*768".
/// </summary>
public string AspectRatiosSelection { get; set; } = "768*768";


// --- CONTROLNET / IMAGE PROMPTS ---

/// <summary>
/// Première image de référence (ControlNet).
/// Peut être une URL ou un chemin.
/// </summary>
public string? CnImg1 { get; set; }

/// <summary>
/// Type de ControlNet pour CnImg1 (ex. "Canny", "Depth", "ImagePrompt").
/// </summary>
public string? CnType1 { get; set; }

/// <summary>
/// Poids de l’influence de CnImg1 (0–1).
/// Défaut ≈ 0.8.
/// </summary>
public double? CnWeight1 { get; set; }

/// <summary>
/// Étape à laquelle arrêter ControlNet (0–1).
/// Défaut ≈ 0.2.
/// </summary>
public double? CnStop1 { get; set; }

// (Idem pour 4 images maximum)
public string? CnImg2 { get; set; }
public string? CnType2 { get; set; }
public double? CnWeight2 { get; set; }
public double? CnStop2 { get; set; }

public string? CnImg3 { get; set; }
public string? CnType3 { get; set; }
public double? CnWeight3 { get; set; }
public double? CnStop3 { get; set; }

public string? CnImg4 { get; set; }
public string? CnType4 { get; set; }
public double? CnWeight4 { get; set; }
public double? CnStop4 { get; set; }


// --- LORAS ---

/// <summary>
/// Utiliser les LoRAs par défaut du modèle.
/// Défaut = true.
/// </summary>
public bool UseDefaultLoras { get; set; } = false;

/// <summary>
/// URLs de LoRAs personnalisés (séparés par des virgules).
/// </summary>
public string? LorasCustomUrls { get; set; }

/// <summary>
/// Poids d’influence des LoRAs.
/// Défaut = 1.0.
/// </summary>
public double? LorasWeight { get; set; } = 0.0;


// --- INPAINTING ---

/// <summary>
/// Image d’entrée pour l’inpainting (corriger une zone).
/// </summary>
public string? InpaintInputImage { get; set; }

/// <summary>
/// Masque de l’inpainting (zones à modifier en noir).
/// </summary>
public string? InpaintInputMask { get; set; }


// --- OUTPAINTING ---

/// <summary>
/// Mode d’outpainting (agrandir une image au-delà des bords).
/// Exemple : "left,right,top".
/// </summary>
public string? OutpaintSelections { get; set; }

/// <summary>
/// Distance d’extension en haut (en pixels).
/// Défaut = 0.
/// </summary>
public int OutpaintDistanceTop { get; set; } = 0;

public int OutpaintDistanceLeft { get; set; } = 0;
public int OutpaintDistanceRight { get; set; } = 0;
public int OutpaintDistanceBottom { get; set; } = 0;


// --- UPSCALE / VARIATIONS ---

/// <summary>
/// Méthode appliquée : "Upscale", "Variation", "Outpaint".
/// </summary>
public string? UovMethod { get; set; }

/// <summary>
/// Image d’entrée pour upscale ou variation.
/// </summary>
public string? UovInputImage { get; set; }

}

}

+ 346
- 0
FooocusService/Services/FooocusService.cs Voir le fichier

@@ -0,0 +1,346 @@
using System.Text;
using System.Text.Json;
using ToolsServices;

namespace Services.Fooocus
{
public static class FooocusService
{
#region Méthodes publiques

#region Gestion des paramètres

public static (string, string, string) LoadParametresGenerateImg()
{
LoggerService.LogInfo("FooocusService.LoadParametresGenerateImg");

string url = "";
string endpointv1 = "";
string endpointv2 = "";

try
{
if (File.Exists(FichiersInternesService.ParamsGenerateImg))
{

string[] lignes = File.ReadAllLines(FichiersInternesService.ParamsGenerateImg);

if (lignes.Length > 0)
url = lignes[0];

if (lignes.Length > 1)
endpointv1 = lignes[1];

if (lignes.Length > 2)
endpointv2 = lignes[2];

}
return (url, endpointv1, endpointv2);

}
catch
{
return ("", "", "");
}
}

public static bool SaveParametresGenerateImg(string url, string endpointv1, string endpointv2)
{
LoggerService.LogInfo("FooocusService.SaveParametresGenerateImg");

try
{
StringBuilder sb = new();
sb.AppendLine(url);
sb.AppendLine(endpointv1);
sb.AppendLine(endpointv2);

File.WriteAllText(FichiersInternesService.ParamsGenerateImg, sb.ToString());

return true;
}
catch
{
return false;
}

}

public static FooocusRequest_Text_to_Image LoadParametresFooocus()
{
LoggerService.LogInfo("FooocusService.LoadParametresFooocus");

FooocusRequest_Text_to_Image param = new();

try
{
if (File.Exists(FichiersInternesService.ParamsFooocus))
{

string[] lignes = File.ReadAllLines(FichiersInternesService.ParamsFooocus);

if (lignes.Length > 0)
param.negative_prompt = lignes[0];

if (lignes.Length > 1)
param.guidance_scale = double.Parse(lignes[1]);

if (lignes.Length > 2)
param.performance_selection = lignes[2];

if (lignes.Length > 3)
param.sharpness = double.Parse(lignes[3]);

if (lignes.Length > 4)
param.style_selections = lignes[4].Split(',', StringSplitOptions.RemoveEmptyEntries) // coupe sur les virgules
.Select(s => s.Trim()) // enlève les espaces inutiles
.ToArray();
}
return param;

}
catch
{
return param;
}
}

public static bool SaveParametresFooocus(FooocusRequest_Text_to_Image param, List<string> ListeSelectedsStylesFooocus)
{
LoggerService.LogInfo("FooocusService.SaveParametresFooocus");

try
{
StringBuilder sb = new();
sb.AppendLine(param.negative_prompt);
sb.AppendLine(param.guidance_scale.ToString());
sb.AppendLine(param.performance_selection);
sb.AppendLine(param.sharpness.ToString());
sb.AppendLine(string.Join(", ", ListeSelectedsStylesFooocus));

File.WriteAllText(FichiersInternesService.ParamsFooocus, sb.ToString());

return true;
}
catch
{
return false;
}

}
#endregion

#region Interactions
public static async Task<bool> IsFooocusActif()
{
LoggerService.LogInfo($"FooocusService.IsFooocusActif");
try
{
(string baseUrl, _, _) = LoadParametresGenerateImg();
if (baseUrl == "")
{
var msg = "Erreur de chargement des paramètres Fooocus.";
LoggerService.LogWarning(msg);
return false;
}
var url = $"{baseUrl}";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(360) };

client.Timeout = TimeSpan.FromSeconds(30);
var response = await client.GetAsync(url);
LoggerService.LogDebug($"FooocusService.IsFooocusActif : {response.IsSuccessStatusCode}");
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
LoggerService.LogError($"FooocusService.IsFooocusActif : False --> {ex.Message}");
return false;
}
}

public static async Task<List<string>> GetFooocusStylesAsync()
{
LoggerService.LogInfo("FooocusService.GetFooocusStylesAsync");

(string baseUrl, _, _) = LoadParametresGenerateImg();
var url = $"{baseUrl}/v1/engines/styles";
if (url == "")
{
LoggerService.LogWarning("FooocusService.GetFooocusStylesAsync : URL de l'API de génération d'image non configurée.");
var lst = new List<string>();
lst.Add("URL de l'API de génération d'image non configurée.");
return lst;
}

using var client = new HttpClient();

var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync();

// Le JSON est du type ["Fooocus V2","Random Style",...]
var styles = JsonSerializer.Deserialize<List<string>>(json);

return styles ?? new List<string>();
}

public static async Task<string> GenerateTextToImageWithIP(string promptFinal, List<string> imagesFullFilename)
{
LoggerService.LogInfo("FooocusService.GenerateTextToImageWithIP");

(string baseUrl, _, string endpoint) = LoadParametresGenerateImg();
//endpoint = "/v2/generation/text-to-image-with-ip";

var images = new List<object>();
double poidsTotal = 1.9;
foreach (var img in imagesFullFilename)
{
poidsTotal = poidsTotal - 0.1;
if (poidsTotal < 0.2)
poidsTotal = 0.2;

var uneImg = new
{
cn_img = Convert.ToBase64String(File.ReadAllBytes(img)),
cn_stop = 0.5,
cn_weight = poidsTotal,
cn_type = "ImagePrompt"
};

images.Add(uneImg);
}



var requestBody = new
{
prompt = promptFinal,
image_prompts = images.ToArray()
};

var url = $"{baseUrl}{endpoint}";

using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(360) };

var json = JsonSerializer.Serialize(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");

// Envoi de la requête POST
var response = await client.PostAsync($"{url}", content);
response.EnsureSuccessStatusCode();

// Lecture de la réponse JSON
var responseJson = await response.Content.ReadAsStringAsync();

// Désérialisation
using var doc = JsonDocument.Parse(responseJson);

var imageUrl = doc.RootElement[0].GetProperty("url").GetString();
if (imageUrl == null)
{
return "";
}
imageUrl = imageUrl.Replace("http://127.0.0.1:8888", baseUrl);

return $"{imageUrl}";

}

public static async Task<string> GenerateImageWithFooocus(FooocusRequest_Text_to_Image requestBody)
{
LoggerService.LogInfo("FooocusService.GenerateImageWithFooocus(requestBody)");

try
{
(string baseUrl, string endpoint, _) = LoadParametresGenerateImg();
var url = $"{baseUrl}{endpoint}";
if (url == "")
{
LoggerService.LogWarning("FooocusService.GenerateImageWithFooocus : URL de l'API de génération d'image non configurée.");
return "URL de l'API de génération d'image non configurée.";
}

using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(360) };

var json = JsonSerializer.Serialize(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");

// Envoi de la requête POST
var response = await client.PostAsync($"{url}", content);
response.EnsureSuccessStatusCode();

// Lecture de la réponse JSON
var responseJson = await response.Content.ReadAsStringAsync();

// Désérialisation
using var doc = JsonDocument.Parse(responseJson);

var imageUrl = doc.RootElement[0].GetProperty("url").GetString();
if (imageUrl == null)
{
return "";
}
imageUrl = imageUrl.Replace("http://127.0.0.1:8888", baseUrl);
return $"{imageUrl}";
}
catch (Exception ex)
{
LoggerService.LogError($"Erreur lors de la génération de l'image : {ex.Message}");
return $"Erreur lors de la génération de l'image : {ex.Message}";
}
}

public static async Task<string> GenerateImageWithFooocus(string prompt)
{
LoggerService.LogInfo("FooocusService.GenerateImageWithFooocus(prompt)");

try
{
// Création du JSON à envoyer
var requestBody = new FooocusRequest_Text_to_Image();
if (File.Exists(FichiersInternesService.ParamsFooocus))
{
requestBody = LoadParametresFooocus();
}
requestBody.prompt = prompt;

/*
Lora lora1 = new Lora()
{
enabled = true,
model_name = "sd_xl_offset_example-lora_1.0.safetensors",
weight = 0.5
};
requestBody.loras.Append(lora1);
*/

/*
var requestBody = new
{
prompt = prompt,//"un chat steampunk sur un toit",
negative_prompt = "blurry, low quality, distorted, watermark, text, extra limbs, cropped",
guidance_scale = 9.0,
StyleSelections = new string[0], // vide => pas d’expansion Fooocus V2
performance_selection = "Quality",
sharpness = 2.0,
image_seed = -1,
image_number = 1
};
*/


return await GenerateImageWithFooocus(requestBody);

}
catch (Exception ex)
{
LoggerService.LogError($"Erreur lors de la génération de l'image : {ex.Message}");
return $"Erreur lors de la génération de l'image : {ex.Message}";
}
}
#endregion

#endregion
}
}

+ 14
- 0
MaUI/App.xaml Voir le fichier

@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MaUI"
x:Class="MaUI.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

+ 11
- 0
MaUI/App.xaml.cs Voir le fichier

@@ -0,0 +1,11 @@
namespace MaUI;

public partial class App : Application
{
public App()
{
InitializeComponent();

MainPage = new AppShell();
}
}

+ 15
- 0
MaUI/AppShell.xaml Voir le fichier

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="MaUI.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MaUI"
Shell.FlyoutBehavior="Disabled"
Title="MaUI">

<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />

</Shell>

+ 9
- 0
MaUI/AppShell.xaml.cs Voir le fichier

@@ -0,0 +1,9 @@
namespace MaUI;

public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}

+ 84
- 0
MaUI/MaUI.csproj Voir le fichier

@@ -0,0 +1,84 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->

<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->

<OutputType>Exe</OutputType>
<RootNamespace>MaUI</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<!-- Display name -->
<ApplicationTitle>MaUI</ApplicationTitle>

<!-- App Identifier -->
<ApplicationId>com.companyname.maui</ApplicationId>

<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>

<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>

<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />

<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />

<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />

<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />

<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.9.0" />
</ItemGroup>

<ItemGroup>
<Compile Update="Pages\ChatRoom\ChatRoom_LLM.xaml.cs">
<DependentUpon>ChatRoom_LLM.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Mails\ListeEmails.xaml.cs">
<DependentUpon>ListeEmails.xaml</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
<MauiXaml Update="Pages\ChatRoom\ChatRoom_LLM.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Pages\Mails\ListeEmails.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>

</Project>

+ 8
- 0
MaUI/MaUI.csproj.user Voir le fichier

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<IsFirstTimeProjectOpen>False</IsFirstTimeProjectOpen>
<ActiveDebugFramework>net8.0-windows10.0.19041.0</ActiveDebugFramework>
<ActiveDebugProfile>Windows Machine</ActiveDebugProfile>
</PropertyGroup>
</Project>

+ 15
- 0
MaUI/MainPage.xaml Voir le fichier

@@ -0,0 +1,15 @@

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MaUI.MainPage">
<ScrollView>
<StackLayout Padding="10">
<Label Text="MainMenu" FontSize="20"/>
<Button Text="Liste Emails" Clicked="OnListeEmails_Clicked" />
<Button Text="ChatRoom" Clicked="OnChatRoom_Clicked" />

<Button Text="Gestion LOGS" Clicked="OnLOGS_Liste_Clicked" />

</StackLayout>
</ScrollView>
</ContentPage>

+ 48
- 0
MaUI/MainPage.xaml.cs Voir le fichier

@@ -0,0 +1,48 @@

namespace MaUI
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
//VerifConnexion();
}

#region Méthodes pour la vérification de connexion
private void VerifConnexion()
{
if (!UserConntected.IsConnected)
Navigation.PushAsync(new AuthLogin());
}

protected override void OnAppearing()
{
//VerifConnexion();
}

#endregion

#region Méthodes du Menu
private async void OnListeEmails_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new ListeEmails());
}

private async void OnChatRoom_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new ChatRoomPage());
}

private async void OnLOGS_Liste_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new LOGS_Liste_Page());
}



#endregion
}

}

+ 24
- 0
MaUI/MauiProgram.cs Voir le fichier

@@ -0,0 +1,24 @@
using Microsoft.Extensions.Logging;

namespace MaUI;

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});

#if DEBUG
builder.Logging.AddDebug();
#endif

return builder.Build();
}
}

+ 22
- 0
MaUI/Model/base/LOGS.cs Voir le fichier

@@ -0,0 +1,22 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MaUI
{
public partial class LOGS_DTO
{
public Guid ID { get; set; }
public string NIVEAU { get; set; } ="";
public DateTime HORODATAGE { get; set; }
public string MESSAGE { get; set; } ="";


//Les propriétés de navigation
}
}


+ 0
- 0
MaUI/Model/custom/_METTRE ICI LES AJOUTS CUSTOMS.txt Voir le fichier


+ 46
- 0
MaUI/Pages/AuthLogin/Login.xaml Voir le fichier

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MaUI.AuthLogin"
Title="Login">

<ScrollView>
<StackLayout Padding="10" Spacing="10">


<!-- LOGIN -->
<Grid ColumnDefinitions="150, *" Padding="0,5">
<Label Text="Login* :" FontAttributes="Bold" VerticalOptions="Center" Grid.Row="0" Grid.Column="0"/>
<Entry Text="{Binding SelectedItem.Username, Mode=TwoWay}" Placeholder="Saisissez votre login" Grid.Row="0" Grid.Column="1"/>
</Grid>

<!-- PASSWORD -->
<Grid ColumnDefinitions="150, *" Padding="0,5">
<Label Text="Password* :" FontAttributes="Bold" VerticalOptions="Center" Grid.Row="1" Grid.Column="0"/>
<Entry x:Name="PasswordEntry" Text="{Binding SelectedItem.Password, Mode=TwoWay}" IsPassword="True" Placeholder="Saisissez votre mot de passe" Grid.Row="1" Grid.Column="1"/>
</Grid>

<Grid ColumnDefinitions="200, 200" Padding="0,5">
<Button Text="Annuler"
Command="{Binding AnnulerCommand}"
BackgroundColor="Gray"
TextColor="White"
CornerRadius="10"
HeightRequest="50"
WidthRequest="120"
Grid.Row="6" Grid.Column="0"
Margin="0,0,0,0"/>

<Button Text="Valider"
Command="{Binding ValiderCommand}"
BackgroundColor="Green"
TextColor="White"
CornerRadius="10"
HeightRequest="50"
WidthRequest="120"
Grid.Row="6" Grid.Column="1"
Margin="0,0,0,0"/>
</Grid>
</StackLayout>
</ScrollView>
</ContentPage>

+ 13
- 0
MaUI/Pages/AuthLogin/Login.xaml.cs Voir le fichier

@@ -0,0 +1,13 @@
namespace MaUI;

public partial class AuthLogin : ContentPage
{
AuthLogin_VM _VM;
public AuthLogin()
{
InitializeComponent();
_VM = new AuthLogin_VM();
BindingContext = _VM;
}

}

+ 83
- 0
MaUI/Pages/ChatRoom/ChatRoom_LLM.xaml Voir le fichier

@@ -0,0 +1,83 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MaUI"
x:Class="MaUI.ChatRoomPage"
BackgroundColor="#FFF0F0F0">

<ContentPage.BindingContext>
<viewmodels:ChatRoom_VM />
</ContentPage.BindingContext>

<Grid RowDefinitions="Auto,*">
<!-- Barre supérieure -->
<HorizontalStackLayout Padding="10" Spacing="20">
<Button Text="🗑" WidthRequest="74" Command="{Binding DeleteConversationCommand}" />
<Button Text="Nouvelle conversation" WidthRequest="138" Command="{Binding NewConversationCommand}" />
<Picker ItemsSource="{Binding InstalledModels}"
ItemDisplayBinding="{Binding Name}"
SelectedItem="{Binding SelectedModel}" WidthRequest="300" />
</HorizontalStackLayout>

<!-- Contenu principal -->
<Grid Row="1" ColumnDefinitions="200,*" Padding="5">

<!-- Colonne gauche -->
<VerticalStackLayout>
<CollectionView ItemsSource="{Binding Conversations}" SelectionMode="Single"
SelectedItem="{Binding SelectedConversation}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Title}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>

<Button Text="Ajout de documents" Command="{Binding AddDocumentsCommand}" />
<CollectionView ItemsSource="{Binding SelectedDocumentsListe}" />
<Button Text="🗑" Command="{Binding ClearDocumentsCommand}" />
</VerticalStackLayout>

<!-- Colonne droite : chat -->
<Grid Grid.Column="1" RowDefinitions="*,Auto">
<!-- Historique -->
<ScrollView>
<VerticalStackLayout BindableLayout.ItemsSource="{Binding CurrentMessages}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Frame Padding="10" CornerRadius="10" Margin="5"
BackgroundColor="{Binding Role, Converter={StaticResource RoleToColorConverter}}">
<Label Text="{Binding Content}" LineBreakMode="WordWrap" />
</Frame>
</DataTemplate>
</BindableLayout.ItemTemplate>
</VerticalStackLayout>
</ScrollView>

<!-- Saisie -->
<VerticalStackLayout Grid.Row="1" Padding="5">
<Editor Text="{Binding NewMessage}" AutoSize="TextChanges" HeightRequest="70" />
<HorizontalStackLayout Spacing="10">
<CheckBox IsChecked="{Binding IsApiExterne}" />
<Label Text="API Externe" VerticalOptions="Center" />
</HorizontalStackLayout>

<HorizontalStackLayout Spacing="10">
<CheckBox IsChecked="{Binding IsRAG}" />
<Label Text="RAG ?" VerticalOptions="Center" />
</HorizontalStackLayout>
<HorizontalStackLayout Spacing="10">
<CheckBox IsChecked="{Binding IsGenerateImg}" />
<Label Text="Générer une image ?" VerticalOptions="Center" />
</HorizontalStackLayout>
<HorizontalStackLayout Spacing="10">
<CheckBox IsChecked="{Binding IsWithAssistant}" />
<Label Text="Assistant IA ?" VerticalOptions="Center" />
</HorizontalStackLayout>

<Button Text="Envoyer" Command="{Binding SendMessageCommand}" />
</VerticalStackLayout>
</Grid>
</Grid>
</Grid>
</ContentPage>

+ 18
- 0
MaUI/Pages/ChatRoom/ChatRoom_LLM.xaml.cs Voir le fichier

@@ -0,0 +1,18 @@

namespace MaUI;

public partial class ChatRoomPage : ContentPage
{
ChatRoom_VM _VM;

public ChatRoomPage()
{
InitializeComponent();
_VM = new ChatRoom_VM();
BindingContext = _VM;
}



}

+ 30
- 0
MaUI/Pages/Mails/ListeEmails.xaml Voir le fichier

@@ -0,0 +1,30 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MaUI.ListeEmails"
Title="Liste LOGS">
<ScrollView>
<StackLayout Padding="10" Spacing="10">
<!-- Liste des Items -->
<ListView
ItemsSource="{Binding Liste_Items}"
SelectedItem="{Binding SelectedItem}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid Padding="10" ColumnDefinitions="*">
<Label Text="{Binding NIVEAU}" FontSize="16" VerticalOptions="Center" HorizontalOptions="Start" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>

<!-- Boutons pour les opérations -->
<StackLayout Orientation="Horizontal" Spacing="10">
<Button Text="Créer" WidthRequest="120" Command="{Binding CreateCommand}" Background="Blue" />
<Button Text="Modifier" WidthRequest="120" Command="{Binding EditCommand}" Background="Green" IsEnabled="{Binding SelectedItem}" />
<Button Text="Supprimer" WidthRequest="120" Command="{Binding DeleteCommand}" Background="Red" IsEnabled="{Binding SelectedItem}" />
</StackLayout>
</StackLayout>
</ScrollView>
</ContentPage>

+ 15
- 0
MaUI/Pages/Mails/ListeEmails.xaml.cs Voir le fichier

@@ -0,0 +1,15 @@
namespace MaUI;

public partial class ListeEmails : ContentPage
{
ListeEmails_VM _VM;

public ListeEmails()
{
InitializeComponent();
_VM = new ListeEmails_VM();
BindingContext = _VM;
}

}


+ 6
- 0
MaUI/Platforms/Android/AndroidManifest.xml Voir le fichier

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

+ 10
- 0
MaUI/Platforms/Android/MainActivity.cs Voir le fichier

@@ -0,0 +1,10 @@
using Android.App;
using Android.Content.PM;
using Android.OS;

namespace MaUI;

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}

+ 15
- 0
MaUI/Platforms/Android/MainApplication.cs Voir le fichier

@@ -0,0 +1,15 @@
using Android.App;
using Android.Runtime;

namespace MaUI;

[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}

protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

+ 6
- 0
MaUI/Platforms/Android/Resources/values/colors.xml Voir le fichier

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>

+ 9
- 0
MaUI/Platforms/MacCatalyst/AppDelegate.cs Voir le fichier

@@ -0,0 +1,9 @@
using Foundation;

namespace MaUI;

[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

+ 14
- 0
MaUI/Platforms/MacCatalyst/Entitlements.plist Voir le fichier

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
<dict>
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>


+ 38
- 0
MaUI/Platforms/MacCatalyst/Info.plist Voir le fichier

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->

<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

+ 15
- 0
MaUI/Platforms/MacCatalyst/Program.cs Voir le fichier

@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;

namespace MaUI;

public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

+ 16
- 0
MaUI/Platforms/Tizen/Main.cs Voir le fichier

@@ -0,0 +1,16 @@
using System;
using Microsoft.Maui;
using Microsoft.Maui.Hosting;

namespace MaUI;

class Program : MauiApplication
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

static void Main(string[] args)
{
var app = new Program();
app.Run(args);
}
}

+ 15
- 0
MaUI/Platforms/Tizen/tizen-manifest.xml Voir le fichier

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="MaUI.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
</manifest>

+ 8
- 0
MaUI/Platforms/Windows/App.xaml Voir le fichier

@@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="MaUI.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:MaUI.WinUI">

</maui:MauiWinUIApplication>

+ 24
- 0
MaUI/Platforms/Windows/App.xaml.cs Voir le fichier

@@ -0,0 +1,24 @@
using Microsoft.UI.Xaml;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace MaUI.WinUI;

/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
}

protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}


+ 46
- 0
MaUI/Platforms/Windows/Package.appxmanifest Voir le fichier

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">

<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />

<mp:PhoneIdentity PhoneProductId="09977730-76D0-4D6E-9F2F-051D59EA6ED0" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>

<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>

<Resources>
<Resource Language="x-generate" />
</Resources>

<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
</Application>
</Applications>

<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>

</Package>

+ 15
- 0
MaUI/Platforms/Windows/app.manifest Voir le fichier

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MaUI.WinUI.app"/>

<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

+ 9
- 0
MaUI/Platforms/iOS/AppDelegate.cs Voir le fichier

@@ -0,0 +1,9 @@
using Foundation;

namespace MaUI;

[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

+ 32
- 0
MaUI/Platforms/iOS/Info.plist Voir le fichier

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

+ 15
- 0
MaUI/Platforms/iOS/Program.cs Voir le fichier

@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;

namespace MaUI;

public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

+ 51
- 0
MaUI/Platforms/iOS/Resources/PrivacyInfo.xcprivacy Voir le fichier

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps.
The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK.

You are responsible for adding extra entries as needed for your application.

More information: https://aka.ms/maui-privacy-manifest
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<!--
The entry below is only needed when you're using the Preferences API in your app.
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict> -->
</array>
</dict>
</plist>

+ 8
- 0
MaUI/Properties/launchSettings.json Voir le fichier

@@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "MsixPackage",
"nativeDebugging": false
}
}
}

+ 4
- 0
MaUI/Resources/AppIcon/appicon.svg Voir le fichier

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

+ 8
- 0
MaUI/Resources/AppIcon/appiconfg.svg Voir le fichier

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

BIN
MaUI/Resources/Fonts/OpenSans-Regular.ttf Voir le fichier


BIN
MaUI/Resources/Fonts/OpenSans-Semibold.ttf Voir le fichier


BIN
MaUI/Resources/Images/dotnet_bot.png Voir le fichier

Before After
Width: 1200  |  Height: 738  |  Size: 68KB

+ 15
- 0
MaUI/Resources/Raw/AboutAssets.txt Voir le fichier

@@ -0,0 +1,15 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories). Deployment of the asset to your application
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.

<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />

These files will be deployed with your package and will be accessible using Essentials:

async Task LoadMauiAsset()
{
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
using var reader = new StreamReader(stream);

var contents = reader.ReadToEnd();
}

+ 8
- 0
MaUI/Resources/Splash/splash.svg Voir le fichier

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

+ 45
- 0
MaUI/Resources/Styles/Colors.xaml Voir le fichier

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">

<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->

<Color x:Key="Primary">#512BD4</Color>
<Color x:Key="PrimaryDark">#ac99ea</Color>
<Color x:Key="PrimaryDarkText">#242424</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="SecondaryDarkText">#9880e5</Color>
<Color x:Key="Tertiary">#2B0B98</Color>

<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Magenta">#D600AA</Color>
<Color x:Key="MidnightBlue">#190649</Color>
<Color x:Key="OffBlack">#1f1f1f</Color>

<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>

<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>

<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
</ResourceDictionary>

+ 427
- 0
MaUI/Resources/Styles/Styles.xaml Voir le fichier

@@ -0,0 +1,427 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">

<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>

<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
</Style>

<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
</Style>

<Style TargetType="BoxView">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>

<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="Frame">
<Setter Property="HasShadow" Value="False" />
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>

<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value="0.5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="Span">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</Style>

<Style TargetType="Label" x:Key="Headline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="32" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>

<Style TargetType="Label" x:Key="SubHeadline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>

<Style TargetType="ListView">
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>

<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>

<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="Shadow">
<Setter Property="Radius" Value="15" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Offset" Value="10,10" />
</Style>

<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>

<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>

<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
</Style>

<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>

<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>

<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>

</ResourceDictionary>

+ 180
- 0
MaUI/Services/base/ApiService.cs Voir le fichier

@@ -0,0 +1,180 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Claims;

namespace MaUI
{
public static class UserConntected
{
public static bool IsConnected { get; private set; } = false;
public static string Token { get; private set; } = "";
public static string ID { get; private set; } = "";
public static string FirstName { get; private set; } = "";
public static string LastName { get; private set; } = "";
public static string Email { get; private set; } = "";
public static List<string>? Roles { get; private set; } = new();

public static void SetToken(string jwt)
{
Token = jwt;

var handler = new JwtSecurityTokenHandler();
var decodedToken = handler.ReadJwtToken(jwt);
var identity = new ClaimsIdentity(decodedToken.Claims, "jwt");

//FirstName = decodedToken.Claims.FirstOrDefault(c => c.Type == "FirstName")!.Value;
//LastName = decodedToken.Claims.FirstOrDefault(c => c.Type == "LastName")!.Value;
//Email = decodedToken.Claims.FirstOrDefault(c => c.Type == "Email")!.Value;
ID = decodedToken.Claims.FirstOrDefault(c => c.Type == "ID")!.Value;
IsConnected = true;

var user = new ClaimsPrincipal(identity);

Roles = user.FindAll(ClaimTypes.Role).Select(r => r.Value).ToList();

SecureStorage.SetAsync("authToken", jwt); // Stocke le token
}
}

public class AuthorizedHttpClientHandler : DelegatingHandler
{
public AuthorizedHttpClientHandler()
{
InnerHandler = new HttpClientHandler();
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await SecureStorage.GetAsync("authToken");
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
return await base.SendAsync(request, cancellationToken);
}
}

public class ApiService
{
private readonly HttpClient _httpClient;
private readonly HttpClient _httpClientLogin;

#region Constructeur
public ApiService()
{
_httpClientLogin = new HttpClient { BaseAddress = new Uri("https://localhost:7008") };
_httpClient = new HttpClient(new AuthorizedHttpClientHandler());
_httpClient.BaseAddress = new Uri("https://localhost:7008");
}
#endregion

#region Service Create
public async Task<bool> Create_Async<T>(string url, T data)
{
var response = await _httpClient.PostAsJsonAsync(url, data);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return false;
}
else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
{
throw new Exception("Internal server error");
}
return response.IsSuccessStatusCode;
}
#endregion

#region Service Update
public async Task<bool> Update_Async<T>(string url, T data)
{
var response = await _httpClient.PutAsJsonAsync(url, data);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return false;
}
else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
{
throw new Exception("Internal server error");
}
return response.IsSuccessStatusCode;
}
#endregion

#region Service Delete
public async Task<bool> Delete_Async<T>(string url)
{
var response = await _httpClient.DeleteAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return false;
}
else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
{
throw new Exception("Internal server error");
}
return response.IsSuccessStatusCode;
}
#endregion

#region Service GetAll
public async Task<T[]> GetAll_Async<T>(string url)
{
// Charger les items
var response = await _httpClient.GetFromJsonAsync<T[]>(url);
return response ?? Array.Empty<T>();
}
#endregion

#region Service EnvoiRequete
public async Task<string> EnvoiRequete<T>(string url, T data)
{
var response = await _httpClient.PostAsJsonAsync(url, data);

if (!response.IsSuccessStatusCode)
return $"Erreur : {response.StatusCode}";

// lire le contenu JSON renvoyé
var result = await response.Content.ReadAsStringAsync();
return result;
}
#endregion

#region Service GetById
/* A IMPLEMENTER SI BESOIN
public async Task<T> GetById_Async<T>(string url)
{

}
*/
#endregion

#region AuthLogin
public async Task<bool> Connexion(LoginModel item)
{
try
{
string url= $"api/auth/login";
var response = await _httpClientLogin.PostAsJsonAsync(url, item);
response.EnsureSuccessStatusCode();
var loginResponse = await response.Content.ReadFromJsonAsync<LoginResponse>();
if (loginResponse is not null && !string.IsNullOrWhiteSpace(loginResponse.Token))
{
UserConntected.SetToken(loginResponse.Token);
}
else
{
return false;
}
return true;
}
catch
{
return false;
}
}
#endregion
}
}



+ 0
- 0
MaUI/Services/custom/_METTRE ICI LES AJOUTS CUSTOMS.txt Voir le fichier


+ 122
- 0
MaUI/ViewModel/AuthLogin/AuthLogin_VM.cs Voir le fichier

@@ -0,0 +1,122 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace MaUI;

public partial class AuthLogin_VM : INotifyPropertyChanged
{
#region Variables
private readonly ApiService _apiService;
#endregion

#region Propriétés

#region Commands
public ICommand ValiderCommand { get; set; }
public ICommand AnnulerCommand { get; }
#endregion

#region Element de saisie
private LoginModel _selectedItem = null!;
public LoginModel SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
OnPropertyChanged();
//MettreAJourSelection();
}
}
#endregion



#endregion

#region Constructeur
public AuthLogin_VM()
{
SelectedItem = new();

ValiderCommand = new Command(ExecuterValidation);
AnnulerCommand = new Command(ExecuterAnnulation);

_apiService = new ApiService();
}
#endregion

#region Méthodes actions
private async void ExecuterAnnulation()
{
if (Application.Current?.MainPage != null)
{
bool reponse = await Application.Current.MainPage.DisplayAlert(
"Confirmation",
"Voulez-vous vraiment annuler les modifications ?",
"Oui",
"Non");

if (reponse)
{
// Retour à la page précédente
//await Application.Current.MainPage.Navigation.PopAsync();
}
}
}

private async void ExecuterValidation()
{
if (Application.Current?.MainPage != null)
{
if (SelectedItem != null)
{
bool rep = false;
string msg = "";

rep = await _apiService.Connexion(SelectedItem);

if (!rep)
{
msg = "Votre connexion est refusée.";
await Application.Current.MainPage.DisplayAlert("Erreur", msg, "OK");
}
else
{
_selectedItem = new LoginModel();
await Application.Current.MainPage.Navigation.PushAsync(new MainPage());
}


}
}
}

#endregion

#region Implémentation de INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;

protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
if (propertyName is null)
return;

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}

#region Autres classes
public class LoginModel
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}

public class LoginResponse
{
public string Token { get; set; } = string.Empty;
}
#endregion

+ 160
- 0
MaUI/ViewModel/base/ChatRoom/ChatRoom_VM.cs Voir le fichier

@@ -0,0 +1,160 @@

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using System.Xml.Linq;


namespace MaUI;


public class ChatRoom_VM : BindableObject
{
// === PROPRIÉTÉS ===
private string _newMessage = "";
public string NewMessage
{
get => _newMessage;
set { _newMessage = value; OnPropertyChanged(); }
}

public ObservableCollection<Conversation> Conversations { get; } = new();
public ObservableCollection<ChatMessage> CurrentMessages { get; } = new();

private Conversation? _selectedConversation;
public Conversation? SelectedConversation
{
get => _selectedConversation;
set { _selectedConversation = value; OnPropertyChanged(); }
}

public ObservableCollection<string> SelectedDocumentsListe { get; } = new();

public List<ModelInfo> InstalledModels { get; } = new()
{
new ModelInfo { Name = "gemma3:12b" },
new ModelInfo { Name = "llama3.1:8b" }
};

private ModelInfo? _selectedModel;
public ModelInfo? SelectedModel
{
get => _selectedModel;
set { _selectedModel = value; OnPropertyChanged(); }
}

public bool IsApiExterne { get; set; }
public bool IsRAG { get; set; }
public bool IsGenerateImg { get; set; }
public bool IsWithAssistant { get; set; }
public bool IsCODER { get; set; }

// === COMMANDES ===
public ICommand DeleteConversationCommand { get; }
public ICommand NewConversationCommand { get; }
public ICommand AddDocumentsCommand { get; }
public ICommand ClearDocumentsCommand { get; }
public ICommand SendMessageCommand { get; }

private readonly ApiService _apiService;

// === CONSTRUCTEUR ===
public ChatRoom_VM()
{
DeleteConversationCommand = new Command(DeleteConversation);
NewConversationCommand = new Command(NewConversation);
AddDocumentsCommand = new Command(AddDocuments);
ClearDocumentsCommand = new Command(ClearDocuments);
SendMessageCommand = new Command(async () => await SendMessageAsync());

_apiService = new ApiService();
}

// === MÉTHODES ===
private async void DeleteConversation()
{
if (SelectedConversation != null)
{
Conversations.Remove(SelectedConversation);
SelectedConversation = null;
CurrentMessages.Clear();
await Application.Current.MainPage.DisplayAlert("Info", "Conversation supprimée", "OK");
}
}

private void NewConversation()
{
var conv = new Conversation { Title = $"Conversation {Conversations.Count + 1}" };
Conversations.Add(conv);
SelectedConversation = conv;
CurrentMessages.Clear();
}

private async void AddDocuments()
{
// Ici tu pourras brancher le FilePicker MAUI
var result = await FilePicker.Default.PickAsync();
if (result != null)
{
SelectedDocumentsListe.Add(result.FullPath);
}
}

private void ClearDocuments()
{
SelectedDocumentsListe.Clear();
}

private async Task SendMessageAsync()
{
if (string.IsNullOrWhiteSpace(NewMessage))
return;

var userMsg = new ChatMessage { Role = "user", Content = NewMessage };
CurrentMessages.Add(userMsg);
NewMessage = "";

try
{
// Appel API (à brancher avec ton ApiService)
var dto = new ChatRequest { Requete = userMsg.Content };
var response = await _apiService.EnvoiRequete<string>(
"api/ChatRoom/llm",
userMsg.Content
);

if (response != null)
{
var assistantMsg = new ChatMessage { Role = "assistant", Content = response };
CurrentMessages.Add(assistantMsg);
}
}
catch (Exception ex)
{
await Application.Current.MainPage.DisplayAlert("Erreur", ex.Message, "OK");
}
}
}

// === CLASSES D’APPUIS ===
public class Conversation
{
public string Title { get; set; } = "";
}

public class ChatMessage
{
public string Role { get; set; } = "";
public string Content { get; set; } = "";
}

public class ModelInfo
{
public string Name { get; set; } = "";
}

public class ChatRequest
{
public string Requete { get; set; } = "";
}

+ 167
- 0
MaUI/ViewModel/base/Mails/ListeEmails_VM.cs Voir le fichier

@@ -0,0 +1,167 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Input;

namespace MaUI
{
public partial class ListeEmails_VM : INotifyPropertyChanged
{
#region Variables
private readonly ApiService _apiService;
#endregion

#region Liste observable
public ObservableCollection<LOGS_DTO> Liste_Items { get; set; }
#endregion

#region Element sélectionnée
private LOGS_DTO? _selectedItem;
public LOGS_DTO? SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
OnPropertyChanged();
}
}
#endregion

#region Commands
public ICommand LoadCommand { get; }
public ICommand EditCommand { get; }
public ICommand DeleteCommand { get; }
public ICommand CreateCommand { get; }
#endregion

#region Constructeur
public ListeEmails_VM()
{
_apiService = new ApiService();
Liste_Items = new ObservableCollection<LOGS_DTO>();

// Initialisation des commandes
LoadCommand = new Command(async () => await LoadDataAsync());
EditCommand = new Command(async () => await EditSelectedItemAsync(), () => SelectedItem != null);
DeleteCommand = new Command(async () => await DeleteSelectedItemAsync(), () => SelectedItem != null);
CreateCommand = new Command(async () => await CreateNewItemAsync());

// Charger initialement les données
_ = LoadDataAsync();
}
#endregion

#region Méthodes
public async Task LoadDataAsync()
{
try
{
var items = await _apiService.GetAll_Async<LOGS_DTO>("api/LOGS_DTO");
Liste_Items.Clear();
foreach (var item in items)
{
Liste_Items.Add(item);
}
}
catch (Exception ex)
{
Console.WriteLine($"Erreur lors du chargement des données : {ex.Message}");
await Application.Current!.MainPage!.DisplayAlert(
"Erreur",
$"Erreur lors du chargement des données : {ex.Message}",
"OK");
}
}

public async Task EditSelectedItemAsync()
{
if (SelectedItem != null && Application.Current?.MainPage?.Navigation != null)
{
await Application.Current.MainPage.Navigation.PushAsync(new LOGS_Detail_Page(SelectedItem));
}
else
{
Console.WriteLine("Navigation ou SelectedItem est null.");
await Application.Current!.MainPage!.DisplayAlert(
"Erreur",
$"Navigation ou SelectedItem est null.",
"OK");
}
}

public async Task DeleteSelectedItemAsync()
{
if (SelectedItem != null && Application.Current?.MainPage != null)
{
bool confirm = await Application.Current.MainPage.DisplayAlert(
"Confirmation",
"Voulez-vous vraiment supprimer cet élément ?",
"Oui", "Non");

if (confirm)
{
try
{
var result = await _apiService.Delete_Async<Guid>($"api/LOGS_DTO/{SelectedItem.ID}");
if (result)
{
Liste_Items.Remove(SelectedItem);
await Application.Current.MainPage.DisplayAlert("Succès", "Suppression effectuée.", "OK");
}
else
{
await Application.Current.MainPage.DisplayAlert("Erreur", "Suppression non effectuée.", "OK");
}
}
catch (Exception ex)
{
Console.WriteLine($"Erreur lors de la suppression : {ex.Message}");
await Application.Current!.MainPage!.DisplayAlert(
"Erreur",
$"Erreur lors de la suppression : {ex.Message}",
"OK");
}
}
}
else
{
Console.WriteLine("MainPage or SelectedItem est null.");
await Application.Current!.MainPage!.DisplayAlert(
"Erreur",
$"MainPage or SelectedItem est null.",
"OK");
}
}

public async Task CreateNewItemAsync()
{
if (Application.Current?.MainPage?.Navigation != null)
{
await Application.Current.MainPage.Navigation.PushAsync(new LOGS_Detail_Page(null));
}
else
{
Console.WriteLine("Navigation est null.");
await Application.Current!.MainPage!.DisplayAlert(
"Erreur",
$"Navigation est null",
"OK");
}
}
#endregion

#region Implémentation de INotifyPropertyChanged pour notifier les changements
public event PropertyChangedEventHandler? PropertyChanged = delegate { };

protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}



+ 0
- 0
MaUI/ViewModel/custom/_METTRE ICI LES AJOUTS CUSTOMS.txt Voir le fichier


+ 46
- 0
OllamaModels/OllamaModels.cs Voir le fichier

@@ -0,0 +1,46 @@
using System.Text.Json.Serialization;

namespace Services.Models
{
public class OllamaModel
{
[JsonPropertyName("name")]
public string Name { get; set; } = "";

[JsonPropertyName("model")]
public string Model { get; set; } = "";

[JsonPropertyName("modified_at")]
public DateTime Modified_At { get; set; }

[JsonPropertyName("size")]
public long Size { get; set; }

[JsonPropertyName("digest")]
public string Digest { get; set; } = "";

[JsonPropertyName("details")]
public OllamaModelDetails Details { get; set; } = new();
}

public class OllamaModelDetails
{
[JsonPropertyName("parent_model")]
public string Parent_Model { get; set; } = "";

[JsonPropertyName("format")]
public string Format { get; set; } = "";

[JsonPropertyName("family")]
public string Family { get; set; } = "";

[JsonPropertyName("families")]
public List<string> Families { get; set; } = new();

[JsonPropertyName("parameter_size")]
public string ParameterSize { get; set; } = "";

[JsonPropertyName("quantization_level")]
public string QuantizationLevel { get; set; } = "";
}
}

+ 9
- 0
OllamaModels/OllamaModels.csproj Voir le fichier

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>

+ 35
- 0
OllamaModels/OllamaRequest.cs Voir le fichier

@@ -0,0 +1,35 @@
using System.Text.Json.Serialization;

namespace OllamaService.Models
{
public class OllamaRequest
{
[JsonPropertyName("model")]
public string Model { get; set; } = "";

[JsonPropertyName("prompt")]
public string Prompt { get; set; } = "";

[JsonPropertyName("images")]
public List<string> Images { get; set; } = new();

[JsonPropertyName("stream")]
public bool Stream { get; set; } = false;

[JsonPropertyName("temperature")]
public double Temperature { get; set; } = 0.01;

[JsonPropertyName("top_p")]
public double Top_p { get; set; } = 0.9;

[JsonPropertyName("max_tokens")]
public int Max_tokens { get; set; } = 400;

}

public class OllamaResponse
{
[JsonPropertyName("response")]
public string Completion { get; set; } = "";
}
}

+ 20
- 0
OllamaService/OllamaService.csproj Voir le fichier

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\ChatConversationStructure\ChatConversationStructure.csproj" />
<ProjectReference Include="..\OllamaModels\OllamaModels.csproj" />
<ProjectReference Include="..\ToolsService\ToolsService.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Models\" />
</ItemGroup>

</Project>

+ 56
- 0
OllamaService/Services/EmbeddingService.cs Voir le fichier

@@ -0,0 +1,56 @@
using System.Text;
using System.Text.Json;
using ToolsServices;

namespace Services.Ollama
{
public static class EmbeddingService
{
public static async Task<(bool, float[]?)> GetEmbeddingAsync(string text, bool isOnDemand)
{
LoggerService.LogDebug("EmbeddingService.GetEmbeddingAsync");
try
{
ParametresOllamaService? ollama = OllamaService.LoadParametres();
if (ollama == null)
{
var msg = "Erreur de chargement des paramètres Ollama.";
LoggerService.LogWarning(msg);
return (false, null);
}
var url = "";
if (isOnDemand)
url = $"{ollama.Ollama_URL}/api/embeddings";
else
url = $"{ollama.Ollama_Batch_URL}/api/embeddings";

var request = new
{
model = "nomic-embed-text",
prompt = text,
stream = false
};
HttpClient httpClient = new();
var json = JsonSerializer.Serialize(request);
var response = await httpClient.PostAsync(url,
new StringContent(json, Encoding.UTF8, "application/json"));

response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync();

using var doc = JsonDocument.Parse(result);
var retour = doc.RootElement
.GetProperty("embedding")
.EnumerateArray()
.Select(e => e.GetSingle())
.ToArray();
return (true, retour);
}
catch (Exception ex)
{
LoggerService.LogError($"Erreur lors de la récupération de l'embedding : {ex.Message}");
return (false, null);
}
}
}
}

+ 167
- 0
OllamaService/Services/ModelSelector.cs Voir le fichier

@@ -0,0 +1,167 @@
using System.Text.Json;
using ToolsServices;

namespace Services.Ollama
{
#region Classes annexes
public class ModelConfig
{
public string Primary { get; set; } = "";
public string Secondary { get; set; } = "";
public string Fallback { get; set; } = "";
}

public class ModelsConfiguration
{
public Dictionary<string, ModelConfig> Config { get; set; } = new();
}
#endregion

#region Classe principale ModelSelector
public class ModelSelector
{
#region Variables
private readonly ModelsConfiguration _Models = new();
private string ConfigPath = FichiersInternesService.ParamsModeles;
#endregion

#region Constructeur
public ModelSelector()
{
if (!File.Exists(ConfigPath))
{
CreateConfig();
}

var json = File.ReadAllText(ConfigPath);
_Models = JsonSerializer.Deserialize<ModelsConfiguration>(
json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
) ?? new ModelsConfiguration();
}
#endregion

#region Méthodes publiques
public bool SaveConfig(string useCase, ModelConfig models)
{
LoggerService.LogInfo($"ModelSelector.SaveConfig pour le cas d'usage '{useCase}'");
try
{
if (_Models.Config.ContainsKey(useCase))
{

_Models.Config[useCase].Primary = models.Primary;
_Models.Config[useCase].Secondary = models.Secondary;
_Models.Config[useCase].Fallback = models.Fallback;

}
else
{
_Models.Config[useCase] = new ModelConfig
{
Primary = models.Primary,
Secondary = models.Secondary,
Fallback = models.Fallback
};
}

ModelsConfiguration config = new ModelsConfiguration
{
Config = _Models.Config
};

var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(ConfigPath, json);
return true;
}
catch (Exception ex)
{
LoggerService.LogError($"Échec de la sauvegarde de la configuration des modèles du cas d'usage {useCase} : {ex.Message}");
return false;
}
}

public IEnumerable<string> GetModelsForUseCase(string useCase)
{
LoggerService.LogInfo($"ModelSelector.GetModelsForUseCase pour le cas d'usage : '{useCase}'");
try
{
if (!_Models.Config.TryGetValue(useCase, out var config))
throw new Exception($"Cas d'usage '{useCase}' introuvable dans la config.");

var allModels = new List<string>();

if (config.Primary != null && config.Primary != "")
allModels.Add(config.Primary);

if (config.Secondary != null && config.Secondary != "")
allModels.Add(config.Secondary);

if (config.Fallback != null && config.Fallback != "")
allModels.Add(config.Fallback);

return allModels;
}
catch (Exception ex)
{
LoggerService.LogError($"GetModelsForUseCase: {ex.Message}");
return new List<string>();
}
}
#endregion

#region Méthodes privées
private void CreateConfig()
{
var txt = """
{
"Config": {
"LLM": {
"Primary": "thewindmom/hermes-3-llama-3.1-8b:latest",
"Secondary": "gemma3:12b",
"Fallback": "deepseek-coder-v2:16b"
},
"RAG": {
"Primary": "gemma3:12b-it-qat",
"Secondary": "llama3-chatqa:8b",
"Fallback": "phi4-mini:latest"
},
"Resume_Documents": {
"Primary": "gemma3:12b-it-qat",
"Secondary": "llama3-chatqa:8b",
"Fallback": "phi4:latest"
},
"Interpretation_Images": {
"Primary": "llava-llama3:latest",
"Secondary": "llava:7b",
"Fallback": ""
},
"Prompt_Generation_Fooocus": {
"Primary": "openchat:latest",
"Secondary": "gemma3:12b",
"Fallback": "phi4-mini:latest"
},
"Analyse_Mails": {
"Primary": "thewindmom/hermes-3-llama-3.1-8b:latest",
"Secondary": "gemma3:12b-it-qat",
"Fallback": "phi4:latest"
},
"Analyse_CV_Mission": {
"Primary": "phi4:latest",
"Secondary": "gemma3:27b",
"Fallback": "llama3-chatqa:8b"
},
"Pdf_To_Xml": {
"Primary": "gemma3:12b-it-qat",
"Secondary": "phi4-mini:latest",
"Fallback": ""
}
}
}
""";
TxtService.CreateTextFile(ConfigPath, txt);
}
#endregion
}
#endregion
}

+ 721
- 0
OllamaService/Services/OllamaService.cs Voir le fichier

@@ -0,0 +1,721 @@
using Services.Models;
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using ToolsServices;
using OllamaService.Models;

namespace Services.Ollama
{
public static class OllamaService
{
#region Variables
private static HttpClient? _HttpClient;
private static readonly string NomFichierData = FichiersInternesService.ParamsOllama;// "paramsOllama.txt";
private static List<string> ImagesFullFilename = new();
#endregion

#region Méthodes publiques

#region InterActions
/// <summary>
/// Vérifie si Ollama est actif
/// </summary>
/// <param name="isOnDemand"></param>
/// <returns></returns>
public static async Task<bool> IsOllamaActif(bool isOnDemand)
{
var precisionContext = isOnDemand ? "OnDemand" : "Batch";
LoggerService.LogInfo($"OllamaService.IsOllamaActif {precisionContext}");
try
{
ParametresOllamaService? ollama = LoadParametres();
if (ollama == null)
{
var msg = "Erreur de chargement des paramètres Ollama.";
LoggerService.LogWarning(msg);
return false;
}
var url = "";
if(isOnDemand)
url = $"{ollama.Ollama_URL}/api/version";
else
url = $"{ollama.Ollama_Batch_URL}/api/version";
_HttpClient = new HttpClient();

_HttpClient.Timeout = TimeSpan.FromSeconds(30);
var response = await _HttpClient.GetAsync(url);
LoggerService.LogDebug($"OllamaService.IsOllamaActif {precisionContext} : {response.IsSuccessStatusCode}");
return response.IsSuccessStatusCode;
}
catch(Exception ex)
{
LoggerService.LogError($"OllamaService.IsOllamaActif {precisionContext} : False --> {ex.Message}");
return false;
}
}

/// <summary>
/// Appelle l'api Generate de Ollama
/// </summary>
/// <param name="prompt"></param>
/// <param name="precision"></param>
/// <param name="model"></param>
/// <param name="isOnDemand"></param>
/// <returns></returns>
public static async Task<(bool, string)> GenererAsync(string prompt, string precision, string model, bool isOnDemand)
{
var precisionContext = isOnDemand ? "OnDemand" : "Batch";
LoggerService.LogInfo($"OllamaService.GenererAsync {precisionContext} : {model}");
string? sReturn = "";
Stopwatch chrono = new Stopwatch();
chrono.Start();
bool isSuccess = true;
ParametresOllamaService? ollama = LoadParametres();
if (ollama == null)
{
var msg = "Erreur de chargement des paramètres Ollama.";
LoggerService.LogWarning(msg);
return (false, msg);
}

try
{
//prompt = "Donne tes réponses dans la langue Française." + "\n" + prompt;
prompt = PromptService.GetPrompt(PromptService.ePrompt.OllamaService_PromptSystem) + "\n" + prompt;
var requestBody = new
{
model = model,
prompt = TruncatePromptBySentenceAsync(prompt),
temperature = isOnDemand ? ollama.Ollama_Temperature : ollama.Ollama_Batch_Temperature, // → 0 : réponses plus précises et conservatrices ; 1 : plus créatives
top_p = isOnDemand ? ollama.Ollama_Top_p : ollama.Ollama_Batch_Top_p, // → on considère uniquement les mots parmi les plus probables (limite l’invention)
max_tokens = isOnDemand ? ollama.Ollama_Max_tokens : ollama.Ollama_Batch_Max_tokens, // → pour ne pas générer des réponses trop longues
stream = false
};
var url = isOnDemand ? ollama.Ollama_URL : ollama.Ollama_Batch_URL;
int timeOut = isOnDemand ? ollama.Ollama_TimeOut : ollama.Ollama_Batch_TimeOut;

url = $"{url}/api/generate";

LoggerService.LogDebug($"Génération Ollama : {precision} - Model : {model} - URL : {url} - Time-out : {timeOut}");
LoggerService.LogDebug($"Prompt : {requestBody.prompt}");

_HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(timeOut) };

var response = await _HttpClient.PostAsJsonAsync($"{url}", requestBody);
if (!response.IsSuccessStatusCode)
{
var err = await response.Content.ReadAsStringAsync();
var msg = $"Erreur Ollama({response.StatusCode}) : {err}";
LoggerService.LogWarning(msg);
return (false, $"{msg}");
}

var json = await response.Content.ReadFromJsonAsync<JsonElement>();
sReturn = json!.GetProperty("response")!.GetString();
if (sReturn != null)
{
sReturn = EpureReponse(sReturn);
}

LoggerService.LogDebug($"OllamaService.GenerateAsync : {sReturn}");
}
catch (HttpRequestException ex)
{
isSuccess = false;
LoggerService.LogError("Erreur HTTP : " + ex.Message);
}
catch (OperationCanceledException)
{
isSuccess = false;
LoggerService.LogError($"La requête a été annulée après {ollama.Ollama_TimeOut} secondes.");
}
catch (Exception ex)
{
isSuccess = false;
sReturn = $"Erreur lors de la génération : {ex.Message}";
LoggerService.LogError(sReturn);
//return $"{msg}";
}
finally
{
chrono.Stop();
LoggerService.LogInfo($"Temps de la génération : {chrono.ElapsedMilliseconds / 1000} s ({precision})");

}
return (isSuccess,$"{sReturn}");
}

/// <summary>
/// Appelle l'api Chat de Ollama
/// </summary>
/// <param name="model"></param>
/// <param name="messages"></param>
/// <returns></returns>
public static async Task<(bool, string)> ChatAsync(string model, List<ChatMessage> messages)
{
LoggerService.LogInfo($"OllamaService.SendMessageAsync : {model}");
Stopwatch chrono = new Stopwatch();
chrono.Start();
string? sReturn = "";
bool isSuccess = true;
ParametresOllamaService? ollama = LoadParametres();
if (ollama == null)
{
var msg = "Erreur de chargement des paramètres Ollama.";
LoggerService.LogWarning(msg);
return (false, msg);
}

try
{
var promptSystem = PromptService.GetPrompt(PromptService.ePrompt.OllamaService_PromptSystem);
var systemPrompt = new ChatMessage { Role = "system", Content = promptSystem, Model = model };
var allMessages = new List<ChatMessage> { systemPrompt };

messages[0].Content = promptSystem + "\n" + messages[0].Content;
allMessages.AddRange(messages);

var request = new
{
model = model,
messages = allMessages.Select(m => new { role = m.Role, content = m.Content }).ToList(),
temperature = ollama.Ollama_Temperature, // → 0 : réponses plus précises et conservatrices ; 1 : plus créatives
top_p = ollama.Ollama_Top_p, // → on considère uniquement les mots parmi les plus probables (limite l’invention)
max_tokens = ollama.Ollama_Max_tokens, // → pour ne pas générer des réponses trop longues
stream = false
};

var url = $"{ollama.Ollama_URL}/api/chat";
LoggerService.LogDebug($"Génération Ollama : CHAT - Model : {model} - URL : {url} - Time-out : {ollama.Ollama_TimeOut}");
LoggerService.LogDebug($"Prompt : {request.messages[1].content}");

_HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ollama.Ollama_TimeOut) };

var response = await _HttpClient.PostAsync(url, new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"));

if (!response.IsSuccessStatusCode)
{
var err = await response.Content.ReadAsStringAsync();
var msg = $"Erreur Ollama({response.StatusCode}) : {err}";
LoggerService.LogWarning(msg);
return (false, $"{msg}");
}

var result = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(result);
sReturn = doc.RootElement.GetProperty("message").GetProperty("content").GetString();
if (sReturn != null)
{
sReturn = EpureReponse(sReturn);
}
LoggerService.LogDebug($"OllamaService.SendMessageAsync : {sReturn}");

}

catch (Exception ex)
{
isSuccess = false;
sReturn = $"Erreur lors de la génération : {ex.Message}";
LoggerService.LogError(sReturn);
}

finally
{
chrono.Stop();
LoggerService.LogInfo($"Temps de la génération : {chrono.ElapsedMilliseconds / 1000} s (LLM)");

if (sReturn == null)
sReturn = "";
}
return (isSuccess, $"{sReturn}");
}

/// <summary>
/// Appelle l'api Generate de Ollama avec envoi d'images dans le prompt
/// </summary>
/// <param name="model"></param>
/// <param name="prompt"></param>
/// <param name="imagesFullFilename"></param>
/// <returns></returns>
public static async Task<(bool, string)> GenererAsync(string model, string prompt, List<string> imagesFullFilename)
{
LoggerService.LogInfo($"OllamaService.SendMessageAsync : {model}");
Stopwatch chrono = new Stopwatch();
chrono.Start();
bool isSuccess = true;
var imagesBase64 = new List<string>();

ParametresOllamaService? ollama = LoadParametres();
if (ollama == null)
{
var msg = "Erreur de chargement des paramètres Ollama.";
LoggerService.LogWarning(msg);
return (false, msg);
}

foreach (var uneImage in imagesFullFilename)
{
byte[] imageBytes = File.ReadAllBytes(uneImage);
imagesBase64.Add(Convert.ToBase64String(imageBytes));
}
var request = new OllamaRequest
{
Model = model,
Prompt = PromptService.GetPrompt(PromptService.ePrompt.OllamaService_PromptSystem) + "\n" + prompt,
Temperature = ollama.Ollama_Temperature, // → 0 : réponses plus précises et conservatrices ; 1 : plus créatives
Top_p = ollama.Ollama_Top_p, // → on considère uniquement les mots parmi les plus probables (limite l’invention)
Max_tokens = ollama.Ollama_Max_tokens, // → pour ne pas générer des réponses trop longues
Images = imagesBase64,
Stream = false
};

string? sReturn = "";


try
{

var url = $"{ollama.Ollama_URL}/api/generate";
LoggerService.LogDebug($"Prompt envoyé à {url}");
_HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ollama.Ollama_TimeOut) };

var response = await _HttpClient.PostAsJsonAsync(url, request);

if (!response.IsSuccessStatusCode)
{
var err = await response.Content.ReadAsStringAsync();
var msg = $"Erreur Ollama({response.StatusCode}) : {err}";
LoggerService.LogWarning(msg);
return (false, $"{msg}");
}

var result = await response.Content.ReadFromJsonAsync<OllamaResponse>();
if (result != null)
{
sReturn = result.Completion.Trim();
sReturn = EpureReponse(sReturn);
}
LoggerService.LogDebug($"OllamaService.SendMessageAsync : {sReturn}");

}
catch (Exception ex)
{
isSuccess=false;
sReturn = $"Erreur lors de la génération : {ex.Message}";
LoggerService.LogError(sReturn);
//return sReturn;
}
finally
{
chrono.Stop();
LoggerService.LogInfo($"Temps de la génération : {chrono.ElapsedMilliseconds / 1000} s (Interprétation images)");

if (sReturn == null)
sReturn = "";
}
return (isSuccess, $"{sReturn}");
}

#endregion

#region Gestion des paramètres
public static ParametresOllamaService? LoadParametres()
{
LoggerService.LogInfo("OllamaService.LoadParametres");
ParametresOllamaService SelectedItem = new();

try
{
if (File.Exists(NomFichierData))
{

string[] lignes = File.ReadAllLines(NomFichierData);

if (lignes.Length > 0)
{
SelectedItem.Ollama_URL = lignes[0];
}
if (lignes.Length > 1)
{
if(int.TryParse(lignes[1], out int timeout))
SelectedItem.Ollama_TimeOut = timeout;
}
if (lignes.Length > 2)
{
if (double.TryParse(lignes[2], out double temperature))
SelectedItem.Ollama_Temperature = temperature;
}
if (lignes.Length > 3)
{
if (double.TryParse(lignes[3], out double top_p))
SelectedItem.Ollama_Top_p = top_p;
}
if (lignes.Length > 4)
{
if (int.TryParse(lignes[4], out int max_tokens))
SelectedItem.Ollama_Max_tokens = max_tokens;
}


if (lignes.Length > 5)
{
SelectedItem.Ollama_Batch_URL = lignes[5];
}
if (lignes.Length > 6)
{
if (int.TryParse(lignes[6], out int timeout))
SelectedItem.Ollama_Batch_TimeOut = timeout;
}
if (lignes.Length > 7)
{
if (double.TryParse(lignes[7], out double temperature))
SelectedItem.Ollama_Batch_Temperature = temperature;
}
if (lignes.Length > 8)
{
if (double.TryParse(lignes[8], out double top_p))
SelectedItem.Ollama_Batch_Top_p = top_p;
}
if (lignes.Length > 9)
{
if (int.TryParse(lignes[9], out int max_tokens))
SelectedItem.Ollama_Batch_Max_tokens = max_tokens;
}

}
return SelectedItem;

}
catch
{
return null;
}
}

public static bool SaveParametres(ParametresOllamaService selectedItem)
{
LoggerService.LogInfo("OllamaService.SaveParametres");
try
{
StringBuilder sb = new();
sb.AppendLine(selectedItem.Ollama_URL);
sb.AppendLine(selectedItem.Ollama_TimeOut.ToString());
sb.AppendLine(selectedItem.Ollama_Temperature.ToString());
sb.AppendLine(selectedItem.Ollama_Top_p.ToString());
sb.AppendLine(selectedItem.Ollama_Max_tokens.ToString());

sb.AppendLine(selectedItem.Ollama_Batch_URL);
sb.AppendLine(selectedItem.Ollama_Batch_TimeOut.ToString());
sb.AppendLine(selectedItem.Ollama_Batch_Temperature.ToString());
sb.AppendLine(selectedItem.Ollama_Batch_Top_p.ToString());
sb.AppendLine(selectedItem.Ollama_Batch_Max_tokens.ToString());

File.WriteAllText(NomFichierData, sb.ToString());

return true;
}
catch
{
return false;
}

}

public static async Task<List<OllamaModel>> GetInstalledModelsAsync(bool isOnDemand)
{
LoggerService.LogInfo("OllamaService.GetInstalledModelsAsync");
try
{
ParametresOllamaService? ollama = LoadParametres();
if (ollama == null)
{
return new List<OllamaModel> { new OllamaModel { Name = "Erreur de chargement des paramètres Ollama." } };
}

var url = "";
int timeOut = 360;
if (isOnDemand)
{
url = $"{ollama.Ollama_URL}/api/tags";
timeOut = ollama.Ollama_TimeOut;
}
else
{
url = $"{ollama.Ollama_Batch_URL}/api/tags";
timeOut = ollama.Ollama_Batch_TimeOut;
}
_HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(timeOut) };

var response = await _HttpClient.GetAsync($"{url}");
LoggerService.LogDebug($"Requête envoyée à {url}");
response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = false };
var result = JsonSerializer.Deserialize<OllamaTagsResponse>(json, options);

if (result?.Models == null)
{
return new List<OllamaModel> { new OllamaModel { Name = "Erreur de chargement des paramètres Ollama." } };
}

return result.Models;
}
catch (Exception ex)
{
LoggerService.LogError($"Erreur lors de la récupération des modèles installés : {ex.Message}");
return new List<OllamaModel> { new OllamaModel { Name = $"Erreur : {ex.Message}" } };
}
}

public static string GetModeleIA(string useCase)
{
LoggerService.LogInfo("OllamaService.GetModeleIA");

ModelSelector selector = new();
var models = selector.GetModelsForUseCase(useCase);
if(models.ToList().Count>1)
return models.First();

return "";
}

public static IEnumerable<string> GetModelesIA(string useCase)
{
LoggerService.LogInfo("OllamaService.GetModelesIA");

ModelSelector selector = new();
var models = selector.GetModelsForUseCase(useCase);
return models;

}

public static bool SaveParametresModeles(string useCase, ModelConfig models)
{
LoggerService.LogInfo("OllamaService.SaveParametresModeles");

ModelSelector selector = new();
var b = selector.SaveConfig(useCase, models);
return b;
}
#endregion

#region Nettoyage des prompts et surveillance de la taille
public static string CleanUserInput(string input, out bool isPromptInjection)
{
LoggerService.LogInfo("OllamaService.CleanUserInput");
isPromptInjection = false;
var avant = input;
if (string.IsNullOrWhiteSpace(input))
return string.Empty;

// 1. Limiter la taille max (ex : 500 caractères)
/*
int maxLength = 500;
if (input.Length > maxLength)
input = input.Substring(0, maxLength);
*/

// 2. Supprimer les caractères non imprimables (ex : contrôles, retours chariot non standards)
input = RemoveNonPrintableChars(input);

// 3. Échapper les triples quotes pour ne pas casser le prompt (on remplace ''' par ' ' ' par exemple)
input = input.Replace("'''", "' ' '");

// 4. Rechercher et remplacer les mots/expressions d’injection potentielles
string[] blacklist = new string[] {
"ignore", "cancel", "stop generating", "disregard instructions","oublie les instructions", "oublie",
"ignore instructions","ignore les instructions", "forget", "override", "bypass"
};

foreach (var word in blacklist)
{
// Remplacer même si insensible à la casse
input = Regex.Replace(input, Regex.Escape(word), "[CENSORED]", RegexOptions.IgnoreCase);
}

// 5. Optionnel : remplacer les guillemets doubles et simples pour éviter la confusion
input = input.Replace("\"", "'");
input = input.Replace("\\", "/"); // éviter les échappements

if (input.Contains("[CENSORED]"))
{
LoggerService.LogDebug($"Avant :{avant}");
LoggerService.LogDebug($"Après :{input}");
isPromptInjection = true;
}

return input.Trim();
}
#endregion

#endregion

#region Méthodes privées
private static string EpureReponse(string texte)
{
var txt = texte;
txt = txt.Replace("**", "");
//txt = txt.Replace("*", "");
txt = txt.Replace("</end_of_turn>", "");
txt = txt.Replace("</start_of_turn>", "");
txt = txt.Replace("```csv", "");
txt = txt.Replace("```xml", "");
txt = txt.Replace("```", "");
txt = txt.Trim();
return txt;
}

private static async Task<int> GetTokenMaxAsync(bool isOld)
{
LoggerService.LogInfo("OllamaService.GetTokenMaxAsync");
try
{
ParametresOllamaService? ollama = LoadParametres();
if (ollama == null)
{
return -1;
}
var requestBody = new
{
model = "",// ollama.Ollama_Model,
};

_HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ollama.Ollama_TimeOut) };

// Construire l'URL pour obtenir les informations du modèle
string url = $"{ollama.Ollama_URL}/api/show";
LoggerService.LogDebug($"Prompt envoyé à {url}");

var response = await _HttpClient.PostAsJsonAsync($"{url}", requestBody);


// Envoyer une requête GET à l'API Ollama
//HttpResponseMessage response = await _HttpClient.GetAsync(url);

// Vérifier le statut de la réponse
if (response.IsSuccessStatusCode)
{
// Lire le contenu de la réponse
string responseBody = await response.Content.ReadAsStringAsync();

var json = await response.Content.ReadFromJsonAsync<JsonElement>();
var contextLength = json
.GetProperty("model_info")
.GetProperty("llama.context_length")
.GetInt32();

return contextLength;

//return maxContextSize.HasValue ? (int)maxContextSize : 0;
}
else
{
var model = "";//ollama.Ollama_Model
LoggerService.LogError($"Erreur lors de la récupération des informations du modèle {model}: {response.StatusCode}");
return -1;
}
}
catch (Exception ex)
{
LoggerService.LogError($"Erreur lors de la récupération du modèle : {ex.Message}");
return -3; // Erreur lors de la récupération du modèle
}
}

// Supprime les caractères non imprimables
private static string RemoveNonPrintableChars(string text)
{
LoggerService.LogInfo("OllamaService.RemoveNonPrintableChars");
var sb = new StringBuilder();
foreach (char c in text)
{
// Exclure uniquement les caractères de contrôle ASCII (< 32), sauf les classiques utiles
if (!char.IsControl(c) || c == '\n' || c == '\r' || c == '\t')
{
sb.Append(c);
}
}
return sb.ToString();
}

private static int GetTokenMaxAsync()
{
LoggerService.LogInfo("OllamaService.GetTokenMaxAsync");
try
{
//await Task.Delay(1);
return 4096;
}
catch (Exception ex)
{
LoggerService.LogError($"Erreur lors de la récupération du modèle : {ex.Message}");
return -3; // Erreur lors de la récupération du modèle
}
}

private static int CountTokensAsync(string prompt)
{
LoggerService.LogInfo("OllamaService.CountTokensAsync");
try
{
// 1 token, en langue francaise, c'est 1.25 mots (environ)
int estimatedTokens = prompt.Split(' ').Length * 5 / 4;
return estimatedTokens;
}
catch (Exception ex)
{
LoggerService.LogError($"Erreur lors de la récupération du modèle : {ex.Message}");
return -1; // Erreur lors de la récupération du modèle
}
}

private static string TruncatePromptBySentenceAsync(string prompt)
{
LoggerService.LogInfo("OllamaService.TruncatePromptBySentenceAsync");
prompt = prompt.Trim();
LoggerService.LogDebug($"Taille avant : {prompt.Length}");

int reserveReponse = 512 * 3; // Réserve pour la réponse
var maxTokens = GetTokenMaxAsync();
var nbrTokens = CountTokensAsync(prompt);
// Cas simple : le prompt tient dans la limite
// On garde 512 * 3 de réserve pour la réponse
if ((nbrTokens + reserveReponse) <= maxTokens)
{
LoggerService.LogDebug($"Taille après : {prompt.Length}");
return prompt;
}

// Découpe le texte en phrases (naïvement en utilisant les points)
var sentences = prompt.Split(new[] { ". ", "? ", "! " }, StringSplitOptions.RemoveEmptyEntries);

StringBuilder builder = new();
foreach (var sentence in sentences)
{
var next = builder.Length > 0 ? builder + ". " + sentence.Trim() : sentence.Trim();

var tokenCount = CountTokensAsync(next.ToString());
if ((tokenCount + reserveReponse) > maxTokens)
{
break; // Trop long, on s'arrête
}

builder.Clear();
builder.Append(next);
}

var rep = builder.ToString().Trim();
LoggerService.LogDebug($"Taille après : {rep.Length}");
return rep;// + "\n\n[Tronqué pour respecter la limite de contexte]";
}
#endregion
}
}

+ 11
- 0
OllamaService/Services/OllamaTagsResponse.cs Voir le fichier

@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
using Services.Models;

namespace Services.Ollama
{
public class OllamaTagsResponse
{
[JsonPropertyName("models")]
public List<OllamaModel>? Models { get; set; }
}
}

+ 17
- 0
OllamaService/Services/ParametresOllamaService.cs Voir le fichier

@@ -0,0 +1,17 @@
namespace Services.Ollama
{
public class ParametresOllamaService
{
public string Ollama_URL { get; set; } = "http://localhost:11434";
public int Ollama_TimeOut { get; set; } = 360;
public double Ollama_Temperature { get; set; } = 0.01;
public double Ollama_Top_p { get; set; } = 0.9;
public int Ollama_Max_tokens { get; set; } = 200;

public string Ollama_Batch_URL { get; set; } = "http://localhost:11435";
public int Ollama_Batch_TimeOut { get; set; } = 360;
public double Ollama_Batch_Temperature { get; set; } = 0.01;
public double Ollama_Batch_Top_p { get; set; } = 0.9;
public int Ollama_Batch_Max_tokens { get; set; } = 200;
}
}

BIN
Prompt et longueur de prompt.docx Voir le fichier


+ 31
- 0
RAGService/RAGService.csproj Voir le fichier

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.18.0" />
<PackageReference Include="Qdrant.Client" Version="1.15.1" />
<PackageReference Include="System.Text.Json" Version="10.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" />
<ProjectReference Include="..\ToolsService\ToolsService.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="onnx\model.onnx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="onnx\tokenizer.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>


</Project>

+ 8
- 0
RAGService/Services/ParametresQdrantService.cs Voir le fichier

@@ -0,0 +1,8 @@
namespace Services
{
public class ParametresQdrantService
{
public string Qdrant_URL { get; set; } = "localhost:6334";
public bool Qdrant_IsHttps { get; set; } = false;
}
}

+ 8
- 0
RAGService/Services/ParametresRAGService.cs Voir le fichier

@@ -0,0 +1,8 @@
namespace Services
{
public class ParametresRAGService
{
public string RAG_Path_ToVectoralize { get; set; } = "";
public string RAG_Path_Vectoralized { get; set; } = "";
}
}

+ 449
- 0
RAGService/Services/QdrantService.cs Voir le fichier

@@ -0,0 +1,449 @@
using Models;
using Qdrant.Client.Grpc;
using System.Collections;
using System.ComponentModel.DataAnnotations;
using System.Numerics;
using System.Text;
using ToolsServices;


namespace Services;

#region enum des type de documents
public enum Domain
{
RH,
Juridique,
Global,
Emails,
CV,
Technique
}
#endregion

#region Classe statique d'extension pour Domain
public static class DomainExtensions
{
private static readonly string _CollectionRH = "RH";
private static readonly string _CollectionJuridique = "Juridique";
private static readonly string _CollectionGlobal = "Global";
private static readonly string _CollectionEmails = "Emails";
private static readonly string _CollectionCV = "CV";
private static readonly string _CollectionTechnique = "Technique";

public static List<string> CollectionsName = GetCollectionsName();
public static List<string> CollectionsNameRestricted = GetCollectionsNameRestricted(false);

private static List<string> GetCollectionsName()
{
List<string> strings = new()
{
_CollectionRH,
_CollectionJuridique,
_CollectionGlobal,
_CollectionEmails,
_CollectionCV,
_CollectionTechnique
};
return strings;
}

private static List<string> GetCollectionsNameRestricted(bool isVeryRestricted)
{
List<string> strings = new();
strings.Add(_CollectionGlobal);
strings.Add(_CollectionTechnique);
if (!isVeryRestricted)
{
strings.Add(_CollectionJuridique);
strings.Add(_CollectionRH);
}
return strings;
}

/// <summary>
/// Retourne le nom de collection Qdrant associé à un domaine.
/// </summary>
public static string ToCollectionName(this Domain domain) => domain switch
{
Domain.RH => _CollectionRH,
Domain.Juridique => _CollectionJuridique,
Domain.Global => _CollectionGlobal,
Domain.Emails => _CollectionEmails,
Domain.CV => _CollectionCV,
Domain.Technique => _CollectionTechnique,
_ => throw new ArgumentOutOfRangeException(nameof(domain), domain, null)
};

/// <summary>
/// Retourne le nom de collection Qdrant associé à un domaine.
/// </summary>
public static Domain GetDomainByCollectionName(string CollectionName)
{
Domain domain = Domain.Global;

switch (CollectionName)
{
case "RH":
domain = Domain.RH;
break;
case "Juridique":
domain = Domain.Juridique;
break;
case "Global":
domain = Domain.Global;
break;
case "Emails":
domain = Domain.Emails;
break;
case "CV":
domain = Domain.CV;
break;
case "Technique":
domain = Domain.Technique;
break;
}
return domain;
}

public static Domain GetDomainByPath(string filePath)
{
var dirs = Path.GetDirectoryName(filePath)?
.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar,
StringSplitOptions.RemoveEmptyEntries);

if (dirs == null)
return Domain.Global;

var collections = GetCollectionsName();

// On cherche la première correspondance
var collectionName = dirs.FirstOrDefault(d =>
collections.Any(c => string.Equals(c, d, StringComparison.OrdinalIgnoreCase)));

return GetDomainByCollectionName(collectionName ?? "Global");
}


}
#endregion

#region Classe QdrantService
/// <summary>
/// http://localhost:6333/dashboard
/// </summary>
public class QdrantService
{

#region Variables
private static readonly string NomFichierData = FichiersInternesService.ParamsQdrant;// "paramsOllama.txt";

private readonly Qdrant.Client.QdrantClient _client;
private const int EmbeddingDim = 768;
private const Distance DefaultDistance = Distance.Cosine;
#endregion

#region Constructeur
public QdrantService()
{
var parametres = LoadParametres();
//_client = new Qdrant.Client.QdrantClient(parametres!.Qdrant_URL, parametres!.Qdrant_Port, parametres!.Qdrant_IsHttps);
var httpString = parametres!.Qdrant_IsHttps ? "https://" : "http://";
_client = new Qdrant.Client.QdrantClient(new Uri($"{httpString}{parametres!.Qdrant_URL}"));
}
#endregion

#region Méthodes publiques

#region Gestion des Paramètres
public static ParametresQdrantService? LoadParametres()
{
ParametresQdrantService? item = new();
LoggerService.LogInfo("QdrantService.LoadParametres");
try
{
if (File.Exists(NomFichierData))
{

string[] lignes = File.ReadAllLines(NomFichierData);

if (lignes.Length > 0)
item.Qdrant_URL = lignes[0];

if (lignes.Length > 1)
{
if (bool.TryParse(lignes[1], out bool isHttps))
item.Qdrant_IsHttps = isHttps;
}
/*
if (lignes.Length > 2)
{
if (int.TryParse(lignes[1], out int port))
item.Qdrant_Port = port;
}
*/
}
return item;

}
catch
{
return null;
}

}

public static bool SaveParametres(ParametresQdrantService item)
{
LoggerService.LogInfo("QdrantService.SaveParametres");
try
{
StringBuilder sb = new();
sb.AppendLine(item.Qdrant_URL);
sb.AppendLine(item.Qdrant_IsHttps.ToString());
//sb.AppendLine(item.Qdrant_Port.ToString());

File.WriteAllText(NomFichierData, sb.ToString());

return true;
}
catch
{
return false;
}
}
#endregion

public async Task<bool> IsAlive()
{
try
{
var health = await _client.HealthAsync();
return (health != null);
}
catch
{
LoggerService.LogError("QdrantService.IsAlive - Qdrant n'est pas accessible.");
return false;
}
}

public async Task InitializeCollectionsAsync(bool isRAZCollections)
{
if(isRAZCollections)
await DeleteCollectionAsync();
foreach (Domain domain in Enum.GetValues(typeof(Domain)))
{
await EnsureCollectionAsync(domain);
}
}

public async Task DeleteDocumentInQdrant(Domain domain, string nomFichier)
{
var pointId = new PointId { Uuid = Guid.NewGuid().ToString() };
var collectionName = domain.ToCollectionName();

// 1. Définir le filtre pour trouver les points avec le même `nom_fichier`
var filter = new Qdrant.Client.Grpc.Filter
{
Must =
{
new Condition
{
Field = new FieldCondition
{
Key = "nom_fichier",
Match = new Match { Text = nomFichier }
}
}
}
};
// 2. Chercher les points existants avec ce filtre
var searchResult = await _client.ScrollAsync(
collectionName,
filter: filter,
limit: 1 // On n'a besoin de vérifier qu'un seul résultat
);
// 3. Vérifier si des points ont été trouvés
if (searchResult.Result.Count > 0)
{
LoggerService.LogDebug($"Un ou plusieurs documents avec le nom de fichier '{nomFichier}' ont été trouvés. Suppression...");

// Si des points existent, on utilise le même filtre pour les supprimer
await _client.DeleteAsync(
collectionName,
filter
);

LoggerService.LogDebug("Documents existants supprimés avec succès.");
}
}

public async Task IngestDocument(Domain domain, float[] vector, string nomFichier, string nomChunck, string content, string emailObject = "", string proprietaire = "", string niveauAcces="")
{
var pointId = new PointId { Uuid = Guid.NewGuid().ToString() };
var collectionName = domain.ToCollectionName();
var vectorObj = new Qdrant.Client.Grpc.Vector();
vectorObj.Data.Add(vector);

/*
// 1. Définir le filtre pour trouver les points avec le même `nom_fichier`
var filter = new Qdrant.Client.Grpc.Filter
{
Must =
{
new Condition
{
Field = new FieldCondition
{
Key = "nom_fichier",
Match = new Match { Text = nomFichier }
}
}
}
};
// 2. Chercher les points existants avec ce filtre
var searchResult = await _client.ScrollAsync(
collectionName,
filter: filter,
limit: 1 // On n'a besoin de vérifier qu'un seul résultat
);
// 3. Vérifier si des points ont été trouvés
if (searchResult.Result.Count > 0)
{
LoggerService.LogDebug($"Un ou plusieurs documents avec le nom de fichier '{nomFichier}' ont été trouvés. Suppression...");

// Si des points existent, on utilise le même filtre pour les supprimer
await _client.DeleteAsync(
collectionName,
filter
);

LoggerService.LogDebug("Documents existants supprimés avec succès.");
}
*/

await _client.UpsertAsync(collectionName, new[]
{
new PointStruct
{
Id = pointId,
Vectors = new Vectors { Vector = vectorObj },
Payload =
{
["categorie"] = collectionName,
["nom_fichier"] = nomFichier,
["email_object"] = emailObject,
["nom_chunck"] = nomChunck,
["niveau_acces"] = niveauAcces,
["content"] = content,
["proprietaire"] = proprietaire
}
}
});
}

//public async Task<IReadOnlyList<ScoredPoint>> SearchAsync(Domain domain, float[] queryVector, float seuilScore, int topK = 5, string adresseMail = "")
public async Task<List<SearchResult>> SearchAsync(Domain domain, float[] queryVector, float seuilScore, int topK=5, string adresseMail="")
{
var collectionName = domain.ToCollectionName();
Filter filter = new();
if (domain == Domain.Emails)
{
filter = new Qdrant.Client.Grpc.Filter
{
Must = {
new Condition
{
Field = new FieldCondition
{
Key = "categorie",
Match = new Match { Text = collectionName }
}
},
new Condition
{
Field = new FieldCondition
{
Key = "proprietaire",
Match = new Match { Text = adresseMail }
}
}
}

};
}
else
{
filter = new Qdrant.Client.Grpc.Filter
{
Must = { new Condition
{
Field = new FieldCondition
{
Key = "categorie",
Match = new Match { Text = collectionName }
}
}}
};
}


var rep = await _client.SearchAsync(collectionName, queryVector, filter, null, (ulong)topK);
var filteredChunks = rep
.Where(r => r.Score >= seuilScore)
.ToArray();

//
var resultList = filteredChunks.Select(r => new SearchResult
{
Text = r.Payload["content"].StringValue,
Nom_Fichier = r.Payload["nom_fichier"].StringValue
}).ToList();
return resultList;
//return filteredChunks;

}
#endregion

#region Méthodes privées
private async Task EnsureCollectionAsync(Domain domain)
{
var collectionName = domain.ToCollectionName();
var existing = await _client.ListCollectionsAsync();

if (!existing.Contains(collectionName))
{
await _client.CreateCollectionAsync(collectionName, new VectorParams
{
Size = (ulong)EmbeddingDim,
Distance = DefaultDistance
});
}
}

private async Task DeleteCollectionAsync()
{
var collections = await _client.ListCollectionsAsync();
foreach (var col in collections)
{
await DeleteCollectionAsync(col);
}
}

private async Task DeleteCollectionAsync(Domain domain)
{
var collectionName = domain.ToCollectionName();
await DeleteCollectionAsync(collectionName);
}

private async Task DeleteCollectionAsync(string collectionName)
{
await _client.DeleteCollectionAsync(collectionName);
}
#endregion
}
#endregion

+ 0
- 0
RAGService/Services/RAGService.cs Voir le fichier


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff

Chargement…
Annuler
Enregistrer