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