Lucas ARRIESSE commited on
Commit
da9ecc9
·
1 Parent(s): 8c42385

Make configuration persistent across sessions

Browse files
static/index.html CHANGED
@@ -348,9 +348,11 @@
348
  </div>
349
  <div>
350
  <p class="font-bold text-m">Quick summary:</p>
351
- <p id="assessment-recommendation-summary">A short resumé of the patcom sayings should go there.</p>
 
352
  </div>
353
- <button class="btn btn-info" id="read-assessment-button">Read whole assessment</button>
 
354
  </div>
355
 
356
  <div class="divider my-1"></div>
@@ -406,50 +408,57 @@
406
  <!--App settings modal container-->
407
  <dialog id="settings_modal" class="modal">
408
  <div class="modal-box w-11/12 max-w-5xl">
409
- <h3 class="text-lg font-bold">Application settings</h3>
410
- <div class="modal-action">
411
- <form method="dialog">
412
- <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
413
- </form>
414
- </div>
415
- <h2 class="text-lg font-bold">Private generation settings</h2>
416
- <p class="py-4">Detail your private LLM credentials under to draft and generate solution using private LLM</p>
417
- <div class="space-y-4">
418
- <div>
419
- <label for="provider-url" class="block mb-2 text-sm font-medium">LLM provider URL</label>
420
- <input id="settings-provider-url" name="provider-url" class="input input-bordered w-full">
421
  </div>
 
 
 
 
 
 
 
 
 
422
 
423
- <div>
424
- <label for="provider-token" class="block mb-2 text-sm font-medium">LLM provider token</label>
425
- <input id="settings-provider-token" name="provider-token" class="input input-bordered w-full"
426
- type="password">
427
- </div>
 
428
 
429
- <div>
430
- <label for="provider-model" class="block mb-2 text-sm font-medium">Model</label>
431
- <button id="settings-fetch-models" class="btn btn-outline">Fetch models</button>
432
- <select id="settings-provider-model" name="provider-model" class="select">
433
- </select>
434
- </div>
435
 
436
- <div>
437
- <label for="assessment-rules" class="block mb-2 text-sm font-medium">Assessment rules</label>
438
- <textarea id="settings-assessment-rules" name="assessment-rules"
439
- class="textarea textarea-bordered w-full h-48"
440
- placeholder="Enter your rules here..."></textarea>
441
- </div>
 
442
 
443
- <div>
444
- <label for="portfolio-info" class="block mb-2 text-sm font-medium">Portfolio information</label>
445
- <textarea id="settings-portfolio" name="portfolio-info"
446
- class="textarea textarea-bordered w-full h-48"
447
- placeholder="Enter your portfolio info here..."></textarea>
 
 
 
 
 
 
 
448
  </div>
449
- </div>
450
- <div class="flex">
451
- <button class="btn btn-success" id="test-btn">Save config</button>
452
- </div>
453
  </div>
454
  </dialog>
455
 
 
348
  </div>
349
  <div>
350
  <p class="font-bold text-m">Quick summary:</p>
351
+ <p id="assessment-recommendation-summary">A short resumé of the patcom
352
+ sayings should go there.</p>
353
  </div>
354
+ <button class="btn btn-info" id="read-assessment-button">Read whole
355
+ assessment</button>
356
  </div>
357
 
358
  <div class="divider my-1"></div>
 
408
  <!--App settings modal container-->
409
  <dialog id="settings_modal" class="modal">
410
  <div class="modal-box w-11/12 max-w-5xl">
411
+ <h2 class="text-xl font-bold">Application session settings</h3>
412
+ <div class="modal-action">
413
+ <form method="dialog">
414
+ <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
415
+ </form>
 
 
 
 
 
 
 
416
  </div>
417
+ <h2 class="text-lg font-bold">Private generation settings</h2>
418
+ <p class="py-4">Detail your private LLM credentials under to draft and generate solution using
419
+ private
420
+ LLM</p>
421
+ <div class="space-y-4">
422
+ <div>
423
+ <label for="provider-url" class="block mb-2 text-sm font-medium">LLM provider URL</label>
424
+ <input id="settings-provider-url" name="provider-url" class="input input-bordered w-full">
425
+ </div>
426
 
427
+ <div>
428
+ <label for="provider-token" class="block mb-2 text-sm font-medium">LLM provider
429
+ token</label>
430
+ <input id="settings-provider-token" name="provider-token"
431
+ class="input input-bordered w-full" type="password">
432
+ </div>
433
 
434
+ <div>
435
+ <label for="provider-model" class="block mb-2 text-sm font-medium">Model</label>
436
+ <button id="settings-fetch-models" class="btn btn-outline">Fetch models</button>
437
+ <select id="settings-provider-model" name="provider-model" class="select">
438
+ </select>
439
+ </div>
440
 
441
+ <div>
442
+ <label for="assessment-rules" class="block mb-2 text-sm font-medium">Assessment
443
+ rules</label>
444
+ <textarea id="settings-assessment-rules" name="assessment-rules"
445
+ class="textarea textarea-bordered w-full h-48"
446
+ placeholder="Enter your rules here..."></textarea>
447
+ </div>
448
 
449
+ <div>
450
+ <label for="portfolio-info" class="block mb-2 text-sm font-medium">Portfolio
451
+ information</label>
452
+ <textarea id="settings-portfolio" name="portfolio-info"
453
+ class="textarea textarea-bordered w-full h-48"
454
+ placeholder="Enter your portfolio info here..."></textarea>
455
+ </div>
456
+ <p class="py-4">⚠ 'Save configuration' to keep the current configuration across sessions.</p>
457
+ </div>
458
+ <div class="mt-2 space-x-2">
459
+ <button class="btn btn-success" id="settings-save-btn">Save config</button>
460
+ <button class="btn btn-error absolute right-5" id="settings-clear-btn">Clear saved configuration</button>
461
  </div>
 
 
 
 
462
  </div>
463
  </dialog>
464
 
static/js/app.js CHANGED
@@ -3,8 +3,9 @@ import {
3
  toggleElementsEnabled, toggleContainersVisibility, showLoadingOverlay, hideLoadingOverlay, populateSelect,
4
  populateCheckboxDropdown, populateDaisyDropdown, extractTableData, switchTab, enableTabSwitching, debounceAutoCategoryCount,
5
  bindTabs, checkPrivateLLMInfoAvailable, moveSolutionToDrafts, buildSolutionSubCategories, handleDraftRefine, renderDraftUI, populateLLMModelSelect,
6
- displayFullAssessment
7
- } from "./ui-utils.js";
 
8
  import { postWithSSE } from "./sse.js";
9
 
10
  // ==================================== Variables globales ========================================
@@ -1056,7 +1057,10 @@ document.addEventListener('DOMContentLoaded', function () {
1056
  const refineBtn = document.getElementById('refine-btn');
1057
  refineBtn.addEventListener('click', handleDraftRefine);
1058
 
 
1059
  renderDraftUI();
 
 
1060
  });
1061
 
1062
  // dseactiver le choix du nb de catégories lorsqu'en mode auto
@@ -1076,6 +1080,10 @@ document.getElementById('copy-reqs-btn').addEventListener('click', (ev) => {
1076
  // copy all requirements
1077
  document.getElementById('copy-all-reqs-btn').addEventListener('click', copyAllRequirementsAsMarkdown);
1078
 
 
 
 
 
1079
  // button to fetch llm models
1080
  document.getElementById('settings-fetch-models').addEventListener('click', _ => {
1081
  const url = document.getElementById('settings-provider-url').value;
@@ -1084,6 +1092,8 @@ document.getElementById('settings-fetch-models').addEventListener('click', _ =>
1084
  populateLLMModelSelect('settings-provider-model', url, token).catch(e => alert("Error while fetching models: " + e))
1085
  });
1086
 
 
 
1087
  // button to open full assessment modal
1088
  document.getElementById('read-assessment-button').addEventListener('click', _ => {
1089
  displayFullAssessment();
 
3
  toggleElementsEnabled, toggleContainersVisibility, showLoadingOverlay, hideLoadingOverlay, populateSelect,
4
  populateCheckboxDropdown, populateDaisyDropdown, extractTableData, switchTab, enableTabSwitching, debounceAutoCategoryCount,
5
  bindTabs, checkPrivateLLMInfoAvailable, moveSolutionToDrafts, buildSolutionSubCategories, handleDraftRefine, renderDraftUI, populateLLMModelSelect,
6
+ displayFullAssessment, handleSaveConfigFields, handleLoadConfigFields,
7
+ handleClearConfig
8
+ } from "./ui.js";
9
  import { postWithSSE } from "./sse.js";
10
 
11
  // ==================================== Variables globales ========================================
 
1057
  const refineBtn = document.getElementById('refine-btn');
1058
  refineBtn.addEventListener('click', handleDraftRefine);
1059
 
1060
+ // render l'ui de draft vide
1061
  renderDraftUI();
1062
+
1063
+ handleLoadConfigFields();
1064
  });
1065
 
1066
  // dseactiver le choix du nb de catégories lorsqu'en mode auto
 
1080
  // copy all requirements
1081
  document.getElementById('copy-all-reqs-btn').addEventListener('click', copyAllRequirementsAsMarkdown);
1082
 
1083
+ // =============== settings events ================
1084
+ document.getElementById('settings-save-btn').addEventListener('click', handleSaveConfigFields);
1085
+ document.getElementById('settings-clear-btn').addEventListener('click', handleClearConfig);
1086
+
1087
  // button to fetch llm models
1088
  document.getElementById('settings-fetch-models').addEventListener('click', _ => {
1089
  const url = document.getElementById('settings-provider-url').value;
 
1092
  populateLLMModelSelect('settings-provider-model', url, token).catch(e => alert("Error while fetching models: " + e))
1093
  });
1094
 
1095
+ // ================== solution drafting events =============
1096
+
1097
  // button to open full assessment modal
1098
  document.getElementById('read-assessment-button').addEventListener('click', _ => {
1099
  displayFullAssessment();
static/js/persistence.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // clées utilisées pour la generation avec private compute
2
+ const CONFIG_KEYS = ["providerUrl", "providerToken", "providerModel", "assessmentRules", "businessPortfolio"];
3
+ // clée pour marquer si la config est remplie.
4
+ const POPULATED_CONFIG_KEY = "_config_populated";
5
+
6
+ /**
7
+ * Récupère la configuration pour la generation avec private compute, si elle est renseignée, sinon retourne null.
8
+ */
9
+ export function loadConfig() {
10
+ if (localStorage.getItem(POPULATED_CONFIG_KEY) != null) {
11
+ let configObj = {};
12
+ CONFIG_KEYS
13
+ .forEach(k => {
14
+ configObj[k] = localStorage.getItem(k)
15
+ })
16
+ return configObj;
17
+ }
18
+ else
19
+ return null;
20
+ }
21
+
22
+ /**
23
+ * Sauvegarde la configuration pour le private compute
24
+ */
25
+ export function saveConfig(config) {
26
+ CONFIG_KEYS
27
+ .forEach(k => {
28
+ if (config[k])
29
+ localStorage.setItem(k, config[k]);
30
+ })
31
+
32
+ localStorage.setItem(POPULATED_CONFIG_KEY, "true");
33
+ }
34
+
35
+ export function clearConfig() {
36
+ localStorage.clear();
37
+ }
static/js/{ui-utils.js → ui.js} RENAMED
@@ -1,5 +1,6 @@
1
  import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/16.1.1/lib/marked.esm.js';
2
  import { assessSolution, getModelList, refineSolution } from "./gen.js"
 
3
 
4
  // =============================================================================
5
  // FONCTIONS UTILITAIRES POUR LA GESTION DES ÉLÉMENTS
@@ -345,23 +346,29 @@ export function debounceAutoCategoryCount(state) {
345
  document.getElementById('category-count').disabled = state;
346
  }
347
 
348
- export function getPrivateLLMInfo() {
349
- const provider_url = document.getElementById('settings-provider-url').value;
350
- const provider_token = document.getElementById('settings-provider-token').value;
351
- const provider_model = document.getElementById('settings-provider-model').value;
352
- const assessment_rules = document.getElementById('settings-assessment-rules').value;
353
- const portfolio_info = document.getElementById('settings-portfolio').value;
354
 
355
- return { provider_url, provider_token, provider_model, assessment_rules, portfolio_info };
 
 
 
 
 
 
 
 
 
 
 
356
  }
357
 
358
  /**
359
  * Vérifie si les paramètres sont bien renseignés pour utiliser la génération privée.
360
  */
361
  export function checkPrivateLLMInfoAvailable() {
362
- const { provider_url, provider_token, provider_model, assessment_rules, portfolio_info } = getPrivateLLMInfo();
363
  const isEmpty = (str) => (!str?.length);
364
- return !isEmpty(provider_url) && !isEmpty(provider_token) && !isEmpty(assessment_rules) && !isEmpty(portfolio_info) && !isEmpty(provider_model);
365
  // return true;
366
  }
367
 
@@ -414,7 +421,49 @@ export async function populateLLMModelSelect(selectElementId, providerUrl, apiKe
414
  }
415
  }
416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
 
419
  // ================================================================================ Solution drafting using private LLMs ==========================================================
420
 
@@ -446,14 +495,15 @@ export function moveSolutionToDrafts(solution) {
446
 
447
  switchTab('draft-tab');
448
 
449
- const { provider_url, provider_token, provider_model, assessment_rules, portfolio_info } = getPrivateLLMInfo();
450
 
451
  showLoadingOverlay("Assessing solution ....");
452
- assessSolution(provider_url, provider_model, provider_token, solution, assessment_rules, portfolio_info).then(response => {
453
  // reset the state of the draft history
454
  draftHistory = [];
455
  draftCurrentIndex = -1;
456
 
 
457
  const insights = response.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false }));
458
 
459
  // push the solution to the draft history
@@ -475,33 +525,6 @@ export function moveSolutionToDrafts(solution) {
475
  })
476
  }
477
 
478
- /**
479
- * SIMULATED API CALL
480
- * In a real app, this would be an async fetch call to a backend AI/service.
481
- * @param {string} previousSolution - The text of the previous solution.
482
- * @param {Array<string>} selectedInsights - An array of the selected insight texts.
483
- * @returns {object} - An object with the new solution and new insights.
484
- */
485
- function assessSolutionDraft(previousSolution, selectedInsights = []) {
486
- const version = draftHistory.length + 1;
487
- let newSolutionText;
488
-
489
- if (selectedInsights.length > 0) {
490
- newSolutionText = `V${version}: Based on "${previousSolution.substring(0, 30)}..." and the insights: [${selectedInsights.join(', ')}], we've refined the plan to focus more on digital outreach and local partnerships.`;
491
- } else {
492
- newSolutionText = `V${version}: This is an initial assessment of "${previousSolution.substring(0, 50)}...". The plan seems solid but could use more detail.`;
493
- }
494
-
495
- // Generate some random new insights
496
- const newInsights = [
497
- { id: `v${version}-i1`, text: `Consider social media marketing (Insight ${Math.floor(Math.random() * 100)})`, checked: false },
498
- { id: `v${version}-i2`, text: `Focus on customer retention (Insight ${Math.floor(Math.random() * 100)})`, checked: false },
499
- { id: `v${version}-i3`, text: `Expand the product line (Insight ${Math.floor(Math.random() * 100)})`, checked: false },
500
- ];
501
-
502
- return { newSolutionText, newInsights };
503
- }
504
-
505
  /**
506
  * Renders the timeline UI based on the current state
507
  * @param {Number} currentIndex - Current index for latest draft
@@ -597,19 +620,20 @@ export function handleDraftRefine() {
597
  }
598
  // ---
599
 
600
- const { provider_url, provider_token, provider_model, assessment_rules, portfolio_info } = getPrivateLLMInfo();
601
 
602
  showLoadingOverlay('Refining and assessing ....')
603
 
604
- refineSolution(provider_url, provider_model, provider_token, currentState.solution, selectedInsights, assessment_rules, portfolio_info)
605
  .then(newSolution => {
606
  const refinedSolution = newSolution;
607
- return assessSolution(provider_url, provider_model, provider_token, newSolution, assessment_rules, portfolio_info)
608
  .then(assessedResult => {
609
  return { refinedSolution, assessedResult };
610
  });
611
  })
612
  .then(result => {
 
613
  const newInsights = result.assessedResult.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false }));
614
 
615
  draftHistory.push({
@@ -641,6 +665,9 @@ function jumpToDraft(index) {
641
  }
642
  }
643
 
 
 
 
644
  export function displayFullAssessment() {
645
  const full_assessment_content = document.getElementById('read-assessment-content');
646
  const modal = document.getElementById('read-assessment-modal');
 
1
  import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/16.1.1/lib/marked.esm.js';
2
  import { assessSolution, getModelList, refineSolution } from "./gen.js"
3
+ import { clearConfig, loadConfig, saveConfig } from "./persistence.js";
4
 
5
  // =============================================================================
6
  // FONCTIONS UTILITAIRES POUR LA GESTION DES ÉLÉMENTS
 
346
  document.getElementById('category-count').disabled = state;
347
  }
348
 
349
+ // ============================================================================================ Overlay des paramètres ====================================================================
 
 
 
 
 
350
 
351
+ /**
352
+ * Récupère les valeurs des champs de config des infos LLM.
353
+ * @returns
354
+ */
355
+ export function getConfigFields() {
356
+ const providerUrl = document.getElementById('settings-provider-url').value;
357
+ const providerToken = document.getElementById('settings-provider-token').value;
358
+ const providerModel = document.getElementById('settings-provider-model').value;
359
+ const assessmentRules = document.getElementById('settings-assessment-rules').value;
360
+ const businessPortfolio = document.getElementById('settings-portfolio').value;
361
+
362
+ return { providerUrl, providerToken, providerModel, assessmentRules, businessPortfolio };
363
  }
364
 
365
  /**
366
  * Vérifie si les paramètres sont bien renseignés pour utiliser la génération privée.
367
  */
368
  export function checkPrivateLLMInfoAvailable() {
369
+ const { providerUrl, providerToken, providerModel, assessmentRules, businessPortfolio } = getConfigFields();
370
  const isEmpty = (str) => (!str?.length);
371
+ return !isEmpty(providerUrl) && !isEmpty(providerToken) && !isEmpty(assessmentRules) && !isEmpty(businessPortfolio) && !isEmpty(providerModel);
372
  // return true;
373
  }
374
 
 
421
  }
422
  }
423
 
424
+ /**
425
+ * Charge le contenu de la configuration locale dans les champs HTML.
426
+ */
427
+ export function handleLoadConfigFields() {
428
+ const configuration = loadConfig();
429
+ if (configuration === null)
430
+ return;
431
+
432
+ const providerUrl = document.getElementById('settings-provider-url');
433
+ const providerToken = document.getElementById('settings-provider-token');
434
+ const providerModel = document.getElementById('settings-provider-model');
435
+ const assessmentRules = document.getElementById('settings-assessment-rules');
436
+ const businessPortfolio = document.getElementById('settings-portfolio');
437
 
438
+ providerUrl.value = configuration.providerUrl;
439
+ providerToken.value = configuration.providerToken;
440
+ assessmentRules.value = configuration.assessmentRules;
441
+ businessPortfolio.value = configuration.businessPortfolio;
442
+
443
+ // on doit d'abord recup les modeles avant de set la valeur
444
+ populateLLMModelSelect('settings-provider-model', configuration.providerUrl, configuration.providerToken).then(() => {
445
+ providerModel.value = configuration.providerModel;
446
+ }).catch(e => {
447
+ alert("Failed to set LLM model in model selector. Model may not be available anymore, check model in LLM settings.");
448
+ })
449
+ }
450
+
451
+ /**
452
+ * Sauvegarde le contenu des champs dans la configuration locale.
453
+ */
454
+ export function handleSaveConfigFields() {
455
+ saveConfig(getConfigFields());
456
+ alert("Configuration saved locally.");
457
+ }
458
+
459
+ /**
460
+ * Clear le contenu de la configuration stockée dans localStorage.
461
+ * LA CONFIGURATION DANS LES CHAMPS HTML N'EST PAS AFFECTEE.
462
+ */
463
+ export function handleClearConfig() {
464
+ clearConfig();
465
+ alert("Saved configuration has been cleared. Configuration set in the fields won't be saved.");
466
+ }
467
 
468
  // ================================================================================ Solution drafting using private LLMs ==========================================================
469
 
 
495
 
496
  switchTab('draft-tab');
497
 
498
+ const { providerUrl, providerToken, providerModel, assessmentRules, businessPortfolio } = getConfigFields();
499
 
500
  showLoadingOverlay("Assessing solution ....");
501
+ assessSolution(providerUrl, providerModel, providerToken, solution, assessmentRules, businessPortfolio).then(response => {
502
  // reset the state of the draft history
503
  draftHistory = [];
504
  draftCurrentIndex = -1;
505
 
506
+ // map from a list of insights to a selectable list of insights
507
  const insights = response.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false }));
508
 
509
  // push the solution to the draft history
 
525
  })
526
  }
527
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  /**
529
  * Renders the timeline UI based on the current state
530
  * @param {Number} currentIndex - Current index for latest draft
 
620
  }
621
  // ---
622
 
623
+ const { providerUrl, providerToken, providerModel, assessmentRules, businessPortfolio } = getConfigFields();
624
 
625
  showLoadingOverlay('Refining and assessing ....')
626
 
627
+ refineSolution(providerUrl, providerModel, providerToken, currentState.solution, selectedInsights, assessmentRules, businessPortfolio)
628
  .then(newSolution => {
629
  const refinedSolution = newSolution;
630
+ return assessSolution(providerUrl, providerModel, providerToken, newSolution, assessmentRules, businessPortfolio)
631
  .then(assessedResult => {
632
  return { refinedSolution, assessedResult };
633
  });
634
  })
635
  .then(result => {
636
+ // map from a list of insights to a selectable list of insights
637
  const newInsights = result.assessedResult.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false }));
638
 
639
  draftHistory.push({
 
665
  }
666
  }
667
 
668
+ /**
669
+ * Displays the whole idea evaluation.
670
+ */
671
  export function displayFullAssessment() {
672
  const full_assessment_content = document.getElementById('read-assessment-content');
673
  const modal = document.getElementById('read-assessment-modal');