aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio_22/Tango.Portal.Chat.Web
diff options
context:
space:
mode:
authorRoy Ben Shabat <roy.mail.net@gmail.com>2025-09-04 12:31:46 +0300
committerRoy Ben Shabat <roy.mail.net@gmail.com>2025-09-04 12:31:46 +0300
commit13f9257daed202db98442f4a97167fd4d0e09e14 (patch)
tree1b735c7212ed42ff808a8ce16b71e6991a44b32c /Software/Visual_Studio_22/Tango.Portal.Chat.Web
parent5c8f370f9733b881aea4232391b86a640c218f42 (diff)
downloadTango-13f9257daed202db98442f4a97167fd4d0e09e14.tar.gz
Tango-13f9257daed202db98442f4a97167fd4d0e09e14.zip
Multiple Improvements.
Diffstat (limited to 'Software/Visual_Studio_22/Tango.Portal.Chat.Web')
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs208
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/planner_prompt.txt9
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/ploty_sample.txt126
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/schema.json85
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/Contracts.cs1
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/KqlGuard.cs7
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/LlmClient.cs112
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/SchemaRegistry.cs11
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Tango.Portal.Chat.Web.csproj13
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/DataHelper.cs128
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Home/Index.cshtml98
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Shared/_Layout.cshtml2
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.Development.json2
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.json2
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/assets/css/template.css4
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/favicon.icobin0 -> 32038 bytes
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/favicon.pngbin0 -> 281 bytes
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/site.css20
18 files changed, 642 insertions, 186 deletions
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs
index c204df7c7..da91d31e0 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs
@@ -4,6 +4,10 @@ using Tango.Portal.Chat.Web.Models;
using Tango.Portal.Chat.Web.Services;
using Kusto.Data.Data;
using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json.Linq;
+using System.Text.Json.Nodes;
+using System.Collections;
+using Tango.Portal.Chat.Web.Utils;
namespace Tango.Portal.Chat.Web.Controllers
{
@@ -32,80 +36,26 @@ namespace Tango.Portal.Chat.Web.Controllers
{
var schemaJson = _schema.GetSchemaJson();
var plannerPrompt = _schema.GetPlannerPrompt();
+ var plotySample = _schema.GetPlotySample();
// 1) Ask the model for KQL
- var plan = await _llm.ProposeKqlAsync(plannerPrompt, req.Question, schemaJson, req.History, ct);
+ var plan = await _llm.ProposeKqlAsync(plannerPrompt, plotySample, req.Question, schemaJson, req.History, ct);
if (plan.Assistant == "data")
{
- // 2) Guardrail validation
- var val = _guard.Validate(plan.Kql);
- if (!val.IsOk) return BadRequest(new { error = "Invalid KQL", details = val.Error, plan });
-
- // 4) Execute in ADX
- DataTable table;
- try
- {
- table = await _adx.QueryAsync(plan.Kql, plan.Parameters, ct);
- }
- catch (Exception ex)
- {
- // Return error to the client so they can iterate
- return new ChatResponse
- {
- Answer = $"Seems like my kusto query ran into some issue..\n{ex.Message}",
- ThreadId = req.ThreadId,
- UsedKql = plan.Kql
- };
- }
-
- // 5) Build compact facts (limit rows/cols)
- var preview = ToPreview(table, 200);
- var facts = JsonSerializer.Serialize(preview);
-
- // 6) Ask model for final answer
- //var answer = await _llm.AnswerFromFactsAsync(req.Question, facts, plan.Kql, ct);
-
- var run = await _llm.AnswerWithAssistantAsync(
- LlmClient.AssistantType.Data,
- req.Question,
- facts,
- plan.Kql,
- req.ThreadId, // <-- reuse if provided
- ct);
-
- return new ChatResponse
- {
- Answer = run.Answer,
- UsedKql = plan.Kql,
- Preview = preview,
- ThreadId = run.ThreadId // <-- echo back the thread id used/created
- };
+ return await AnswerWithDataAssistant(req, plan, ct);
+ }
+ else if (plan.Assistant == "ploty")
+ {
+ return await AnswerWithDataAssistant(req, plan, ct);
}
else if (plan.Assistant == "docs")
{
- // AFTER
- var run = await _llm.AnswerWithAssistantAsync(
- LlmClient.AssistantType.Docs,
- req.Question,
- string.Empty,
- plan.Kql,
- req.ThreadId, // <-- reuse if provided
- ct);
-
- return new ChatResponse
- {
- Answer = run.Answer,
- ThreadId = run.ThreadId
- };
+ return await AnswerWithDocsAssistant(req, plan, ct);
}
else
{
- return new ChatResponse
- {
- Answer = plan.ConversationAnswer,
- ThreadId = req.ThreadId
- };
+ return AnswerWithPlannerConversation(req, plan);
}
}
catch (Exception ex)
@@ -118,19 +68,131 @@ namespace Tango.Portal.Chat.Web.Controllers
}
}
- private static object ToPreview(DataTable dt, int maxRows)
+ private static ActionResult<ChatResponse> AnswerWithPlannerConversation(ChatRequest req, ProposeKqlResult plan)
+ {
+ return new ChatResponse
+ {
+ Answer = plan.ConversationAnswer,
+ ThreadId = req.ThreadId
+ };
+ }
+
+ private async Task<ActionResult<ChatResponse>> AnswerWithDocsAssistant(ChatRequest req, ProposeKqlResult plan, CancellationToken ct)
+ {
+ // AFTER
+ var run = await _llm.AnswerWithAssistantAsync(
+ LlmClient.AssistantType.Docs,
+ req.Question,
+ string.Empty,
+ plan.Kql,
+ req.ThreadId, // <-- reuse if provided
+ ct);
+
+ return new ChatResponse
+ {
+ Answer = run.Answer,
+ ThreadId = run.ThreadId
+ };
+ }
+
+ private async Task<ActionResult<ChatResponse>> AnswerWithDataAssistant(ChatRequest req, ProposeKqlResult plan, CancellationToken ct)
{
- var cols = dt.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToArray();
- var rows = new List<Dictionary<string, object?>>();
- int count = 0;
- foreach (DataRow r in dt.Rows)
+ // 2) Guardrail validation
+ var val = _guard.Validate(plan.Kql);
+ if (!val.IsOk)
+ {
+ // Return error to the client so they can iterate
+ return new ChatResponse
+ {
+ Answer = $"The generated kusto query contains invalid tokens..\n{val.Error}",
+ ThreadId = req.ThreadId,
+ UsedKql = plan.Kql
+ };
+ }
+
+ // 4) Execute in ADX
+ DataTable table;
+ try
+ {
+ table = await _adx.QueryAsync(plan.Kql, plan.Parameters, ct);
+ }
+ catch (Exception ex)
+ {
+ // Return error to the client so they can iterate
+ return new ChatResponse
+ {
+ Answer = $"Seems like my kusto query ran into some issue..\n{ex.Message}",
+ ThreadId = req.ThreadId,
+ UsedKql = plan.Kql
+ };
+ }
+
+ if (table.Columns.Contains("ploty") && table.Rows.Count > 0)
+ {
+ return AnswerWithPloty(req, plan, table);
+ }
+
+ // 5) Build compact facts (limit rows/cols)
+
+ if (table.Rows.Count <= 200)
+ {
+ return await AnswerWithDataAssistantInternal(req, plan, table, ct);
+ }
+ else
{
- if (count++ >= maxRows) break;
- var d = new Dictionary<string, object?>();
- foreach (var c in cols) d[c] = r[c];
- rows.Add(d);
+ return AnswerWithMarkdownTable(req, plan, table);
}
- return new { columns = cols, rows };
+ }
+
+ private async Task<ActionResult<ChatResponse>> AnswerWithDataAssistantInternal(ChatRequest req, ProposeKqlResult plan, DataTable table, CancellationToken ct)
+ {
+ var preview = DataHelper.ToPreview(table, 200);
+ var facts = JsonSerializer.Serialize(preview);
+
+ // 6) Ask model for final answer
+ //var answer = await _llm.AnswerFromFactsAsync(req.Question, facts, plan.Kql, ct);
+
+ var run = await _llm.AnswerWithAssistantAsync(
+ LlmClient.AssistantType.Data,
+ req.Question,
+ facts,
+ plan.Kql,
+ req.ThreadId, // <-- reuse if provided
+ ct);
+
+ return new ChatResponse
+ {
+ Answer = run.Answer,
+ UsedKql = plan.Kql,
+ Preview = preview,
+ ThreadId = run.ThreadId // <-- echo back the thread id used/created
+ };
+ }
+
+ private static ActionResult<ChatResponse> AnswerWithMarkdownTable(ChatRequest req, ProposeKqlResult plan, DataTable table)
+ {
+ var markdown = DataHelper.ToMarkdownTable(table);
+
+ return new ChatResponse
+ {
+ Answer = $"The result set was too big for me to analyze so I'm just going to drop the entire result set.\n{markdown}",
+ UsedKql = plan.Kql,
+ Preview = markdown,
+ ThreadId = req.ThreadId // <-- echo back the thread id used/created
+ };
+ }
+
+ private static ActionResult<ChatResponse> AnswerWithPloty(ChatRequest req, ProposeKqlResult plan, DataTable table)
+ {
+ String? ploty = table.Rows[0]["ploty"]?.ToString();
+
+ return new ChatResponse
+ {
+ Answer = plan.ConversationAnswer,
+ ThreadId = req.ThreadId,
+ UsedKql = plan.Kql,
+ Ploty = ploty ?? String.Empty
+ };
}
}
}
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/planner_prompt.txt b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/planner_prompt.txt
index c8557a41f..b68b05755 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/planner_prompt.txt
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/planner_prompt.txt
@@ -23,24 +23,29 @@ SCHEMA USE
- If you are joining and want to project two columns with the same name append '1' to the end of the second table name. (e.g: SitesTable | join kind=inner (OrganizationsTable) on $left.ORGANIZATION_GUID == $right.GUID | project Site = NAME, Organization = NAME1).
- To get machine's organization name example: MachinesTable | where SERIAL_NUMBER == '30001' | join kind=inner (OrganizationsTable) on $left.ORGANIZATION_GUID == $right.GUID | project OrganizationName = NAME1.
- To get machine's site name example: MachinesTable | where SERIAL_NUMBER == '30001' | join kind=inner (SitesTable) on $left.SITE_GUID == $right.GUID | project SiteName = NAME1.
+- Use the LogsTable where Category == "Error" to identify machine errors.
+- When asked to generate a chart or graph, produce KQL that returns ploty output as a result, similar to the provided ploty examples, and classify as "ploty".
+- Use the provided ploty examples as a reference on how to properly design the query and ploty output.
ROUTING
- Classify into exactly one:
- "data": requires querying telemetry via KQL (counts, trends, top-N, rates, timelines, lookups).
+ - "ploty": user asked to generate a chart or graph.
- "docs": architectural/how-to/design/definitions; no live data needed.
- "none": small-talk (“thanks”, “hi”), meta-questions about how you chose a query, or requests to rephrase without needing data.
- Use conversational context from prior turns. If the current question refers to a previous result (“that”, “the same one”, “which organization is that?”), KEEP the same assistant as the previous turn unless the user clearly asks for documentation.
OUTPUT FORMAT — RAW JSON ONLY (no code fences)
Return a single JSON object with these fields:
-- assistant: "data" | "docs" | "none"
-- kql: string ("" when assistant ≠ "data")
+- assistant: "data" | "ploty" | "docs" | "none"
+- kql: string ("" when assistant ≠ "data" or "ploty")
- parameters: object with any needed parameters ({} when assistant ≠ "data"). Use fromDate/toDate when applicable.
- parameterTypes (optional): object (e.g., {"fromDate":"datetime","toDate":"datetime"})
- assumptions: array of short strings (may be empty)
- why: short natural-language rationale (string)
- conversation: string
* When assistant == "none": put a SHORT friendly reply (1–3 sentences) that addresses the user directly without KQL.
+ * When assistant == "ploty": put a title and a description of the chat/graph.
* Otherwise: set to "".
EXAMPLES
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/ploty_sample.txt b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/ploty_sample.txt
new file mode 100644
index 000000000..d4c4e0c42
--- /dev/null
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/ploty_sample.txt
@@ -0,0 +1,126 @@
+--Ploty Examples--
+
+Pie Chart of Job Statuses:
+
+let data =
+JobRunsTable
+ | summarize Completed = countif(Status == "Completed"), Failed = countif(Status == "Failed"), Aborted = countif(Status == "Aborted");
+data
+| extend TotalJobs = Completed + Failed + Aborted
+| extend ploty = strcat(
+ '{"data":[{"type":"pie",',
+ '"labels":["Completed","Failed","Aborted"],',
+ '"values":[', tostring(Completed), ',', tostring(Failed), ',', tostring(Aborted), '],',
+ '"marker":{"colors":["green","red","orange"]},',
+ '"textinfo":"label+value",',
+ '"hoverinfo":"label+value"}],',
+ '"layout":{"title":"Job Status Distribution (', tostring(TotalJobs), ')",',
+ '"showlegend":false}}'
+)
+| project ploty
+
+
+Ink Consumption by Color (L)
+
+JobRunsTable
+| summarize
+ Cyan = sum(OutputCyan),
+ Magenta = sum(OutputMagenta),
+ Yellow = sum(OutputYellow),
+ Black = sum(OutputBlack),
+ LightCyan = sum(OutputLightCyan),
+ LightMagenta = sum(OutputLightMagenta),
+ LightYellow = sum(OutputLightYellow),
+ Blue = sum(OutputBlue),
+ LightBlue = sum(OutputLightBlue),
+ Orange = sum(OutputOrange),
+ LightOrange = sum(OutputLightOrange),
+ Rubine = sum(OutputRubine),
+ LightRubine = sum(OutputLightRubine),
+ Navy = sum(OutputNavy),
+ Violet = sum(OutputViolet),
+ Transparent = sum(OutputTransparent)
+| extend keys = pack_array("Cyan","Magenta","Yellow","Black","Light Cyan","Light Magenta","Light Yellow","Blue","Light Blue","Orange","Light Orange","Rubine","Light Rubine","Navy","Violet","Transparent")
+| extend values = pack_array(
+ toreal(Cyan)/1000000000.0,
+ toreal(Magenta)/1000000000.0,
+ toreal(Yellow)/1000000000.0,
+ toreal(Black)/1000000000.0,
+ toreal(LightCyan)/1000000000.0,
+ toreal(LightMagenta)/1000000000.0,
+ toreal(LightYellow)/1000000000.0,
+ toreal(Blue)/1000000000.0,
+ toreal(LightBlue)/1000000000.0,
+ toreal(Orange)/1000000000.0,
+ toreal(LightOrange)/1000000000.0,
+ toreal(Rubine)/1000000000.0,
+ toreal(LightRubine)/1000000000.0,
+ toreal(Navy)/1000000000.0,
+ toreal(Violet)/1000000000.0,
+ toreal(Transparent)/1000000000.0
+)
+| extend colors = pack_array("#00FFFF", "#FF00FF", "#FFFF00", "#000000", "#E0FFFF", "#FFB6C1", "#FFFFE0", "#0000FF", "#ADD8E6", "#FFA500", "#FFE4B5", "#D2042D", "#FF69B4", "#000080", "#8A2BE2", "#FFFFFF")
+| extend ploty = strcat(
+ '{"data":[{"type":"bar",',
+ '"x":', tostring(keys), ',',
+ '"y":', tostring(values), ',',
+ '"marker":{"color":', tostring(colors), '}',
+ '}],',
+ '"layout":{"title":"Ink Consumption by Color (L)","xaxis":{"title":"Color"},"yaxis":{"title":"Total Ink (L)"}}}'
+)
+| project ploty
+
+
+Failing Machines (TOP 10):
+
+let data = JobRunsTable
+| where Status == "Failed"
+| summarize FailedCount = count() by SerialNumber, Site
+| top 10 by FailedCount desc
+| order by FailedCount desc
+| extend SerialNumberWithSite = strcat(SerialNumber, iif(isnull(Site), "", strcat(" (", Site, ")")));
+let x = toscalar(data | summarize make_list(SerialNumberWithSite));
+let y = toscalar(data | summarize make_list(FailedCount));
+let colors = dynamic([
+ "#B30000", "#CC0000", "#E60000", "#FF0000", "#FF2A2A",
+ "#FF3333", "#FF4D4D", "#FF6666", "#FF9999", "#FFCCCC"
+]);
+print ploty = tostring(strcat(
+ '{"data":[',
+ '{"x":', tostring(x),
+ ',"y":', tostring(y),
+ ',"type":"bar",',
+ '"marker":{"color":', tostring(colors), '}',
+ '}],',
+ '"layout":{"title":"",',
+ '"xaxis":{"title":"Machine (Site)","automargin":true},',
+ '"yaxis":{"title":"Failure Count"},',
+ '"margin":{"b":150}}}'
+))
+
+
+Resume Stats:
+
+let r1="Continuous request message 'JobRequest' had failed to provide a response for a period of 10 seconds and has timed out.";
+let r2="Transporter disconnected.";
+let ids_with_disconnect=JobRunsTable
+| where isnotempty(FailureReason)
+| where FailureReason endswith r1 or FailureReason endswith r2
+| distinct ID;
+let latest_per_id=JobRunsTable
+| summarize arg_max(CreatedTime, *) by ID
+| extend LatestIsDisconnect=isnotempty(FailureReason) and (FailureReason endswith r1 or FailureReason endswith r2);
+let totals=latest_per_id
+| where ID in (ids_with_disconnect)
+| summarize Resumed=countif(not(LatestIsDisconnect)), NotResumed=countif(LatestIsDisconnect);
+totals
+| extend ["Job Runs Recovered By Resume"]=Resumed, ["Job Runs Could not Resume"]=NotResumed, ["Total Disconnections"]=Resumed+NotResumed, ["Success Rate"]=round(100.0*todouble(Resumed)/todouble(Resumed+NotResumed),2)
+| project-away Resumed, NotResumed
+| extend keys=pack_array("Job Runs Recovered By Resume","Job Runs Could not Resume","Total Disconnections","Success Rate"),
+ values=pack_array(toreal(['Job Runs Recovered By Resume']),toreal(['Job Runs Could not Resume']),toreal(['Total Disconnections']),toreal(['Success Rate'])),
+ colors=pack_array("#28a745","#dc3545","#6c757d","#007bff")
+| extend ploty=strcat('{"data":[{"type":"bar","x":', tostring(keys), ',"y":', tostring(values), ',"marker":{"color":', tostring(colors), '}}],"layout":{"title":"Job Resume Analysis","xaxis":{"title":""},"yaxis":{"title":"Value"},"showlegend":false}}')
+
+
+
+--Ploty Examples-- \ No newline at end of file
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/schema.json b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/schema.json
index 41da6e63a..dba20ffb8 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/schema.json
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Data/schema.json
@@ -136,17 +136,91 @@
}
]
},
- "JobStatusTable": {
+ "MachineStatusesTable": {
"columns": {
"SerialNumber": "string",
+ "MachineType": "string",
+ "Environment": "string",
+ "Organization": "string",
+ "Site": "string",
"CreatedTime": "datetime",
+ "UploadTime": "datetime",
"State": "string",
"OverallTemperature": "real",
"Status": "dynamic"
},
+
+ "primaryTimeColumn": "CreatedTime",
+ "fallbackTimeColumns": [ "UploadTime" ],
+ "defaultTimeWindowDays": 1,
+
+ "keys": {
+ "machineKey": [ "SerialNumber" ]
+ },
+
+ "enums": {
+ "State": [
+ "PowerUp",
+ "Ready",
+ "Sleep",
+ "Error",
+ "ShuttingDown",
+ "PreparingJob",
+ "RunningJob",
+ "PowerDown"
+ ]
+ },
+
"notes": [
- "High-volume time series of machine status snapshots. Short retention."
- ]
+ "One row per machine status snapshot. Use CreatedTime for analysis; UploadTime is the ingestion/upload timestamp.",
+ "Status is a dynamic JSON object mirroring the on-device status payload (e.g., State, OverallTemperature, SpoolState, IDSPacksLevels[], error flags).",
+ "OverallTemperature is also duplicated at the top level for quick filtering/aggregation; the authoritative nested copy is Status.OverallTemperature.",
+ "Common expansions: Status.State (string), Status.SpoolState (string), Status.IDSPacksLevels (array of per-pack objects with MidTankLevel, DispenserLevel, etc.).",
+ "Example: latest state per machine (last 24h): MachineStatusesTable | where CreatedTime > ago(1d) | summarize arg_max(CreatedTime, *) by SerialNumber | project SerialNumber, Time=CreatedTime, State, OverallTemperature",
+ "Example: detect low MidTankLevel on any pack: MachineStatusesTable | where CreatedTime > ago(1d) | mv-expand pack=Status.IDSPacksLevels | project SerialNumber, CreatedTime, Index=todouble(pack.Index), MidTankLevel=toreal(pack.MidTankLevel) | where MidTankLevel < 2 | summarize min(MidTankLevel), any(CreatedTime) by SerialNumber, Index",
+ "Example: error intervals: MachineStatusesTable | where CreatedTime > ago(7d) | summarize Start=min(CreatedTime), End=max(CreatedTime), Count=count() by SerialNumber, State | where State == \"Error\" | order by Count desc",
+ "Example: Get the overall temperature during the last job for machine 30001: let LastJob = JobRunsTable | where SerialNumber == \"30001\" | summarize arg_max(EndTime, StartTime, EndTime) by SerialNumber; MachineStatusesTable | where SerialNumber == \"30001\" | join kind=inner (LastJob) on SerialNumber | where CreatedTime between (StartTime .. EndTime)"
+ ],
+
+ "guardrails": {
+ "requireTimeFilter": true,
+ "maxRowsSuggested": 500,
+ "encourageSummarizeOrTop": true
+ },
+
+ "joinHints": [
+ {
+ "table": "JobRunsTable",
+ "on": [ "SerialNumber" ],
+ "why": "Correlate state transitions (PreparingJob/RunningJob/Error) with job windows."
+ },
+ {
+ "table": "MachinesTable",
+ "on": [ "SerialNumber" ],
+ "why": "Enrich with machine metadata (organization/site/version)."
+ },
+ {
+ "table": "LogsTable",
+ "on": [ "SerialNumber" ],
+ "why": "Investigate logs around status changes using a time window."
+ }
+ ],
+
+ "display": {
+ "rename": {
+ "CreatedTime": "Time"
+ },
+ "suggestedProjections": [
+ "SerialNumber",
+ "Time",
+ "State",
+ "OverallTemperature",
+ "Status.SpoolState",
+ "Status.IDSPacksLevels"
+ ]
+ }
+
+
},
"MachinesTable": {
"columns": {
@@ -266,7 +340,8 @@
"Application logs emitted by services/apps running on or about the machines.",
"Use CreatedTime for time filters; UploadTime is the ingestion/upload timestamp.",
"Category is limited to Info, Warning, Error, Critical, Debug.",
- "Source can be either Application or Firmware where Firmware represents logs generated by the embedded device and Application is everything else."
+ "Source can be either Application or Firmware where Firmware represents logs generated by the embedded device and Application is everything else.",
+ "Example to get error logs associated with a specific job: let LastJob = JobRunsTable | where SerialNumber == \"30001\" | summarize arg_max(EndTime, StartTime, EndTime) by SerialNumber; LogsTable | join kind=inner (LastJob) on SerialNumber | where Category == \"Error\" | where CreatedTime between (StartTime .. EndTime) | order by CreatedTime asc | project Time = format_datetime(CreatedTime, 'HH:mm:ss.fff'), Message"
],
"guardrails": {
@@ -461,4 +536,4 @@
]
}
}
-} \ No newline at end of file
+}
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/Contracts.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/Contracts.cs
index c681e2cee..413583f2f 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/Contracts.cs
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/Contracts.cs
@@ -53,5 +53,6 @@ namespace Tango.Portal.Chat.Web.Models
public string Answer { get; set; } = string.Empty;
public string UsedKql { get; set; } = string.Empty;
public object? Preview { get; set; }
+ public string Ploty { get; set; } = string.Empty;
}
}
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/KqlGuard.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/KqlGuard.cs
index b248fb502..729aaa435 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/KqlGuard.cs
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/KqlGuard.cs
@@ -5,7 +5,7 @@ namespace Tango.Portal.Chat.Web.Services
public sealed class KqlGuard
{
private static readonly string[] Banned = new[] {
- "externaldata", "evaluate", "cluster(", "database(", "ingest", "print", "datatable", "delete", "drop", "truncate", "update", "set", "declare", "let", "materializedview", "mv-merge", "alter", "create", "append", "ingestiontime()", ".show", ".set", ".clear", ".drop", ".alter"
+ "externaldata", "evaluate", "cluster(", "database(", "ingest", "datatable", "delete", "drop", "truncate", "update", "set", "materializedview", "mv-merge", "alter", "create", "append", "ingestiontime()", ".show", ".set", ".clear", ".drop", ".alter"
};
public KqlValidationResult Validate(string kql)
@@ -13,8 +13,11 @@ namespace Tango.Portal.Chat.Web.Services
var text = kql.ToLowerInvariant();
foreach (var token in Banned)
- if (text.Contains(token))
+ {
+ var pattern = $@"\b{Regex.Escape(token)}\b";
+ if (Regex.IsMatch(text, pattern, RegexOptions.IgnoreCase))
return KqlValidationResult.Fail($"Query uses banned token: {token}");
+ }
// Ensure only allowed tables are referenced (quick heuristic)
//var tableNames = new HashSet<string>(allowTables.Select(t => t.ToLowerInvariant()));
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/LlmClient.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/LlmClient.cs
index 1577b7e08..fc970d52e 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/LlmClient.cs
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/LlmClient.cs
@@ -31,102 +31,7 @@ namespace Tango.Portal.Chat.Web.Services
_opt = opt.Value;
}
- public async Task<ProposeKqlResult> ProposeKqlAsync(string question, string schemaJson, CancellationToken ct = default)
- {
- var system = string.Join("\n", new[] {
- "You are a Kusto (KQL) assistant for Azure Data Explorer.",
- "Use ONLY the tables/columns provided in the SCHEMA JSON that follows.",
- "ALWAYS try to query for the least amount of data neccessary to answer the question.",
- "Return a JSON object with fields: assistant, kql, parameters, parameterTypes (optional), assumptions, why.",
- "When asked to query by months ago, convert number of months to days (e.g last to months = StartTime >= ago(60d))",
- "When joining tables, this example for correct syntax: EventsTable | join kind=inner (EventTypesTable) on $left.EventTypeGuid == $right.GUID.",
- "When querying MachinesTable you can fetch the machine's Organization and Site by joining the latest record from JobRunsTable by SerialNumber.",
- "Output raw JSON ONLY (no code fences).",
- @"Classify the user's question into exactly one of:
- - ""data"": requires querying telemetry via KQL (numbers, trends, counts, rates, top-N, timelines).
- - ""docs"": architectural/how-to/design/definitions/“what is/how do we” that do not require live data. place data or docs in the assistant field you return.
- - ""none"": conversational content like Thank you/Hi/How are you. Or questions about why did you chose to execute the previouse KQL query the way you did and maybe ask for correction. In this case answer nicely and try to have a good conversation without going out of context.",
- "If the question is classified as 'docs', set kql to an empty string and parameters to an empty object.",
-
-
- });
-
- var schemaBlock = $"SCHEMA:\n{schemaJson}";
- var user = $"Question: {question}\n\n{schemaBlock}";
-
- var payload = new
- {
- model = _opt.Model,
- temperature = _opt.Temperature,
- response_format = new { type = "json_object" },
- messages = new object[] {
- new { role = "system", content = system },
- new { role = "user", content = user }
- }
- };
-
- using var req = new HttpRequestMessage(HttpMethod.Post, _opt.Endpoint);
- var json = JsonSerializer.Serialize(payload);
- req.Content = new StringContent(json, Encoding.UTF8, "application/json");
-
- if (_opt.IsAzure) req.Headers.Add("api-key", _opt.ApiKey);
- else req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _opt.ApiKey);
-
- using var resp = await _http.SendAsync(req, ct);
- resp.EnsureSuccessStatusCode();
- var body = await resp.Content.ReadAsStringAsync(ct);
-
- var root = JsonNode.Parse(body)!.AsObject();
- var content = root["choices"]![0]!["message"]!["content"]?.ToString() ?? "{}";
- content = StripCodeFences(content);
-
- var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
- opts.Converters.Add(new FlexibleStringListConverter());
- try
- {
- var result = JsonSerializer.Deserialize<ProposeKqlResult>(content, opts);
- if (result != null) return result;
- }
- catch (JsonException)
- {
- // fall back
- }
-
- // Lenient mapping
- var node = JsonNode.Parse(content) as JsonObject ?? new JsonObject();
- var kql = node["kql"]?.ToString() ?? string.Empty;
-
- var parameters = new Dictionary<string, string>();
- if (node["parameters"] is JsonObject pObj)
- {
- foreach (var kv in pObj)
- parameters[kv.Key] = kv.Value?.ToString() ?? string.Empty;
- }
-
- List<string>? assumptions = null;
- if (node.TryGetPropertyValue("assumptions", out var aNode) && aNode is not null)
- {
- assumptions = new List<string>();
- if (aNode is JsonArray arr)
- {
- foreach (var el in arr) assumptions.Add(el?.ToString() ?? string.Empty);
- }
- else
- {
- assumptions.Add(aNode.ToString());
- }
- }
-
- return new ProposeKqlResult
- {
- Kql = kql,
- Parameters = parameters,
- Assumptions = assumptions,
- Why = node["why"]?.ToString()
- };
- }
-
- public async Task<ProposeKqlResult> ProposeKqlAsync(String plannerPrompt,
+ public async Task<ProposeKqlResult> ProposeKqlAsync(String plannerPrompt, String plotySample,
string question, string schemaJson, IEnumerable<ChatMessage>? history, CancellationToken ct = default)
{
var messages = new List<object> { new { role = "system", content = plannerPrompt } };
@@ -134,11 +39,11 @@ namespace Tango.Portal.Chat.Web.Services
if (history != null)
{
foreach (var m in history.TakeLast(6))
- messages.Add(new { role = m.Role, content = m.Content, usedKql = m.UsedKql });
+ messages.Add(new { role = m.Role, content = CapString(m.Content, 1000), usedKql = m.UsedKql });
}
var schemaBlock = $"SCHEMA:\n{schemaJson}";
- messages.Add(new { role = "user", content = $"Question: {question}\n\n{schemaBlock}" });
+ messages.Add(new { role = "user", content = $"Question: {question}\n\n{schemaBlock}\n\n{plotySample}" });
var payload = new
{
@@ -390,7 +295,7 @@ namespace Tango.Portal.Chat.Web.Services
// Poll until completed (same as your existing loop)
while (true)
{
- await Task.Delay(600, ct);
+ await Task.Delay(2000, ct);
using var gReq = new HttpRequestMessage(HttpMethod.Get, $"https://api.openai.com/v1/threads/{threadId}/runs/{runId}");
AddOpenAIHeaders(gReq);
var gBody = await ReadBodyOrThrowAsync(await _http.SendAsync(gReq, ct), ct);
@@ -437,6 +342,15 @@ namespace Tango.Portal.Chat.Web.Services
return Regex.Replace(s, @"\s*【[^】]*】", string.Empty);
}
+ private static string CapString(string input, int maxLength)
+ {
+ if (string.IsNullOrEmpty(input) || maxLength <= 0)
+ return string.Empty;
+
+ if (input.Length <= maxLength)
+ return input;
+ return input.Substring(0, maxLength) + "…";
+ }
}
}
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/SchemaRegistry.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/SchemaRegistry.cs
index 3ba6ad0c5..1c48b93a2 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/SchemaRegistry.cs
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/SchemaRegistry.cs
@@ -39,5 +39,16 @@ namespace Tango.Portal.Chat.Web.Services
}
return File.ReadAllText(path);
}
+
+ public string GetPlotySample()
+ {
+ var path = Path.Combine(_env.ContentRootPath, "Data", "ploty_sample.txt");
+ if (!File.Exists(path))
+ {
+ _log.LogWarning("Ploty sample file not found at {Path}. Returning empty prompt.", path);
+ return string.Empty;
+ }
+ return File.ReadAllText(path);
+ }
}
}
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Tango.Portal.Chat.Web.csproj b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Tango.Portal.Chat.Web.csproj
index 1c2028ece..3d74069d8 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Tango.Portal.Chat.Web.csproj
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Tango.Portal.Chat.Web.csproj
@@ -6,8 +6,13 @@
</PropertyGroup>
<ItemGroup>
<None Remove="Data\planner_prompt.txt" />
+ <None Remove="favicon.ico" />
+ <None Remove="favicon.png" />
</ItemGroup>
<ItemGroup>
+ <Content Include="Data\ploty_sample.txt">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="Data\planner_prompt.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -16,4 +21,12 @@
<PackageReference Include="Microsoft.Azure.Kusto.Data" Version="12.2.3" />
<PackageReference Include="Azure.Identity" Version="1.12.0" />
</ItemGroup>
+ <ItemGroup>
+ <Content Update="wwwroot\favicon.ico">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Update="wwwroot\favicon.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
</Project>
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/DataHelper.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/DataHelper.cs
new file mode 100644
index 000000000..6f9b4b22c
--- /dev/null
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/DataHelper.cs
@@ -0,0 +1,128 @@
+using Newtonsoft.Json.Linq;
+using System.Collections;
+using System.Data;
+using System.Net;
+using System.Text;
+using System.Text.Json.Nodes;
+
+namespace Tango.Portal.Chat.Web.Utils
+{
+ public static class DataHelper
+ {
+ /// <summary>
+ /// Converts a DataTable into a robust Markdown table string.
+ /// - HTML-encodes (&, <, >) to avoid HTML injection.
+ /// - Replaces '|' with &#124; so it doesn't break cell boundaries.
+ /// - Normalizes CR/LF and turns newlines into <br/> so multi-line cells stay in-row.
+ /// - Replaces tabs with spaces.
+ /// - Optionally truncates very long cells.
+ /// </summary>
+ public static string ToMarkdownTable(DataTable table, int maxCellChars = 100)
+ {
+ if (table == null || table.Columns.Count == 0) return string.Empty;
+
+ string Esc(string? s)
+ {
+ if (string.IsNullOrEmpty(s)) return string.Empty;
+
+ // HTML-encode first (safer in Markdown renderers that allow HTML)
+ string t = WebUtility.HtmlEncode(s);
+
+ // Normalize newlines and tabs
+ t = t.Replace("\r\n", "\n").Replace("\r", "\n");
+ t = t.Replace("\t", " ");
+ t = t.Replace("\n", "<br/>");
+
+ // Escape Markdown table pipes via HTML entity (more reliable than backslash)
+ t = t.Replace("|", "&#124;");
+
+ if (maxCellChars > 0 && t.Length > maxCellChars)
+ t = t.Substring(0, maxCellChars) + "…";
+
+ return t;
+ }
+
+ var sb = new StringBuilder();
+
+ // Header row
+ for (int c = 0; c < table.Columns.Count; c++)
+ sb.Append("| ").Append(Esc(table.Columns[c].ColumnName)).Append(' ');
+ sb.AppendLine("|");
+
+ // Separator row
+ for (int c = 0; c < table.Columns.Count; c++)
+ sb.Append("|---");
+ sb.AppendLine("|");
+
+ // Data rows
+ foreach (DataRow row in table.Rows)
+ {
+ for (int c = 0; c < table.Columns.Count; c++)
+ sb.Append("| ").Append(Esc(row[c]?.ToString())).Append(' ');
+ sb.AppendLine("|");
+ }
+
+ return sb.ToString();
+ }
+
+ public static object ToPreview(DataTable dt, int maxRows)
+ {
+ var cols = dt.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToArray();
+ var rows = new List<Dictionary<string, object?>>();
+ int count = 0;
+
+ foreach (DataRow r in dt.Rows)
+ {
+ if (count++ >= maxRows) break;
+
+ var d = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
+ foreach (var c in cols)
+ {
+ var cell = r[c];
+ d[c] = NormalizeCell(cell);
+ }
+ rows.Add(d);
+ }
+
+ // (Optional but helpful) include the full row count
+ return new { columns = cols, rows, totalRows = dt.Rows.Count };
+ }
+
+ private static object? NormalizeCell(object? v)
+ {
+ if (v is null || v is DBNull) return null;
+
+ // Newtonsoft JToken -> System.Text.Json node
+ if (v is JToken jt)
+ {
+ // Preserves arrays/objects rather than stringifying
+ return JsonNode.Parse(jt.ToString(Newtonsoft.Json.Formatting.None));
+ }
+
+ // Parse JSON strings (dynamic often arrives as text)
+ if (v is string s)
+ {
+ s = s.Trim();
+ if ((s.StartsWith("[") && s.EndsWith("]")) || (s.StartsWith("{") && s.EndsWith("}")))
+ {
+ try { return JsonNode.Parse(s); } catch { /* fall through */ }
+ }
+ return s; // plain string
+ }
+
+ // Flatten enumerables (but not strings)
+ if (v is IEnumerable en && v is not string)
+ {
+ var list = new List<object?>();
+ foreach (var item in en) list.Add(NormalizeCell(item));
+ return list;
+ }
+
+ // Optional: normalize DateTime -> ISO 8601 for consistency
+ if (v is DateTime dt) return dt.ToUniversalTime().ToString("o");
+
+ // Numbers, bools, etc. pass through
+ return v;
+ }
+ }
+}
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Home/Index.cshtml b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Home/Index.cshtml
index ef9144e56..a9fcb402d 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Home/Index.cshtml
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Home/Index.cshtml
@@ -36,6 +36,85 @@
<!-- Markdown renderer -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
+<link href="https://cdn.datatables.net/2.3.3/css/dataTables.dataTables.css" rel="stylesheet">
+<link href="https://cdn.datatables.net/buttons/3.2.4/css/buttons.dataTables.css" rel="stylesheet">
+<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
+<script src="https://cdn.datatables.net/2.3.3/js/dataTables.js"></script>
+<script src="https://cdn.datatables.net/buttons/3.2.4/js/dataTables.buttons.js"></script>
+<script src="https://cdn.datatables.net/buttons/3.2.4/js/buttons.dataTables.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
+<script src="https://cdn.datatables.net/buttons/3.2.4/js/buttons.html5.min.js"></script>
+<script src="https://cdn.datatables.net/buttons/3.2.4/js/buttons.print.min.js"></script>
+<script src="https://cdn.plot.ly/plotly-3.1.0.min.js" charset="utf-8"></script>
+
+
+
+
+<script>
+ function enhanceTable(el){
+ if (el.dataset.dtInitialized) return;
+ el.dataset.dtInitialized = '1';
+ new DataTable(el, {
+ layout: {
+ topStart: {
+ buttons: ['copy', 'csv', 'excel', 'pdf', 'print']
+ }
+ },
+ paging: true,
+ searching: false,
+ ordering: true,
+ info: false,
+ lengthChange: false,
+ // vertical scroll in v2:
+ scrollY: el.getAttribute('data-scroll') || 400,
+ scrollCollapse: !!el.getAttribute('data-scroll'),
+ responsive: true
+ });
+ }
+
+ document.querySelectorAll('table').forEach(enhanceTable);
+
+ new MutationObserver(muts=>{
+ for (const m of muts){
+ m.addedNodes.forEach(n=>{
+ if (n.nodeType!==1) return;
+ if (n.matches?.('table')) enhanceTable(n);
+ n.querySelectorAll?.('table').forEach(enhanceTable);
+ });
+ }
+ }).observe(document.documentElement, {childList:true, subtree:true});
+</script>
+
+<script>
+ // --- Plotly helper (parses string/object and renders as-is) ---
+ function renderPlotlyRaw(ploty, targetEl) {
+ // ploty can be a stringified JSON or an object
+ const plot =
+ typeof ploty === 'string' ? JSON.parse(ploty) :
+ (ploty && typeof ploty === 'object' ? ploty : {});
+
+ // Ensure structure for Plotly
+ plot.data = Array.isArray(plot.data) ? plot.data : [];
+ plot.layout = plot.layout || {};
+
+ // IMPORTANT: Do NOT inject msg.content into the plot title.
+ // The chart will use exactly what the server returned.
+
+ const config = { displayModeBar: true, responsive: true };
+ Plotly.newPlot(targetEl, plot.data, plot.layout, config);
+
+ // keep it responsive
+ window.addEventListener('resize', () => {
+ if (targetEl && targetEl.offsetParent !== null) {
+ Plotly.Plots.resize(targetEl);
+ }
+ }, { passive: true });
+ }
+
+</script>
+
<script>
(() => {
// ---- Ephemeral session state (lost on refresh) ----
@@ -92,6 +171,24 @@
}
bubble.appendChild(main);
+ // --- ADD: If the assistant returned a Plotly payload, render it below the markdown answer
+ if (!isUser && msg.ploty && String(msg.ploty).trim().length > 0) {
+ const chartDiv = document.createElement('div');
+ chartDiv.className = 'plotly-chart';
+ chartDiv.style.width = '100%';
+ chartDiv.style.height = '420px';
+ bubble.appendChild(chartDiv);
+
+ try {
+ renderPlotlyRaw(msg.ploty, chartDiv); // uses the helper from Step 2
+ } catch (err) {
+ const fallback = document.createElement('pre');
+ fallback.textContent = 'Failed to render chart. Raw ploty:\n' +
+ (typeof msg.ploty === 'string' ? msg.ploty : JSON.stringify(msg.ploty, null, 2));
+ bubble.appendChild(fallback);
+ }
+ }
+
// Expandable KQL when present
if (!isUser && msg.usedKql && msg.usedKql.trim().length > 0) {
const details = document.createElement('details');
@@ -207,6 +304,7 @@
role:'assistant',
content: (payload.answer || '').trim() || '(no answer)',
usedKql: (payload.usedKql || ''),
+ ploty: (payload.ploty || ''),
ts: Date.now()
};
messages.push(assistantMsg);
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Shared/_Layout.cshtml b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Shared/_Layout.cshtml
index dd23ad446..f5aece565 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Shared/_Layout.cshtml
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Views/Shared/_Layout.cshtml
@@ -7,7 +7,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Twine Solutions</title>
- <link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon.png">
+ <link rel="icon" type="image/png" href="/favicon.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:200,300,400,500,600,700,800&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Outfit:200,300,400,500,600,700,800,900&amp;display=swap">
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.Development.json b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.Development.json
index aa4ed5184..8a35484d1 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.Development.json
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.Development.json
@@ -3,7 +3,7 @@
"IsAzure": false,
"Endpoint": "https://api.openai.com/v1/chat/completions",
"ApiKey": "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",
- "Model": "gpt-4o-mini",
+ "Model": "gpt-4o",
"Temperature": 0.2,
"AnswererAssistantId": "asst_JRKGFqWUYG2rP6CptUgyVcJk",
"DocsAssistantId": "asst_HQ0C8tsdtzjENITM4qq6kFpz"
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.json b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.json
index f2ed0fa2e..e126e16bb 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.json
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.json
@@ -9,7 +9,7 @@
"IsAzure": false,
"Endpoint": "https://api.openai.com/v1/chat/completions",
"ApiKey": "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",
- "Model": "gpt-4o-mini",
+ "Model": "gpt-4o",
"Temperature": 0.2,
"AnswererAssistantId": "asst_JRKGFqWUYG2rP6CptUgyVcJk",
"DocsAssistantId": "asst_HQ0C8tsdtzjENITM4qq6kFpz"
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/assets/css/template.css b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/assets/css/template.css
index c502ebac7..daf56a861 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/assets/css/template.css
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/assets/css/template.css
@@ -16,11 +16,11 @@
}
td {
- background-color: rgba(255,0,0,0) !important;
+ /*background-color: rgba(255,0,0,0) !important;*/
}
td {
- color: var(--bs-primary) !important;
+ font-size:10pt;
}
@media (max-width: 575px) {
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/favicon.ico b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/favicon.ico
new file mode 100644
index 000000000..a3a799985
--- /dev/null
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/favicon.ico
Binary files differ
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/favicon.png b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/favicon.png
new file mode 100644
index 000000000..cd18a8beb
--- /dev/null
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/favicon.png
Binary files differ
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/site.css b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/site.css
index 66edfd085..1d3d49223 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/site.css
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/wwwroot/site.css
@@ -241,3 +241,23 @@ code {
margin-right: 3rem;
margin-bottom: 10rem;
}
+
+.main-svg
+{
+ background: Transparent !important;
+}
+
+.plotlyjsicon
+{
+ display:none !important;
+}
+
+.modebar-group
+{
+ background: Transparent !important;
+}
+
+.modebar-btn > svg > path
+{
+ fill: white !important;
+} \ No newline at end of file