rinogeek commited on
Commit
e1633a4
·
1 Parent(s): 03ab431
server/aiCore.js CHANGED
@@ -20,9 +20,9 @@ function parseModelList(env) {
20
 
21
  const fromList = rawList
22
  ? rawList
23
- .split(",")
24
- .map((s) => s.trim())
25
- .filter(Boolean)
26
  : [];
27
 
28
  const seed = fromList.length > 0 ? fromList : single ? [single] : [];
@@ -62,24 +62,28 @@ function countSentences(text) {
62
 
63
  function validateObservation({ text, temperatureC, hasHazard }) {
64
  const t = (text || "").trim();
65
-
66
  if (!/^Observation\s*:/i.test(t)) return { ok: false, reason: "Ne commence pas par Observation:" };
67
  if (!/Equation\s*:/i.test(t)) return { ok: false, reason: "equation manquante (Equation:)" };
68
-
 
69
  const parts = t.split(/Equation\s*:/i);
70
  const observation = parts[0].replace(/^Observation\s*:/i, '').trim();
71
- const equation = parts[1].trim();
 
 
72
 
73
  if (observation.length < 10) return { ok: false, reason: "observation trop courte" };
74
  const sentenceCount = countSentences(observation);
75
  if (sentenceCount > 5) return { ok: false, reason: `observation trop longue (${sentenceCount} phrases)` };
76
  if (equation.length < 2) return { ok: false, reason: "equation invalide" };
 
77
 
78
  if (hasHazard && !/secur|prud|attention|protection|danger|gants|lunettes|corros/i.test(observation)) {
79
  // on peut ne pas bloquer rigidement mais le logguer, ici on accepte
80
  }
81
 
82
- return { ok: true, reason: null, parsed: { observation, equation } };
83
  }
84
 
85
  function normalizeOneParagraph(text) {
@@ -105,9 +109,11 @@ function buildPrompt({ temperatureC, substances }) {
105
  "Tu es un assistant de laboratoire de chimie. " +
106
  "Decris l'observation du melange en etant TRES CONCIS (MAXIMUM 2 a 3 phrases), en allant droit au but et en enlevant tous les details inutiles. Inclus les rappels de securite si necessaire. " +
107
  "Donne ensuite l'equation correspondante. " +
 
108
  "Important: Tu dois repondre UNIQUEMENT sous ce format textuel exact (sans markdown, sans blabla):\n" +
109
  "Observation: <tes 2 a 3 phrases ici>\n" +
110
- "Equation: <equation chimique avec -> ou -> ou →>";
 
111
 
112
  const user = [
113
  "Contexte:",
@@ -117,7 +123,8 @@ function buildPrompt({ temperatureC, substances }) {
117
  "",
118
  "Format attendu strictement: ",
119
  "Observation: <...>",
120
- "Equation: <...>"
 
121
  ].join("\n");
122
 
123
  return { system, user };
@@ -259,7 +266,7 @@ async function callGroqChatCompletionsText({ apiKey, model, system, user }) {
259
  // Some models may place the final answer into "reasoning". Accept it only if it matches our constraints.
260
  const candidate = normalizeOneParagraph(msg.reasoning);
261
  // Full validation is done later with real temp/hazard. Here we only ensure it looks like a final answer.
262
- if (/equation\s*:/i.test(candidate) && /cas d'usage\s*:/i.test(candidate) && /(->|→)/.test(candidate)) {
263
  return candidate;
264
  }
265
  throw new UpstreamError(422, "Model returned reasoning without final content");
 
20
 
21
  const fromList = rawList
22
  ? rawList
23
+ .split(",")
24
+ .map((s) => s.trim())
25
+ .filter(Boolean)
26
  : [];
27
 
28
  const seed = fromList.length > 0 ? fromList : single ? [single] : [];
 
62
 
63
  function validateObservation({ text, temperatureC, hasHazard }) {
64
  const t = (text || "").trim();
65
+
66
  if (!/^Observation\s*:/i.test(t)) return { ok: false, reason: "Ne commence pas par Observation:" };
67
  if (!/Equation\s*:/i.test(t)) return { ok: false, reason: "equation manquante (Equation:)" };
68
+ if (!/Application\s*:/i.test(t)) return { ok: false, reason: "application manquante (Application:)" };
69
+
70
  const parts = t.split(/Equation\s*:/i);
71
  const observation = parts[0].replace(/^Observation\s*:/i, '').trim();
72
+ const lowerParts = parts[1].split(/Application\s*:/i);
73
+ const equation = lowerParts[0].trim();
74
+ const application = lowerParts[1] ? lowerParts[1].trim() : "";
75
 
76
  if (observation.length < 10) return { ok: false, reason: "observation trop courte" };
77
  const sentenceCount = countSentences(observation);
78
  if (sentenceCount > 5) return { ok: false, reason: `observation trop longue (${sentenceCount} phrases)` };
79
  if (equation.length < 2) return { ok: false, reason: "equation invalide" };
80
+ if (application.length < 10) return { ok: false, reason: "application invalide ou trop courte" };
81
 
82
  if (hasHazard && !/secur|prud|attention|protection|danger|gants|lunettes|corros/i.test(observation)) {
83
  // on peut ne pas bloquer rigidement mais le logguer, ici on accepte
84
  }
85
 
86
+ return { ok: true, reason: null, parsed: { observation, equation, application } };
87
  }
88
 
89
  function normalizeOneParagraph(text) {
 
109
  "Tu es un assistant de laboratoire de chimie. " +
110
  "Decris l'observation du melange en etant TRES CONCIS (MAXIMUM 2 a 3 phrases), en allant droit au but et en enlevant tous les details inutiles. Inclus les rappels de securite si necessaire. " +
111
  "Donne ensuite l'equation correspondante. " +
112
+ "Termine avec une application pratique de ce melange dans la vie courante ou l'industrie (ex: 'ce melange sert a la fabrication de detergent'). " +
113
  "Important: Tu dois repondre UNIQUEMENT sous ce format textuel exact (sans markdown, sans blabla):\n" +
114
  "Observation: <tes 2 a 3 phrases ici>\n" +
115
+ "Equation: <equation chimique avec -> ou -> ou →>\n" +
116
+ "Application: <1 a 2 phrases sur l'utilite pratique>";
117
 
118
  const user = [
119
  "Contexte:",
 
123
  "",
124
  "Format attendu strictement: ",
125
  "Observation: <...>",
126
+ "Equation: <...>",
127
+ "Application: <...>"
128
  ].join("\n");
129
 
130
  return { system, user };
 
266
  // Some models may place the final answer into "reasoning". Accept it only if it matches our constraints.
267
  const candidate = normalizeOneParagraph(msg.reasoning);
268
  // Full validation is done later with real temp/hazard. Here we only ensure it looks like a final answer.
269
+ if (/equation\s*:/i.test(candidate) && /application\s*:/i.test(candidate) && /(->|→)/.test(candidate)) {
270
  return candidate;
271
  }
272
  throw new UpstreamError(422, "Model returned reasoning without final content");
server/aiMixMiddleware.ts CHANGED
@@ -65,9 +65,11 @@ function buildPrompt(args: {
65
  "Tu es un assistant de laboratoire de chimie. " +
66
  "Decris l'observation du melange en etant TRES CONCIS (MAXIMUM 2 a 3 phrases), en allant droit au but et en enlevant tous les details inutiles. Inclus les rappels de securite si necessaire. " +
67
  "Donne ensuite l'equation correspondante. " +
 
68
  "Important: Tu dois repondre UNIQUEMENT sous ce format textuel exact (sans markdown, sans blabla):\n" +
69
  "Observation: <tes 2 a 3 phrases ici>\n" +
70
- "Equation: <equation chimique avec -> ou -> ou →>";
 
71
 
72
  const user =
73
  [
@@ -78,7 +80,8 @@ function buildPrompt(args: {
78
  "",
79
  "Format attendu strictement: ",
80
  "Observation: <...>",
81
- "Equation: <...>"
 
82
  ].join("\n");
83
 
84
  return { system, user };
@@ -106,21 +109,25 @@ function validateObservation(args: {
106
 
107
  if (!/^Observation\s*:/i.test(t)) return { ok: false, reason: "Ne commence pas par Observation:" };
108
  if (!/Equation\s*:/i.test(t)) return { ok: false, reason: "equation manquante (Equation:)" };
 
109
 
110
  const parts = t.split(/Equation\s*:/i);
111
  const observation = parts[0].replace(/^Observation\s*:/i, '').trim();
112
- const equation = parts[1].trim();
 
 
113
 
114
  if (observation.length < 10) return { ok: false, reason: "observation trop courte" };
115
  const sentenceCount = countSentences(observation);
116
  if (sentenceCount > 5) return { ok: false, reason: `observation trop longue (${sentenceCount} phrases)` };
117
  if (equation.length < 2) return { ok: false, reason: "equation invalide" };
 
118
 
119
  if (args.hasHazard && !/secur|prud|attention|protection|danger|gants|lunettes|corros/i.test(observation)) {
120
  // on l'accepte tout de meme
121
  }
122
 
123
- return { ok: true, reason: null, parsed: { observation, equation } };
124
  }
125
 
126
  async function httpsPostJson(args: {
@@ -282,7 +289,7 @@ async function callGroqChatCompletionsText(args: {
282
  const reasoning = msg?.reasoning;
283
  if (typeof reasoning === "string" && reasoning.trim()) {
284
  const candidate = normalizeOneParagraph(reasoning);
285
- if (/equation\s*:/i.test(candidate) && /(->|→)/.test(candidate)) {
286
  return candidate;
287
  }
288
  throw new UpstreamError(422, "Model returned reasoning without final content");
 
65
  "Tu es un assistant de laboratoire de chimie. " +
66
  "Decris l'observation du melange en etant TRES CONCIS (MAXIMUM 2 a 3 phrases), en allant droit au but et en enlevant tous les details inutiles. Inclus les rappels de securite si necessaire. " +
67
  "Donne ensuite l'equation correspondante. " +
68
+ "Termine avec une application pratique de ce melange dans la vie courante ou l'industrie (ex: 'ce melange sert a la fabrication de detergent'). " +
69
  "Important: Tu dois repondre UNIQUEMENT sous ce format textuel exact (sans markdown, sans blabla):\n" +
70
  "Observation: <tes 2 a 3 phrases ici>\n" +
71
+ "Equation: <equation chimique avec -> ou -> ou →>\n" +
72
+ "Application: <1 a 2 phrases sur l'utilite pratique>";
73
 
74
  const user =
75
  [
 
80
  "",
81
  "Format attendu strictement: ",
82
  "Observation: <...>",
83
+ "Equation: <...>",
84
+ "Application: <...>"
85
  ].join("\n");
86
 
87
  return { system, user };
 
109
 
110
  if (!/^Observation\s*:/i.test(t)) return { ok: false, reason: "Ne commence pas par Observation:" };
111
  if (!/Equation\s*:/i.test(t)) return { ok: false, reason: "equation manquante (Equation:)" };
112
+ if (!/Application\s*:/i.test(t)) return { ok: false, reason: "application manquante (Application:)" };
113
 
114
  const parts = t.split(/Equation\s*:/i);
115
  const observation = parts[0].replace(/^Observation\s*:/i, '').trim();
116
+ const lowerParts = parts[1].split(/Application\s*:/i);
117
+ const equation = lowerParts[0].trim();
118
+ const application = lowerParts[1] ? lowerParts[1].trim() : "";
119
 
120
  if (observation.length < 10) return { ok: false, reason: "observation trop courte" };
121
  const sentenceCount = countSentences(observation);
122
  if (sentenceCount > 5) return { ok: false, reason: `observation trop longue (${sentenceCount} phrases)` };
123
  if (equation.length < 2) return { ok: false, reason: "equation invalide" };
124
+ if (application.length < 10) return { ok: false, reason: "application invalide ou trop courte" };
125
 
126
  if (args.hasHazard && !/secur|prud|attention|protection|danger|gants|lunettes|corros/i.test(observation)) {
127
  // on l'accepte tout de meme
128
  }
129
 
130
+ return { ok: true, reason: null, parsed: { observation, equation, application } };
131
  }
132
 
133
  async function httpsPostJson(args: {
 
289
  const reasoning = msg?.reasoning;
290
  if (typeof reasoning === "string" && reasoning.trim()) {
291
  const candidate = normalizeOneParagraph(reasoning);
292
+ if (/equation\s*:/i.test(candidate) && /application\s*:/i.test(candidate) && /(->|→)/.test(candidate)) {
293
  return candidate;
294
  }
295
  throw new UpstreamError(422, "Model returned reasoning without final content");
src/components/lab/ResultsPanel.tsx CHANGED
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
4
  import { Trash2, Download } from "lucide-react";
5
 
6
  interface ResultsPanelProps {
7
- results: { observation: string; equation: string }[];
8
  onClear: () => void;
9
  }
10
 
@@ -18,7 +18,7 @@ const ResultsPanel = ({ results, onClear }: ResultsPanelProps) => {
18
  const headers = ['#', 'Observation', 'Heure'];
19
  const rows = results.map((result, index) => [
20
  index + 1,
21
- `"${result.observation.replace(/"/g, '""')} [Eq: ${result.equation.replace(/"/g, '""')}]"`, // Escape quotes in CSV
22
  new Date().toLocaleTimeString('fr-FR', {
23
  hour: '2-digit',
24
  minute: '2-digit',
@@ -86,8 +86,8 @@ const ResultsPanel = ({ results, onClear }: ResultsPanelProps) => {
86
  <div className="w-6 h-6 rounded-full bg-primary/10 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
87
  {index + 1}
88
  </div>
89
- <div className="flex-1 min-w-0">
90
- <p className="text-sm leading-relaxed">{result.observation}</p>
91
  <p className="text-xs text-muted-foreground mt-1">
92
  {new Date().toLocaleTimeString('fr-FR', {
93
  hour: '2-digit',
@@ -98,13 +98,21 @@ const ResultsPanel = ({ results, onClear }: ResultsPanelProps) => {
98
  </div>
99
  </div>
100
  {result.equation && (
101
- <div className="mt-1 p-2 bg-muted/50 rounded-md border border-border/50">
102
  <p className="text-[10px] font-semibold text-muted-foreground mb-1 uppercase tracking-wider">Équation chimique</p>
103
- <p className="text-sm font-mono text-primary font-medium overflow-x-auto whitespace-nowrap scrollbar-none">
104
  {result.equation}
105
  </p>
106
  </div>
107
  )}
 
 
 
 
 
 
 
 
108
  </div>
109
  ))}
110
  </div>
 
4
  import { Trash2, Download } from "lucide-react";
5
 
6
  interface ResultsPanelProps {
7
+ results: { observation: string; equation: string; application: string }[];
8
  onClear: () => void;
9
  }
10
 
 
18
  const headers = ['#', 'Observation', 'Heure'];
19
  const rows = results.map((result, index) => [
20
  index + 1,
21
+ `"${result.observation.replace(/"/g, '""')} [Eq: ${result.equation.replace(/"/g, '""')}] [App: ${(result.application || '').replace(/"/g, '""')}]"`, // Escape quotes in CSV
22
  new Date().toLocaleTimeString('fr-FR', {
23
  hour: '2-digit',
24
  minute: '2-digit',
 
86
  <div className="w-6 h-6 rounded-full bg-primary/10 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
87
  {index + 1}
88
  </div>
89
+ <div className="flex-1 min-w-0 break-words">
90
+ <p className="text-sm leading-relaxed break-words">{result.observation}</p>
91
  <p className="text-xs text-muted-foreground mt-1">
92
  {new Date().toLocaleTimeString('fr-FR', {
93
  hour: '2-digit',
 
98
  </div>
99
  </div>
100
  {result.equation && (
101
+ <div className="mt-1 p-2 bg-muted/50 rounded-md border border-border/50 w-full">
102
  <p className="text-[10px] font-semibold text-muted-foreground mb-1 uppercase tracking-wider">Équation chimique</p>
103
+ <p className="text-sm font-mono text-primary font-medium break-words overflow-wrap-anywhere">
104
  {result.equation}
105
  </p>
106
  </div>
107
  )}
108
+ {result.application && (
109
+ <div className="mt-1 p-2 bg-primary/5 rounded-md border border-primary/20 w-full">
110
+ <p className="text-[10px] font-semibold text-primary/80 mb-1 uppercase tracking-wider">Application pratique</p>
111
+ <p className="text-sm text-foreground/90 break-words">
112
+ {result.application}
113
+ </p>
114
+ </div>
115
+ )}
116
  </div>
117
  ))}
118
  </div>
src/pages/Lab.tsx CHANGED
@@ -20,7 +20,7 @@ const STORAGE_KEY = 'lab-workspaces';
20
 
21
  const Lab = () => {
22
  const [selectedItem, setSelectedItem] = useState<string | null>(null);
23
- const [results, setResults] = useState<{ observation: string; equation: string }[]>([]);
24
  const [safetyWarning, setSafetyWarning] = useState<string | null>(null);
25
  const [temperatureC, setTemperatureC] = useState<number>(20);
26
  const canvasRef = useRef<{ getSnapshot: () => string | null; loadSnapshot: (data: string) => void } | null>(null);
 
20
 
21
  const Lab = () => {
22
  const [selectedItem, setSelectedItem] = useState<string | null>(null);
23
+ const [results, setResults] = useState<{ observation: string; equation: string; application: string }[]>([]);
24
  const [safetyWarning, setSafetyWarning] = useState<string | null>(null);
25
  const [temperatureC, setTemperatureC] = useState<number>(20);
26
  const canvasRef = useRef<{ getSnapshot: () => string | null; loadSnapshot: (data: string) => void } | null>(null);
src/services/aiMix.ts CHANGED
@@ -10,7 +10,7 @@ export async function generateAiMixObservation(args: {
10
  temperatureC: number;
11
  // Back-compat (dev): optional ids
12
  substanceIds?: string[];
13
- }): Promise<{ observation: string; equation: string }> {
14
  const resp = await fetch("/api/ai/mix", {
15
  method: "POST",
16
  headers: { "Content-Type": "application/json" },
@@ -33,11 +33,12 @@ export async function generateAiMixObservation(args: {
33
 
34
  const data: any = await resp.json();
35
  const out = data?.text;
36
- if (!out || !out.observation || !out.equation) {
37
  throw new Error("Reponse IA invalide");
38
  }
39
  return {
40
  observation: out.observation.trim(),
41
- equation: out.equation.trim()
 
42
  };
43
  }
 
10
  temperatureC: number;
11
  // Back-compat (dev): optional ids
12
  substanceIds?: string[];
13
+ }): Promise<{ observation: string; equation: string; application: string }> {
14
  const resp = await fetch("/api/ai/mix", {
15
  method: "POST",
16
  headers: { "Content-Type": "application/json" },
 
33
 
34
  const data: any = await resp.json();
35
  const out = data?.text;
36
+ if (!out || !out.observation || !out.equation || !out.application) {
37
  throw new Error("Reponse IA invalide");
38
  }
39
  return {
40
  observation: out.observation.trim(),
41
+ equation: out.equation.trim(),
42
+ application: out.application.trim()
43
  };
44
  }