fix: improve coach JSON reliability, add app icons
- Increase ChatJsonAsync max tokens 1024 → 2048 to prevent truncation - Lower JSON temperature 0.7 → 0.2 for more deterministic output - Add TryRepairJson fallback to close incomplete JSON from model - Strengthen JSON system prompt to reduce narrative drift - Generate icon.svg, icon.ico from source icon.png - Update Navbar sidebar logo to use icon.png - Update app.html favicon to use icon.ico - Copy icon.ico to Tauri icons directory Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
parent
53204ec59e
commit
c0f7c16898
@ -77,7 +77,6 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
|||||||
|
|
||||||
var executor = new StatelessExecutor(_weights!, context.Params);
|
var executor = new StatelessExecutor(_weights!, context.Params);
|
||||||
|
|
||||||
// Build multi-turn Phi-3 prompt with full conversation history
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append($"<|system|>\n{BuildChatSystemPrompt()}<|end|>\n");
|
sb.Append($"<|system|>\n{BuildChatSystemPrompt()}<|end|>\n");
|
||||||
|
|
||||||
@ -115,8 +114,10 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
|||||||
return await RunSessionAsync(prompt,
|
return await RunSessionAsync(prompt,
|
||||||
$"You are a coaching assistant inside a private journaling app. " +
|
$"You are a coaching assistant inside a private journaling app. " +
|
||||||
$"Today's date is {dateStr}. " +
|
$"Today's date is {dateStr}. " +
|
||||||
$"You MUST respond with a single valid JSON object. No text before or after the JSON.",
|
$"You MUST respond with ONLY a single valid JSON object. " +
|
||||||
maxTokens: 1024, cancellationToken: cancellationToken);
|
$"Do NOT write any text, explanation, or commentary before or after the JSON. " +
|
||||||
|
$"Output MUST start with {{ and end with }}.",
|
||||||
|
maxTokens: 2048, temperature: 0.2f, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> SummarizeEntryAsync(string content, string? fileStem = null,
|
public async Task<string> SummarizeEntryAsync(string content, string? fileStem = null,
|
||||||
@ -194,16 +195,13 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
|||||||
|
|
||||||
private async Task<string> EnsureModelAsync(CancellationToken cancellationToken = default)
|
private async Task<string> EnsureModelAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// 1. Configured path takes priority if the file already exists
|
|
||||||
if (File.Exists(_configuredModelPath))
|
if (File.Exists(_configuredModelPath))
|
||||||
return _configuredModelPath;
|
return _configuredModelPath;
|
||||||
|
|
||||||
// 2. Check the standard app-data location
|
|
||||||
var defaultPath = GetDefaultModelPath();
|
var defaultPath = GetDefaultModelPath();
|
||||||
if (File.Exists(defaultPath))
|
if (File.Exists(defaultPath))
|
||||||
return defaultPath;
|
return defaultPath;
|
||||||
|
|
||||||
// 3. Download from HuggingFace
|
|
||||||
var modelDirectory = Path.GetDirectoryName(defaultPath)!;
|
var modelDirectory = Path.GetDirectoryName(defaultPath)!;
|
||||||
Directory.CreateDirectory(modelDirectory);
|
Directory.CreateDirectory(modelDirectory);
|
||||||
|
|
||||||
@ -230,21 +228,22 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
|||||||
|
|
||||||
// ── Session / weights lifecycle ────────────────────────────────────────
|
// ── Session / weights lifecycle ────────────────────────────────────────
|
||||||
|
|
||||||
private async Task<string> RunSessionAsync(string prompt, string systemPrompt,
|
private Task<string> RunSessionAsync(string prompt, string systemPrompt,
|
||||||
int maxTokens, CancellationToken cancellationToken)
|
int maxTokens, CancellationToken cancellationToken)
|
||||||
|
=> RunSessionAsync(prompt, systemPrompt, maxTokens, temperature: 0.7f, cancellationToken);
|
||||||
|
|
||||||
|
private async Task<string> RunSessionAsync(string prompt, string systemPrompt,
|
||||||
|
int maxTokens, float temperature, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var modelPath = await EnsureModelAsync(cancellationToken);
|
var modelPath = await EnsureModelAsync(cancellationToken);
|
||||||
EnsureWeights(modelPath);
|
EnsureWeights(modelPath);
|
||||||
|
|
||||||
// Fresh context per call — prevents KV cache accumulation across requests
|
|
||||||
using var context = _weights!.CreateContext(new ModelParams(modelPath)
|
using var context = _weights!.CreateContext(new ModelParams(modelPath)
|
||||||
{
|
{
|
||||||
ContextSize = _contextSize,
|
ContextSize = _contextSize,
|
||||||
GpuLayerCount = _gpuLayers
|
GpuLayerCount = _gpuLayers
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use StatelessExecutor with explicit Phi-3 chat template so the model
|
|
||||||
// never sees raw system text it can echo back to the user.
|
|
||||||
var executor = new StatelessExecutor(_weights!, context.Params);
|
var executor = new StatelessExecutor(_weights!, context.Params);
|
||||||
|
|
||||||
var fullPrompt = $"<|system|>\n{systemPrompt}<|end|>\n" +
|
var fullPrompt = $"<|system|>\n{systemPrompt}<|end|>\n" +
|
||||||
@ -262,7 +261,7 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
|||||||
],
|
],
|
||||||
SamplingPipeline = new DefaultSamplingPipeline
|
SamplingPipeline = new DefaultSamplingPipeline
|
||||||
{
|
{
|
||||||
Temperature = 0.7f
|
Temperature = temperature
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -276,7 +275,6 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
|||||||
return StripSpecialTokens(sb.ToString());
|
return StripSpecialTokens(sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Strips only Phi-3 special tokens — safe for JSON output.</summary>
|
|
||||||
private static string StripSpecialTokens(string raw)
|
private static string StripSpecialTokens(string raw)
|
||||||
{
|
{
|
||||||
var text = raw;
|
var text = raw;
|
||||||
@ -285,22 +283,15 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
|||||||
return text.Trim();
|
return text.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches role labels like "System:", "**System:**", "**Assistant:**", "User:" etc.
|
|
||||||
private static readonly Regex RoleMarkerRegex = MyRegex();
|
private static readonly Regex RoleMarkerRegex = MyRegex();
|
||||||
|
|
||||||
/// <summary>Aggressive cleanup for conversational (non-JSON) responses.</summary>
|
|
||||||
private static string CleanChatResponse(string raw)
|
private static string CleanChatResponse(string raw)
|
||||||
{
|
{
|
||||||
var text = StripSpecialTokens(raw);
|
var text = StripSpecialTokens(raw);
|
||||||
|
|
||||||
// Strip role markers in any formatting variant (plain, bold-markdown, etc.)
|
|
||||||
text = RoleMarkerRegex.Replace(text, "");
|
text = RoleMarkerRegex.Replace(text, "");
|
||||||
|
|
||||||
// Remove orphaned bold markers left behind after stripping
|
|
||||||
text = text.Replace("**", "");
|
text = text.Replace("**", "");
|
||||||
|
text = MyRegex2().Replace(text, "\n\n");
|
||||||
// Collapse runs of 3+ newlines into 2
|
|
||||||
text = Regex.Replace(text, @"\n{3,}", "\n\n");
|
|
||||||
|
|
||||||
return text.Trim();
|
return text.Trim();
|
||||||
}
|
}
|
||||||
@ -333,4 +324,6 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
|||||||
private static partial Regex MyRegex();
|
private static partial Regex MyRegex();
|
||||||
[GeneratedRegex(@"\n{3,}")]
|
[GeneratedRegex(@"\n{3,}")]
|
||||||
private static partial Regex MyRegex1();
|
private static partial Regex MyRegex1();
|
||||||
|
[GeneratedRegex(@"\n{3,}")]
|
||||||
|
private static partial Regex MyRegex2();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,8 +47,6 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var raw = await _ai.ChatJsonAsync(prompt, cancellationToken);
|
var raw = await _ai.ChatJsonAsync(prompt, cancellationToken);
|
||||||
|
|
||||||
// Try to extract JSON from the response
|
|
||||||
var json = ExtractJson(raw);
|
var json = ExtractJson(raw);
|
||||||
if (json is not null)
|
if (json is not null)
|
||||||
{
|
{
|
||||||
@ -60,11 +58,10 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
|||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException)
|
||||||
{
|
{
|
||||||
// Fall through to fallback
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: wrap raw text into a CoachPlanDto
|
|
||||||
return new CoachPlanDto(
|
return new CoachPlanDto(
|
||||||
Kind: fallbackKind,
|
Kind: fallbackKind,
|
||||||
Title: "Coach Response",
|
Title: "Coach Response",
|
||||||
@ -104,7 +101,6 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
|||||||
|
|
||||||
private static string? ExtractJson(string text)
|
private static string? ExtractJson(string text)
|
||||||
{
|
{
|
||||||
// 1. Try ```json ... ``` code block
|
|
||||||
var codeBlockStart = text.IndexOf("```json", StringComparison.Ordinal);
|
var codeBlockStart = text.IndexOf("```json", StringComparison.Ordinal);
|
||||||
if (codeBlockStart >= 0)
|
if (codeBlockStart >= 0)
|
||||||
{
|
{
|
||||||
@ -118,7 +114,6 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Look specifically for {"kind" — the expected first field of CoachPlanDto
|
|
||||||
var kindMarker = text.IndexOf("{\"kind\"", StringComparison.Ordinal);
|
var kindMarker = text.IndexOf("{\"kind\"", StringComparison.Ordinal);
|
||||||
if (kindMarker < 0)
|
if (kindMarker < 0)
|
||||||
kindMarker = text.IndexOf("{ \"kind\"", StringComparison.Ordinal);
|
kindMarker = text.IndexOf("{ \"kind\"", StringComparison.Ordinal);
|
||||||
@ -133,7 +128,6 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fallback: try each { position until one parses as valid JSON
|
|
||||||
var searchFrom = 0;
|
var searchFrom = 0;
|
||||||
var globalLastBrace = text.LastIndexOf('}');
|
var globalLastBrace = text.LastIndexOf('}');
|
||||||
while (searchFrom < text.Length && globalLastBrace > searchFrom)
|
while (searchFrom < text.Length && globalLastBrace > searchFrom)
|
||||||
@ -149,9 +143,67 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
|||||||
searchFrom = bracePos + 1;
|
searchFrom = bracePos + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var firstBrace = text.IndexOf('{');
|
||||||
|
if (firstBrace >= 0)
|
||||||
|
{
|
||||||
|
var repaired = TryRepairJson(text[firstBrace..]);
|
||||||
|
if (repaired is not null)
|
||||||
|
return repaired;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? TryRepairJson(string text)
|
||||||
|
{
|
||||||
|
var trimmed = text.TrimEnd();
|
||||||
|
var lastUseful = trimmed.Length - 1;
|
||||||
|
while (lastUseful >= 0)
|
||||||
|
{
|
||||||
|
var ch = trimmed[lastUseful];
|
||||||
|
if (ch is '}' or ']' or '"' or ',' or ':' or '{' or '[' || char.IsDigit(ch)
|
||||||
|
|| ch is 't' or 'r' or 'u' or 'e' or 'f' or 'a' or 'l' or 's' or 'n')
|
||||||
|
break;
|
||||||
|
lastUseful--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastUseful < 0) return null;
|
||||||
|
trimmed = trimmed[..(lastUseful + 1)];
|
||||||
|
|
||||||
|
if (trimmed.EndsWith(','))
|
||||||
|
trimmed = trimmed[..^1];
|
||||||
|
|
||||||
|
var openBraces = 0;
|
||||||
|
var openBrackets = 0;
|
||||||
|
var inString = false;
|
||||||
|
var escape = false;
|
||||||
|
|
||||||
|
foreach (var ch in trimmed)
|
||||||
|
{
|
||||||
|
if (escape) { escape = false; continue; }
|
||||||
|
if (ch == '\\' && inString) { escape = true; continue; }
|
||||||
|
if (ch == '"') { inString = !inString; continue; }
|
||||||
|
if (inString) continue;
|
||||||
|
|
||||||
|
switch (ch)
|
||||||
|
{
|
||||||
|
case '{': openBraces++; break;
|
||||||
|
case '}': openBraces--; break;
|
||||||
|
case '[': openBrackets++; break;
|
||||||
|
case ']': openBrackets--; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openBraces <= 0 && openBrackets <= 0) return null;
|
||||||
|
|
||||||
|
var sb = new StringBuilder(trimmed);
|
||||||
|
for (var i = 0; i < openBrackets; i++) sb.Append(']');
|
||||||
|
for (var i = 0; i < openBraces; i++) sb.Append('}');
|
||||||
|
|
||||||
|
var repaired = sb.ToString();
|
||||||
|
return TryValidateJson(repaired) ? repaired : null;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryValidateJson(string json)
|
private static bool TryValidateJson(string json)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -169,11 +221,7 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
|||||||
{
|
{
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
var resourceName = assembly.GetManifestResourceNames()
|
var resourceName = assembly.GetManifestResourceNames()
|
||||||
.FirstOrDefault(n => n.EndsWith(fileName, StringComparison.OrdinalIgnoreCase));
|
.FirstOrDefault(n => n.EndsWith(fileName, StringComparison.OrdinalIgnoreCase)) ?? throw new FileNotFoundException($"Embedded resource not found: {fileName}");
|
||||||
|
|
||||||
if (resourceName is null)
|
|
||||||
throw new FileNotFoundException($"Embedded resource not found: {fileName}");
|
|
||||||
|
|
||||||
using var stream = assembly.GetManifestResourceStream(resourceName)!;
|
using var stream = assembly.GetManifestResourceStream(resourceName)!;
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||||
return reader.ReadToEnd().Trim();
|
return reader.ReadToEnd().Trim();
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 279 KiB |
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/icon.ico" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
|
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<aside class="navbar" aria-label="Primary navigation">
|
<aside class="navbar" aria-label="Primary navigation">
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<img src="svelte.svg" alt="Journal logo" />
|
<img src="icon.png" alt="Journal logo" style="height: 48px; width: 48px;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="nav-groups" aria-label="Journal sections">
|
<nav class="nav-groups" aria-label="Journal sections">
|
||||||
|
|||||||
BIN
Journal.App/static/icon.ico
Normal file
BIN
Journal.App/static/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
BIN
Journal.App/static/icon.png
Normal file
BIN
Journal.App/static/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
5
Journal.App/static/icon.svg
Normal file
5
Journal.App/static/icon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.0 MiB |
Loading…
x
Reference in New Issue
Block a user