Gemini Test

This commit is contained in:
Michael Schimmel
2026-01-08 17:50:29 +01:00
parent 0efce47972
commit f35dafeac2
+359
View File
@@ -0,0 +1,359 @@
unit Myc.Api.MarkdownStream;
interface
uses
System.SysUtils,
System.Classes,
System.IOUtils,
System.Threading,
System.Generics.Collections,
System.JSON,
FMX.WebBrowser;
type
TBlockType = (btMarkdown, btRawText, btHtml);
/// <summary>
/// Manages an incremental Markdown/HTML stream within a TWebBrowser.
/// Supports block-based rendering and collapsible raw text sections.
/// </summary>
IMarkdownStream = interface
['{A1B2C3D4-E5F6-4788-9900-112233445566}']
procedure InitializeBrowser(const Browser: TWebBrowser);
procedure BeginBlock(const AType: TBlockType; const ATitle: string = '');
procedure Append(const Content: string);
procedure Clear;
end;
TMarkdownStream = class(TInterfacedObject, IMarkdownStream)
strict private
type
TCmd = record
Func: string;
Arg: string;
constructor Create(const F, A: string);
end;
strict private
FBrowser: TWebBrowser;
FIsReady: Boolean;
FCommandQueue: TList<TCmd>;
FTempFileName: string;
procedure OnDidFinishLoad(Sender: TObject);
function GetBaseHtml: string;
procedure ExecuteJS(const FunctionName, Data: string);
procedure FlushQueue;
function BlockTypeToString(const AType: TBlockType): string;
public
constructor Create;
destructor Destroy; override;
procedure InitializeBrowser(const Browser: TWebBrowser);
procedure BeginBlock(const AType: TBlockType; const ATitle: string = '');
procedure Append(const Content: string);
procedure Clear;
end;
implementation
{ TMarkdownStream.TCmd }
constructor TMarkdownStream.TCmd.Create(const F, A: string);
begin
Func := F;
Arg := A;
end;
{ TMarkdownStream }
constructor TMarkdownStream.Create;
begin
inherited;
FCommandQueue := TList<TCmd>.Create;
FIsReady := False;
FTempFileName := TPath.Combine(TPath.GetTempPath, 'myc_md_stream.html');
end;
destructor TMarkdownStream.Destroy;
begin
if Assigned(FBrowser) then
FBrowser.OnDidFinishLoad := nil;
FCommandQueue.Free;
if FileExists(FTempFileName) then
try
TFile.Delete(FTempFileName);
except
end;
inherited;
end;
procedure TMarkdownStream.InitializeBrowser(const Browser: TWebBrowser);
begin
FBrowser := Browser;
FIsReady := False;
FCommandQueue.Clear;
if Assigned(FBrowser) then
begin
FBrowser.OnDidFinishLoad := OnDidFinishLoad;
try
TFile.WriteAllText(FTempFileName, GetBaseHtml, TEncoding.UTF8);
FBrowser.Navigate('file://' + FTempFileName);
except
on E: Exception do
;
end;
end;
end;
procedure TMarkdownStream.OnDidFinishLoad(Sender: TObject);
begin
TThread.ForceQueue(
nil,
procedure
begin
FIsReady := True;
FlushQueue;
end
);
end;
procedure TMarkdownStream.FlushQueue;
var
cmd: TCmd;
begin
if not Assigned(FBrowser) then
exit;
for cmd in FCommandQueue do
ExecuteJS(cmd.Func, cmd.Arg);
FCommandQueue.Clear;
end;
function TMarkdownStream.BlockTypeToString(const AType: TBlockType): string;
begin
case AType of
btMarkdown: Result := 'markdown';
btRawText: Result := 'text';
btHtml: Result := 'html';
end;
end;
procedure TMarkdownStream.BeginBlock(const AType: TBlockType; const ATitle: string);
var
payload: string;
begin
payload := BlockTypeToString(AType) + '|' + ATitle;
if FIsReady then
ExecuteJS('startBlock', payload)
else
FCommandQueue.Add(TCmd.Create('startBlock', payload));
end;
procedure TMarkdownStream.Append(const Content: string);
begin
if Content.IsEmpty then
exit;
if FIsReady then
ExecuteJS('appendData', Content)
else
FCommandQueue.Add(TCmd.Create('appendData', Content));
end;
procedure TMarkdownStream.Clear;
begin
FCommandQueue.Clear;
if FIsReady then
ExecuteJS('clearAll', '')
else
FCommandQueue.Add(TCmd.Create('clearAll', ''));
end;
procedure TMarkdownStream.ExecuteJS(const FunctionName, Data: string);
var
jsCommand: string;
jsonVal: TJSONValue;
begin
if not Assigned(FBrowser) then
exit;
// Sicherer Umgang mit leeren Daten und korrektes Escaping
jsonVal := TJSONString.Create(Data);
try
jsCommand := Format('window.%s(%s);', [FunctionName, jsonVal.ToJSON]);
try
FBrowser.EvaluateJavaScript(jsCommand);
except
// Browser-Context evtl. während der Zerstörung nicht mehr valide
end;
finally
jsonVal.Free;
end;
end;
function TMarkdownStream.GetBaseHtml: string;
begin
Result :=
'''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<style>
body {
font-family: "Segoe UI Variable Text", "Segoe UI", sans-serif;
font-size: 14px; line-height: 1.5; padding: 16px; margin: 0; background: #fff;
-webkit-font-smoothing: antialiased;
}
#history { display: flex; flex-direction: column; gap: 12px; }
#active { margin-top: 12px; }
.block-header {
font-family: "Segoe UI Variable Display", sans-serif;
font-weight: 600; font-size: 11px; text-transform: uppercase;
color: #0078d4; border-bottom: 1px solid #f0f0f0; margin-bottom: 6px;
}
.raw-wrapper {
background-color: #f3f2f1;
border: 1px solid #edebe9;
border-radius: 4px;
margin: 4px 0;
display: flex;
flex-direction: column;
}
.raw-header {
padding: 6px 10px;
font-size: 11px;
font-weight: bold;
color: #323130;
background: #faf9f8;
display: flex;
align-items: center;
border-bottom: 1px solid transparent;
cursor: pointer;
user-select: none;
}
.raw-header:hover { background: #f0f0f0; color: #0078d4; }
.icon-svg {
width: 14px; height: 14px; margin-right: 8px;
fill: none; stroke: currentColor; stroke-width: 2;
stroke-linecap: round; stroke-linejoin: round;
transition: transform 0.2s;
}
.raw-content {
font-family: "Cascadia Code", "Consolas", monospace;
font-size: 12.5px; white-space: pre-wrap;
padding: 10px; margin: 0; overflow: hidden; color: #201f1e;
}
/* Collapsed Logic */
.collapsed .raw-content {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.collapsed .icon-svg { transform: rotate(-90deg); }
.expanded .icon-svg { transform: rotate(0deg); }
.expanded .raw-header { border-bottom-color: #edebe9; }
blockquote { border-left: 4px solid #0078d4; padding-left: 12px; color: #605e5c; }
pre { background: #f6f8fa; padding: 12px; border: 1px solid #d0d7de; border-radius: 6px; }
</style>
</head>
<body>
<div id="history"></div>
<div id="active"></div>
<script>
const historyDiv = document.getElementById("history");
const activeDiv = document.getElementById("active");
let currentMode = "markdown";
let currentTitle = "";
let currentBuffer = "";
// Inline SVG für das Icon
const chevronSvg = '<svg class="icon-svg" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"></polyline></svg>';
function toggleBlock(header) {
const wrapper = header.closest('.raw-wrapper');
if (wrapper.classList.contains('collapsed')) {
wrapper.classList.replace('collapsed', 'expanded');
} else {
wrapper.classList.replace('expanded', 'collapsed');
}
}
function renderTo(target, content, mode, title, isFinal) {
target.innerHTML = "";
if (mode === "markdown") {
if (title) {
const h = document.createElement("div");
h.className = "block-header";
h.innerText = title;
target.appendChild(h);
}
const c = document.createElement("div");
c.innerHTML = (typeof marked !== "undefined") ? marked.parse(content) : content;
target.appendChild(c);
} else if (mode === "text") {
const wrapper = document.createElement("div");
wrapper.className = "raw-wrapper " + (isFinal ? "collapsed" : "expanded");
const header = document.createElement("div");
header.className = "raw-header";
header.onclick = function() { toggleBlock(this); };
header.innerHTML = chevronSvg + '<span>' + (title || "Raw Output") + '</span>';
const code = document.createElement("pre");
code.className = "raw-content";
code.innerText = content;
wrapper.appendChild(header);
wrapper.appendChild(code);
target.appendChild(wrapper);
} else {
target.innerHTML = content;
}
}
window.startBlock = function(payload) {
const parts = payload.split('|');
if (currentBuffer.length > 0) {
const oldBlock = document.createElement("div");
renderTo(oldBlock, currentBuffer, currentMode, currentTitle, true);
historyDiv.appendChild(oldBlock);
}
currentMode = parts[0];
currentTitle = parts[1] || "";
currentBuffer = "";
activeDiv.innerHTML = "";
};
window.appendData = function(chunk) {
currentBuffer += chunk;
renderTo(activeDiv, currentBuffer, currentMode, currentTitle, false);
window.scrollTo(0, document.body.scrollHeight);
};
window.clearAll = function() {
currentBuffer = ""; activeDiv.innerHTML = ""; historyDiv.innerHTML = "";
};
</script>
</body>
</html>
''';
end;
end.