Token-Optimierung: Prompt Caching + Tool-Result-Truncation in History
This commit is contained in:
1 parent
ec5917ef63
commit
f74409dd89
12 files changed
+220
-59
No files matched your search
@@ -77,36 +77,7 @@ public static class ChatEndpoint
|
|||||||
res.Headers["Cache-Control"] = "no-cache";
|
res.Headers["Cache-Control"] = "no-cache";
|
||||||
res.Headers["Connection"] = "keep-alive";
|
res.Headers["Connection"] = "keep-alive";
|
||||||
|
|
||||||
var promptParts = new List<string>();
|
var systemPromptText = SystemPromptBuilder.Build(agent, session);
|
||||||
promptParts.Add($"Du bist {agent.Name}.");
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(agent.Persona))
|
|
||||||
{
|
|
||||||
promptParts.Add($"## Persona\n{agent.Persona}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(agent.Instructions))
|
|
||||||
{
|
|
||||||
promptParts.Add($"## Instructions\n{agent.Instructions}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(session.Workspace))
|
|
||||||
{
|
|
||||||
promptParts.Add(@$"## Tools
|
|
||||||
Du hast Tools zur Verfügung (read_file, write_file, edit_file, list_files, delete_file, execute). Benutze sie um Dateien zu lesen, schreiben und bearbeiten.
|
|
||||||
|
|
||||||
Der Workspace-Pfad ist: `{session.Workspace}`
|
|
||||||
Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder absolute Pfade verwenden, die mit diesem Pfad beginnen.
|
|
||||||
|
|
||||||
Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: foo.txt:11]`), zähle Leerzeilen immer mit. Verwende `cat -n` oder ein ähnliches Tool um Zeilennummern exakt zu bestimmen - NIEMALS Leerzeilen beim Zählen überspringen.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(session.Context))
|
|
||||||
{
|
|
||||||
promptParts.Add($"## Kontext\n{session.Context}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemPromptText = string.Join("\n\n", promptParts);
|
|
||||||
|
|
||||||
var chainId = Guid.NewGuid().ToString("N");
|
var chainId = Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
@@ -171,9 +142,11 @@ Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: fo
|
|||||||
}, attachmentRecords);
|
}, attachmentRecords);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cache_control markiert den System-Prompt als Cache-Breakpoint.
|
||||||
|
// Anthropic cached alles bis einschließlich dieses Blocks für 5 Minuten → 90% Rabatt auf diese Tokens.
|
||||||
var systemPrompt = new List<object>
|
var systemPrompt = new List<object>
|
||||||
{
|
{
|
||||||
new { type = "text", text = systemPromptText }
|
new { type = "text", text = systemPromptText, cache_control = new { type = "ephemeral" } }
|
||||||
};
|
};
|
||||||
|
|
||||||
var workspace = session.Workspace;
|
var workspace = session.Workspace;
|
||||||
|
|||||||
@@ -121,18 +121,7 @@ public static class SessionEndpoint
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var promptParts = new List<string>();
|
var systemPrompt = SystemPromptBuilder.Build(agent, session);
|
||||||
promptParts.Add($"Du bist {agent.Name}.");
|
|
||||||
if (!string.IsNullOrWhiteSpace(agent.Persona))
|
|
||||||
promptParts.Add($"## Persona\n{agent.Persona}");
|
|
||||||
if (!string.IsNullOrWhiteSpace(agent.Instructions))
|
|
||||||
promptParts.Add($"## Instructions\n{agent.Instructions}");
|
|
||||||
if (!string.IsNullOrWhiteSpace(session.Workspace))
|
|
||||||
promptParts.Add("## Tools\nDu hast Tools zur Verfügung (read_file, write_file, edit_file, list_files, delete_file, execute). Benutze sie um Dateien zu lesen, schreiben und bearbeiten. Beschreibe NICHT was du tun würdest - tu es einfach über die Tools.");
|
|
||||||
if (!string.IsNullOrWhiteSpace(session.Context))
|
|
||||||
promptParts.Add($"## Kontext\n{session.Context}");
|
|
||||||
|
|
||||||
var systemPrompt = string.Join("\n\n", promptParts);
|
|
||||||
|
|
||||||
ctx.Response.ContentType = "text/plain; charset=utf-8";
|
ctx.Response.ContentType = "text/plain; charset=utf-8";
|
||||||
await ctx.Response.WriteAsync(systemPrompt);
|
await ctx.Response.WriteAsync(systemPrompt);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ public class ChainRecord
|
|||||||
public string SystemPrompt { get; set; }
|
public string SystemPrompt { get; set; }
|
||||||
public long InputTokens { get; set; }
|
public long InputTokens { get; set; }
|
||||||
public long OutputTokens { get; set; }
|
public long OutputTokens { get; set; }
|
||||||
|
public long CacheCreationTokens { get; set; }
|
||||||
|
public long CacheReadTokens { get; set; }
|
||||||
public int Iterations { get; set; }
|
public int Iterations { get; set; }
|
||||||
public string StopReason { get; set; }
|
public string StopReason { get; set; }
|
||||||
public long DurationMs { get; set; }
|
public long DurationMs { get; set; }
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public class AnthropicService
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(workspace))
|
if (!string.IsNullOrEmpty(workspace))
|
||||||
payload["tools"] = _toolService.GetToolDefinitions();
|
payload["tools"] = _toolService.GetToolDefinitions(markLastAsCached: true);
|
||||||
|
|
||||||
var client = CreateClient();
|
var client = CreateClient();
|
||||||
var json = JsonSerializer.Serialize(payload);
|
var json = JsonSerializer.Serialize(payload);
|
||||||
@@ -150,6 +150,8 @@ public class AnthropicService
|
|||||||
model = modelId,
|
model = modelId,
|
||||||
inputTokens = usage.InputTokens,
|
inputTokens = usage.InputTokens,
|
||||||
outputTokens = usage.OutputTokens,
|
outputTokens = usage.OutputTokens,
|
||||||
|
cacheCreationTokens = usage.CacheCreationTokens,
|
||||||
|
cacheReadTokens = usage.CacheReadTokens,
|
||||||
iterations,
|
iterations,
|
||||||
chainId,
|
chainId,
|
||||||
durationMs = sw.ElapsedMilliseconds,
|
durationMs = sw.ElapsedMilliseconds,
|
||||||
@@ -161,11 +163,20 @@ public class AnthropicService
|
|||||||
var toolResults = new List<object>();
|
var toolResults = new List<object>();
|
||||||
foreach (var tr in executedTools)
|
foreach (var tr in executedTools)
|
||||||
{
|
{
|
||||||
|
// Tool-Results in der History auf 20.000 Zeichen kürzen.
|
||||||
|
// Das vollständige Ergebnis wurde bereits in der DB gespeichert.
|
||||||
|
// 20k Zeichen ≈ ~5k Tokens – verhindert Token-Explosion bei großen
|
||||||
|
// read_file / execute-Outputs die sich über alle weiteren Iterationen schleppen.
|
||||||
|
const int MaxToolResultChars = 20_000;
|
||||||
|
var content = tr.Result.Length > MaxToolResultChars
|
||||||
|
? tr.Result[..MaxToolResultChars] + $"\n\n[... gekürzt, {tr.Result.Length - MaxToolResultChars} weitere Zeichen ...]"
|
||||||
|
: tr.Result;
|
||||||
|
|
||||||
toolResults.Add(new
|
toolResults.Add(new
|
||||||
{
|
{
|
||||||
type = "tool_result",
|
type = "tool_result",
|
||||||
tool_use_id = tr.Id,
|
tool_use_id = tr.Id,
|
||||||
content = tr.Result,
|
content,
|
||||||
is_error = tr.IsError
|
is_error = tr.IsError
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -202,6 +213,8 @@ public class AnthropicService
|
|||||||
{
|
{
|
||||||
chain.InputTokens = usage.InputTokens;
|
chain.InputTokens = usage.InputTokens;
|
||||||
chain.OutputTokens = usage.OutputTokens;
|
chain.OutputTokens = usage.OutputTokens;
|
||||||
|
chain.CacheCreationTokens = usage.CacheCreationTokens;
|
||||||
|
chain.CacheReadTokens = usage.CacheReadTokens;
|
||||||
chain.Iterations = iterations;
|
chain.Iterations = iterations;
|
||||||
chain.StopReason = stopReason;
|
chain.StopReason = stopReason;
|
||||||
chain.DurationMs = sw.ElapsedMilliseconds;
|
chain.DurationMs = sw.ElapsedMilliseconds;
|
||||||
@@ -246,9 +259,15 @@ public class AnthropicService
|
|||||||
case "message_start":
|
case "message_start":
|
||||||
{
|
{
|
||||||
if (evt.TryGetProperty("message", out var msg) &&
|
if (evt.TryGetProperty("message", out var msg) &&
|
||||||
msg.TryGetProperty("usage", out var u) &&
|
msg.TryGetProperty("usage", out var u))
|
||||||
u.TryGetProperty("input_tokens", out var it))
|
{
|
||||||
usage.InputTokens += it.GetInt64();
|
if (u.TryGetProperty("input_tokens", out var it))
|
||||||
|
usage.InputTokens += it.GetInt64();
|
||||||
|
if (u.TryGetProperty("cache_creation_input_tokens", out var cc))
|
||||||
|
usage.CacheCreationTokens += cc.GetInt64();
|
||||||
|
if (u.TryGetProperty("cache_read_input_tokens", out var cr))
|
||||||
|
usage.CacheReadTokens += cr.GetInt64();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,4 +424,6 @@ public class TokenUsage
|
|||||||
{
|
{
|
||||||
public long InputTokens { get; set; }
|
public long InputTokens { get; set; }
|
||||||
public long OutputTokens { get; set; }
|
public long OutputTokens { get; set; }
|
||||||
|
public long CacheCreationTokens { get; set; }
|
||||||
|
public long CacheReadTokens { get; set; }
|
||||||
}
|
}
|
||||||
@@ -122,6 +122,8 @@ public class DatabaseService
|
|||||||
TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0");
|
TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0");
|
||||||
TryAddColumn(conn, "sessions", "custom_commands", "TEXT");
|
TryAddColumn(conn, "sessions", "custom_commands", "TEXT");
|
||||||
TryAddColumn(conn, "chains", "agent_id", "TEXT");
|
TryAddColumn(conn, "chains", "agent_id", "TEXT");
|
||||||
|
TryAddColumn(conn, "chains", "cache_creation_tokens", "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
TryAddColumn(conn, "chains", "cache_read_tokens", "INTEGER NOT NULL DEFAULT 0");
|
||||||
TryAddColumn(conn, "messages", "attachment_count", "INTEGER NOT NULL DEFAULT 0");
|
TryAddColumn(conn, "messages", "attachment_count", "INTEGER NOT NULL DEFAULT 0");
|
||||||
TryAddColumn(conn, "messages", "attachment_names", "TEXT");
|
TryAddColumn(conn, "messages", "attachment_names", "TEXT");
|
||||||
}
|
}
|
||||||
@@ -376,6 +378,8 @@ public class DatabaseService
|
|||||||
UPDATE chains SET
|
UPDATE chains SET
|
||||||
input_tokens = @InputTokens,
|
input_tokens = @InputTokens,
|
||||||
output_tokens = @OutputTokens,
|
output_tokens = @OutputTokens,
|
||||||
|
cache_creation_tokens = @CacheCreationTokens,
|
||||||
|
cache_read_tokens = @CacheReadTokens,
|
||||||
iterations = @Iterations,
|
iterations = @Iterations,
|
||||||
stop_reason = @StopReason,
|
stop_reason = @StopReason,
|
||||||
duration_ms = @DurationMs
|
duration_ms = @DurationMs
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Gott.Models;
|
||||||
|
|
||||||
|
namespace Gott.Services;
|
||||||
|
|
||||||
|
public static class SystemPromptBuilder
|
||||||
|
{
|
||||||
|
public static string Build(AgentRecord agent, SessionRecord session)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
parts.Add($"Du bist {agent.Name}.");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(agent.Persona))
|
||||||
|
parts.Add($"## Persona\n{agent.Persona}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(agent.Instructions))
|
||||||
|
parts.Add($"## Instructions\n{agent.Instructions}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(session.Workspace))
|
||||||
|
{
|
||||||
|
parts.Add(@$"## Tools
|
||||||
|
Du hast Tools zur Verfügung (read_file, write_file, edit_file, list_files, delete_file, execute). Benutze sie um Dateien zu lesen, schreiben und bearbeiten.
|
||||||
|
|
||||||
|
Der Workspace-Pfad ist: `{session.Workspace}`
|
||||||
|
Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder absolute Pfade verwenden, die mit diesem Pfad beginnen.
|
||||||
|
|
||||||
|
Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: foo.txt:11]`), zähle Leerzeilen immer mit. Verwende `cat -n` oder ein ähnliches Tool um Zeilennummern exakt zu bestimmen - NIEMALS Leerzeilen beim Zählen überspringen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(session.Context))
|
||||||
|
parts.Add($"## Kontext\n{session.Context}");
|
||||||
|
|
||||||
|
return string.Join("\n\n", parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-11
@@ -11,7 +11,7 @@ namespace Gott.Services;
|
|||||||
|
|
||||||
public class ToolService
|
public class ToolService
|
||||||
{
|
{
|
||||||
public object[] GetToolDefinitions()
|
public object[] GetToolDefinitions(bool markLastAsCached = false)
|
||||||
{
|
{
|
||||||
return new object[]
|
return new object[]
|
||||||
{
|
{
|
||||||
@@ -88,20 +88,38 @@ public class ToolService
|
|||||||
required = new[] { "path" }
|
required = new[] { "path" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new
|
// Letztes Tool bekommt cache_control – Anthropic cached alles bis einschließlich
|
||||||
{
|
// dieses Breakpoints (System-Prompt + alle Tool-Definitionen) für 5 Minuten.
|
||||||
name = "execute",
|
markLastAsCached
|
||||||
description = "Führt einen Shell-Befehl im Workspace-Verzeichnis aus. Gibt stdout und stderr zurück. Timeout: 30 Sekunden.",
|
? (object)new
|
||||||
input_schema = new
|
|
||||||
{
|
{
|
||||||
type = "object",
|
name = "execute",
|
||||||
properties = new Dictionary<string, object>
|
description = "Führt einen Shell-Befehl im Workspace-Verzeichnis aus. Gibt stdout und stderr zurück. Timeout: 30 Sekunden.",
|
||||||
|
input_schema = new
|
||||||
{
|
{
|
||||||
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen" }
|
type = "object",
|
||||||
|
properties = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen" }
|
||||||
|
},
|
||||||
|
required = new[] { "command" }
|
||||||
},
|
},
|
||||||
required = new[] { "command" }
|
cache_control = new { type = "ephemeral" }
|
||||||
|
}
|
||||||
|
: new
|
||||||
|
{
|
||||||
|
name = "execute",
|
||||||
|
description = "Führt einen Shell-Befehl im Workspace-Verzeichnis aus. Gibt stdout und stderr zurück. Timeout: 30 Sekunden.",
|
||||||
|
input_schema = new
|
||||||
|
{
|
||||||
|
type = "object",
|
||||||
|
properties = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen" }
|
||||||
|
},
|
||||||
|
required = new[] { "command" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -383,3 +383,56 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* System-Prompt Modal */
|
||||||
|
|
||||||
|
.sp-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-modal {
|
||||||
|
background: var(--f2-bg);
|
||||||
|
border: var(--border-width) solid var(--f2-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 700px;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--box-padding);
|
||||||
|
border-bottom: var(--border-width) solid var(--f1-border);
|
||||||
|
background: var(--f1-bg);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-close {
|
||||||
|
width: 2em;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-body {
|
||||||
|
padding: var(--page-padding);
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-loading {
|
||||||
|
text-align: center;
|
||||||
|
opacity: var(--semi-transparent);
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
@@ -144,6 +144,7 @@
|
|||||||
<div class="header-menu">
|
<div class="header-menu">
|
||||||
<button class="header-menu-trigger" id="chatMenuBtn">⋮</button>
|
<button class="header-menu-trigger" id="chatMenuBtn">⋮</button>
|
||||||
<div class="header-menu-dropdown" id="chatMenuDropdown" hidden>
|
<div class="header-menu-dropdown" id="chatMenuDropdown" hidden>
|
||||||
|
<button class="header-menu-item" id="systemPromptBtn">System-Prompt anzeigen</button>
|
||||||
<button class="header-menu-item" id="archiveBtn">Archivieren</button>
|
<button class="header-menu-item" id="archiveBtn">Archivieren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ async function apiExecCommand(sessionId, command) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function apiGetSystemPrompt(sessionId) {
|
||||||
|
const resp = await fetch(`/api/sessions/${sessionId}/system-prompt`);
|
||||||
|
if (!resp.ok) throw new Error('System-Prompt konnte nicht geladen werden');
|
||||||
|
return resp.text();
|
||||||
|
}
|
||||||
|
|
||||||
async function apiIsStreaming(sessionId) {
|
async function apiIsStreaming(sessionId) {
|
||||||
const resp = await fetch(`/api/sessions/${sessionId}/streaming`);
|
const resp = await fetch(`/api/sessions/${sessionId}/streaming`);
|
||||||
return resp.json();
|
return resp.json();
|
||||||
|
|||||||
@@ -183,6 +183,13 @@ registerPage('chat', {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// System-Prompt anzeigen
|
||||||
|
const systemPromptBtn = document.getElementById('systemPromptBtn');
|
||||||
|
if (systemPromptBtn) systemPromptBtn.addEventListener('click', () => {
|
||||||
|
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
|
||||||
|
showSystemPromptModal(sessionId);
|
||||||
|
});
|
||||||
|
|
||||||
// Archivieren
|
// Archivieren
|
||||||
const archiveBtn = document.getElementById('archiveBtn');
|
const archiveBtn = document.getElementById('archiveBtn');
|
||||||
if (archiveBtn) archiveBtn.addEventListener('click', () => {
|
if (archiveBtn) archiveBtn.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -409,6 +409,58 @@ async function loadAndShowAttachments(messageId, indicator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── System-Prompt Modal ──
|
||||||
|
|
||||||
|
async function showSystemPromptModal(sessionId) {
|
||||||
|
// Overlay
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'sp-overlay';
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'sp-modal';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'sp-header';
|
||||||
|
const title = document.createElement('span');
|
||||||
|
title.textContent = 'System-Prompt';
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.textContent = '✕';
|
||||||
|
closeBtn.className = 'sp-close';
|
||||||
|
header.appendChild(title);
|
||||||
|
header.appendChild(closeBtn);
|
||||||
|
|
||||||
|
// Body
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'sp-body markdown';
|
||||||
|
body.innerHTML = '<div class="sp-loading">Laden...</div>';
|
||||||
|
|
||||||
|
modal.appendChild(header);
|
||||||
|
modal.appendChild(body);
|
||||||
|
overlay.appendChild(modal);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Schließen
|
||||||
|
const close = () => overlay.remove();
|
||||||
|
closeBtn.addEventListener('click', close);
|
||||||
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay) close();
|
||||||
|
});
|
||||||
|
const onKey = (e) => {
|
||||||
|
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
|
||||||
|
// Laden
|
||||||
|
try {
|
||||||
|
const prompt = await apiGetSystemPrompt(sessionId);
|
||||||
|
body.innerHTML = renderMarkdown(prompt);
|
||||||
|
} catch (e) {
|
||||||
|
body.innerHTML = '<div class="sp-loading" style="color:var(--dangerous)">Fehler beim Laden des System-Prompts</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Meta Bar ──
|
// ── Meta Bar ──
|
||||||
|
|
||||||
function setMeta(text) {
|
function setMeta(text) {
|
||||||
|
|||||||
Reference in new issue
Block a user