Gemini Test
This commit is contained in:
@@ -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.
|
||||||
Reference in New Issue
Block a user