Gemini Test
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -275,4 +275,15 @@ object Form1: TForm1
|
|||||||
Viewport.Width = 470.000000000000000000
|
Viewport.Width = 470.000000000000000000
|
||||||
Viewport.Height = 605.000000000000000000
|
Viewport.Height = 605.000000000000000000
|
||||||
end
|
end
|
||||||
|
object SaveScriptButton: TButton
|
||||||
|
Anchors = [akTop, akRight]
|
||||||
|
Position.X = 1330.000000000000000000
|
||||||
|
Position.Y = 8.000000000000000000
|
||||||
|
Size.Width = 56.000000000000000000
|
||||||
|
Size.Height = 22.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
TabOrder = 0
|
||||||
|
Text = 'Save'
|
||||||
|
OnClick = SaveScriptButtonClick
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ type
|
|||||||
RTLListView: TListView;
|
RTLListView: TListView;
|
||||||
CompilerStageBox: TComboBox;
|
CompilerStageBox: TComboBox;
|
||||||
SaveTestBtn: TButton;
|
SaveTestBtn: TButton;
|
||||||
|
SaveScriptButton: TButton;
|
||||||
procedure InnerLambdaButtonClick(Sender: TObject);
|
procedure InnerLambdaButtonClick(Sender: TObject);
|
||||||
procedure ClearButtonClick(Sender: TObject);
|
procedure ClearButtonClick(Sender: TObject);
|
||||||
procedure CompilerStageBoxChange(Sender: TObject);
|
procedure CompilerStageBoxChange(Sender: TObject);
|
||||||
@@ -116,6 +117,7 @@ type
|
|||||||
procedure SaveUserLibButtonClick(Sender: TObject);
|
procedure SaveUserLibButtonClick(Sender: TObject);
|
||||||
procedure LoadUserLibButtonClick(Sender: TObject);
|
procedure LoadUserLibButtonClick(Sender: TObject);
|
||||||
procedure RTLListViewChange(Sender: TObject);
|
procedure RTLListViewChange(Sender: TObject);
|
||||||
|
procedure SaveScriptButtonClick(Sender: TObject);
|
||||||
private
|
private
|
||||||
FCurrUnboundAst: IAstNode;
|
FCurrUnboundAst: IAstNode;
|
||||||
FCurrCompiled: ILambdaExpressionNode;
|
FCurrCompiled: ILambdaExpressionNode;
|
||||||
@@ -138,6 +140,7 @@ type
|
|||||||
|
|
||||||
const
|
const
|
||||||
UserLibName = 'T:\Myc\ASTPlayground\UserLib.json';
|
UserLibName = 'T:\Myc\ASTPlayground\UserLib.json';
|
||||||
|
SrcName = 'T:\Myc\ASTPlayground\Script.txt';
|
||||||
|
|
||||||
var
|
var
|
||||||
Form1: TForm1;
|
Form1: TForm1;
|
||||||
@@ -288,10 +291,13 @@ begin
|
|||||||
CompilerStageBox.BringToFront;
|
CompilerStageBox.BringToFront;
|
||||||
ClearButtonClick(Self);
|
ClearButtonClick(Self);
|
||||||
|
|
||||||
// Memo1.Lines.Add(TAstSchema.GenerateFullSystemPrompt(FEnvironment.RootScope));
|
if FileExists(SrcName) then
|
||||||
|
ScriptMemo.Lines.LoadFromFile(SrcName);
|
||||||
|
|
||||||
|
// Memo1.Lines.Add(TAstSchema.GenerateFullSystemPrompt(FEnvironment.RootScope));
|
||||||
|
|
||||||
// "Dieses JSON-Objekt betest du in das Feld response_schema (Gemini) oder json_schema (OpenAI) ein."
|
// "Dieses JSON-Objekt betest du in das Feld response_schema (Gemini) oder json_schema (OpenAI) ein."
|
||||||
Memo1.Lines.Add(TAstSchema.GenerateFullSchema.ToJSON);
|
// Memo1.Lines.Add(TAstSchema.GenerateFullSchema.ToJSON);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TForm1.ShowVizualization;
|
procedure TForm1.ShowVizualization;
|
||||||
@@ -1284,6 +1290,11 @@ begin
|
|||||||
TAstDumper.Dump(FCurrCompiled, Memo1.Lines);
|
TAstDumper.Dump(FCurrCompiled, Memo1.Lines);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TForm1.SaveScriptButtonClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
ScriptMemo.Lines.SaveToFile(SrcName);
|
||||||
|
end;
|
||||||
|
|
||||||
initialization
|
initialization
|
||||||
TAst.RegisterLibrary(RegisterBroker);
|
TAst.RegisterLibrary(RegisterBroker);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
(do
|
||||||
|
|
||||||
|
)
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
program GeminiTest;
|
program GeminiTest;
|
||||||
|
|
||||||
|
{$R *.dres}
|
||||||
|
|
||||||
uses
|
uses
|
||||||
|
FastMM5,
|
||||||
System.StartUpCopy,
|
System.StartUpCopy,
|
||||||
FMX.Forms,
|
FMX.Forms,
|
||||||
MainUnit in 'MainUnit.pas' {Form1},
|
MainUnit in 'MainUnit.pas' {Form1},
|
||||||
|
|||||||
@@ -128,11 +128,15 @@
|
|||||||
</DelphiCompile>
|
</DelphiCompile>
|
||||||
<DCCReference Include="MainUnit.pas">
|
<DCCReference Include="MainUnit.pas">
|
||||||
<Form>Form1</Form>
|
<Form>Form1</Form>
|
||||||
<FormType>fmx</FormType>
|
|
||||||
</DCCReference>
|
</DCCReference>
|
||||||
<DCCReference Include="Myc.Api.Gemini.pas"/>
|
<DCCReference Include="Myc.Api.Gemini.pas"/>
|
||||||
<DCCReference Include="Myc.System.Quota.pas"/>
|
<DCCReference Include="Myc.System.Quota.pas"/>
|
||||||
<DCCReference Include="Myc.Api.MarkdownStream.pas"/>
|
<DCCReference Include="Myc.Api.MarkdownStream.pas"/>
|
||||||
|
<RcItem Include="Myc.Api.MarkdownStream.Base.html">
|
||||||
|
<ContainerId>ResourceItem</ContainerId>
|
||||||
|
<ResourceType>RCDATA</ResourceType>
|
||||||
|
<ResourceId>BASE_HTML</ResourceId>
|
||||||
|
</RcItem>
|
||||||
<BuildConfiguration Include="Base">
|
<BuildConfiguration Include="Base">
|
||||||
<Key>Base</Key>
|
<Key>Base</Key>
|
||||||
</BuildConfiguration>
|
</BuildConfiguration>
|
||||||
@@ -170,9 +174,9 @@
|
|||||||
<Overwrite>true</Overwrite>
|
<Overwrite>true</Overwrite>
|
||||||
</Platform>
|
</Platform>
|
||||||
</DeployFile>
|
</DeployFile>
|
||||||
<DeployFile LocalName="Win32\Debug\GeminiTest.exe" Configuration="Debug" Class="ProjectOutput">
|
<DeployFile LocalName="Myc.Api.MarkdownStream.Base.html" Configuration="Debug" Class="ProjectFile">
|
||||||
<Platform Name="Win32">
|
<Platform Name="Win64">
|
||||||
<RemoteName>GeminiTest.exe</RemoteName>
|
<RemoteDir>.\</RemoteDir>
|
||||||
<Overwrite>true</Overwrite>
|
<Overwrite>true</Overwrite>
|
||||||
</Platform>
|
</Platform>
|
||||||
</DeployFile>
|
</DeployFile>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ object Form1: TForm1
|
|||||||
FormFactor.Height = 480
|
FormFactor.Height = 480
|
||||||
FormFactor.Devices = [Desktop]
|
FormFactor.Devices = [Desktop]
|
||||||
OnCreate = FormCreate
|
OnCreate = FormCreate
|
||||||
|
OnDestroy = FormDestroy
|
||||||
DesignerMasterStyle = 0
|
DesignerMasterStyle = 0
|
||||||
object AskButton: TButton
|
object AskButton: TButton
|
||||||
Anchors = [akRight, akBottom]
|
Anchors = [akRight, akBottom]
|
||||||
@@ -187,4 +188,9 @@ object Form1: TForm1
|
|||||||
Text = 'in/out'
|
Text = 'in/out'
|
||||||
TabOrder = 4
|
TabOrder = 4
|
||||||
end
|
end
|
||||||
|
object ApplicationEvents: TApplicationEvents
|
||||||
|
OnIdle = ApplicationEventsIdle
|
||||||
|
Left = 768
|
||||||
|
Top = 58
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+188
-142
@@ -8,6 +8,8 @@ uses
|
|||||||
System.UITypes,
|
System.UITypes,
|
||||||
System.Classes,
|
System.Classes,
|
||||||
System.Variants,
|
System.Variants,
|
||||||
|
System.Generics.Collections,
|
||||||
|
System.SyncObjs,
|
||||||
FMX.Types,
|
FMX.Types,
|
||||||
FMX.Controls,
|
FMX.Controls,
|
||||||
FMX.Forms,
|
FMX.Forms,
|
||||||
@@ -22,8 +24,11 @@ uses
|
|||||||
FMX.ListBox,
|
FMX.ListBox,
|
||||||
FMX.TabControl,
|
FMX.TabControl,
|
||||||
FMX.WebBrowser,
|
FMX.WebBrowser,
|
||||||
|
Myc.Signals,
|
||||||
|
Myc.Futures,
|
||||||
Myc.Api.Gemini,
|
Myc.Api.Gemini,
|
||||||
Myc.Api.MarkdownStream;
|
Myc.Api.MarkdownStream,
|
||||||
|
FMX.ApplicationEvents;
|
||||||
|
|
||||||
type
|
type
|
||||||
TForm1 = class(TForm)
|
TForm1 = class(TForm)
|
||||||
@@ -43,6 +48,9 @@ type
|
|||||||
GlobalQuoteLabel: TLabel;
|
GlobalQuoteLabel: TLabel;
|
||||||
ChatTabItem: TTabItem;
|
ChatTabItem: TTabItem;
|
||||||
ChatBrowser: TWebBrowser;
|
ChatBrowser: TWebBrowser;
|
||||||
|
ApplicationEvents: TApplicationEvents;
|
||||||
|
procedure FormDestroy(Sender: TObject);
|
||||||
|
procedure ApplicationEventsIdle(Sender: TObject; var Done: Boolean);
|
||||||
procedure FormCreate(Sender: TObject);
|
procedure FormCreate(Sender: TObject);
|
||||||
procedure AskButtonClick(Sender: TObject);
|
procedure AskButtonClick(Sender: TObject);
|
||||||
procedure Edit1Exit(Sender: TObject);
|
procedure Edit1Exit(Sender: TObject);
|
||||||
@@ -52,6 +60,15 @@ type
|
|||||||
FChat: IGeminiChat;
|
FChat: IGeminiChat;
|
||||||
FCachedClient: IGeminiClient;
|
FCachedClient: IGeminiClient;
|
||||||
FMarkdown: IMarkdownStream;
|
FMarkdown: IMarkdownStream;
|
||||||
|
FAsking: TFuture<TGeminiResult>;
|
||||||
|
FResponseLock: TCriticalSection;
|
||||||
|
FResponseQueue: TQueue<TProc>;
|
||||||
|
FModelList: TFuture<TArray<string>>;
|
||||||
|
FUploadCache: TFuture<TProc>;
|
||||||
|
FNeedModelListUpdate: TFlag;
|
||||||
|
FResposeQueueChanged: TFlag;
|
||||||
|
FNewResposeReceived: TFlag;
|
||||||
|
FCacheUploaded: TFlag;
|
||||||
procedure InitChat;
|
procedure InitChat;
|
||||||
procedure UpdateQuotaDisplay;
|
procedure UpdateQuotaDisplay;
|
||||||
public
|
public
|
||||||
@@ -64,7 +81,6 @@ var
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
System.Threading,
|
|
||||||
Myc.System.Quota;
|
Myc.System.Quota;
|
||||||
|
|
||||||
{$R *.fmx}
|
{$R *.fmx}
|
||||||
@@ -72,8 +88,17 @@ uses
|
|||||||
const
|
const
|
||||||
FallbackModel = 'gemini-2.5-flash-lite';
|
FallbackModel = 'gemini-2.5-flash-lite';
|
||||||
|
|
||||||
|
procedure TForm1.FormDestroy(Sender: TObject);
|
||||||
|
begin
|
||||||
|
FResponseQueue.Free;
|
||||||
|
FResponseLock.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TForm1.FormCreate(Sender: TObject);
|
procedure TForm1.FormCreate(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
|
FResponseQueue := TQueue<TProc>.Create;
|
||||||
|
FResponseLock := TCriticalSection.Create;
|
||||||
|
|
||||||
// 1. Markdown Stream initialisieren und mit Browser verbinden
|
// 1. Markdown Stream initialisieren und mit Browser verbinden
|
||||||
FMarkdown := TMarkdownStream.Create;
|
FMarkdown := TMarkdownStream.Create;
|
||||||
FMarkdown.InitializeBrowser(ChatBrowser);
|
FMarkdown.InitializeBrowser(ChatBrowser);
|
||||||
@@ -152,79 +177,68 @@ begin
|
|||||||
// Da User Markdown oft erwarten:
|
// Da User Markdown oft erwarten:
|
||||||
FMarkdown.Append(prompt);
|
FMarkdown.Append(prompt);
|
||||||
|
|
||||||
TTask.Run(
|
FAsking :=
|
||||||
procedure
|
TFuture<TGeminiResult>.Construct(
|
||||||
var
|
FAsking.Done,
|
||||||
CurrentChat: IGeminiChat;
|
function: TGeminiResult
|
||||||
resultData: TGeminiResult;
|
var
|
||||||
// State Tracking für den Stream
|
CurrentChat: IGeminiChat;
|
||||||
LastWasThought: Boolean;
|
// State Tracking für den Stream
|
||||||
FirstChunk: Boolean;
|
LastWasThought: Boolean;
|
||||||
begin
|
FirstChunk: Boolean;
|
||||||
CurrentChat := FChat;
|
begin
|
||||||
if CurrentChat = nil then
|
CurrentChat := FChat;
|
||||||
exit;
|
if CurrentChat = nil then
|
||||||
|
exit;
|
||||||
|
|
||||||
LastWasThought := False;
|
LastWasThought := False;
|
||||||
FirstChunk := True;
|
FirstChunk := True;
|
||||||
|
|
||||||
// --- STREAMING ---
|
// --- STREAMING ---
|
||||||
resultData :=
|
Result :=
|
||||||
CurrentChat.SendMessageStream(
|
CurrentChat.SendMessageStream(
|
||||||
prompt,
|
prompt,
|
||||||
procedure(const TextChunk: string; IsThought: Boolean)
|
procedure(const TextChunk: string; IsThought: Boolean)
|
||||||
begin
|
begin
|
||||||
TThread.Queue(
|
FResponseLock.Enter;
|
||||||
nil,
|
try
|
||||||
procedure
|
FResponseQueue.Enqueue(
|
||||||
begin
|
procedure
|
||||||
// Statuswechsel oder allererster Chunk -> Neuer Block
|
|
||||||
if FirstChunk or (IsThought <> LastWasThought) then
|
|
||||||
begin
|
|
||||||
if IsThought then
|
|
||||||
begin
|
begin
|
||||||
FMarkdown.BeginBlock(btMarkdown, 'Thinking...');
|
// Statuswechsel oder allererster Chunk -> Neuer Block
|
||||||
// Optional: Kursiver Block für Gedanken
|
if FirstChunk or (IsThought <> LastWasThought) then
|
||||||
// Wir hängen Gedanken oft als Zitat oder kursiv an
|
begin
|
||||||
|
if IsThought then
|
||||||
|
begin
|
||||||
|
FMarkdown.BeginBlock(btMarkdown, 'Thinking...');
|
||||||
|
// Optional: Kursiver Block für Gedanken
|
||||||
|
// Wir hängen Gedanken oft als Zitat oder kursiv an
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
// Wenn wir aus einem Gedanken kommen oder starten
|
||||||
|
FMarkdown.BeginBlock(btMarkdown, 'Gemini');
|
||||||
|
end;
|
||||||
|
|
||||||
|
LastWasThought := IsThought;
|
||||||
|
FirstChunk := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
FMarkdown.Append(TextChunk);
|
||||||
end
|
end
|
||||||
else
|
);
|
||||||
begin
|
finally
|
||||||
// Wenn wir aus einem Gedanken kommen oder starten
|
FResponseLock.Leave;
|
||||||
FMarkdown.BeginBlock(btMarkdown, 'Gemini');
|
end;
|
||||||
end;
|
|
||||||
|
|
||||||
LastWasThought := IsThought;
|
FResposeQueueChanged.Notify;
|
||||||
FirstChunk := False;
|
end
|
||||||
end;
|
);
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
// Inhalt anhängen
|
FNewResposeReceived := TFlag.CreateObserver(FAsking.Done.Signal);
|
||||||
if IsThought then
|
|
||||||
FMarkdown.Append(TextChunk) // Wird im selben Markdown-Block gerendert
|
|
||||||
else
|
|
||||||
FMarkdown.Append(TextChunk);
|
|
||||||
end
|
|
||||||
);
|
|
||||||
end
|
|
||||||
);
|
|
||||||
|
|
||||||
// Abschluss im Main Thread
|
|
||||||
TThread.Queue(
|
|
||||||
nil,
|
|
||||||
procedure
|
|
||||||
begin
|
|
||||||
if resultData.Text.StartsWith('Error') then
|
|
||||||
begin
|
|
||||||
FMarkdown.BeginBlock(btMarkdown, 'Error');
|
|
||||||
FMarkdown.Append('> ' + resultData.Text);
|
|
||||||
end;
|
|
||||||
|
|
||||||
QuotaLabel.Text := Format('Input: %d | Output: %d', [resultData.Usage.PromptTokens, resultData.Usage.CandidatesTokens]);
|
|
||||||
UpdateQuotaDisplay;
|
|
||||||
AskButton.Enabled := True;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
end
|
|
||||||
);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TForm1.Edit1Exit(Sender: TObject);
|
procedure TForm1.Edit1Exit(Sender: TObject);
|
||||||
@@ -241,46 +255,19 @@ begin
|
|||||||
if apiKey.IsEmpty then
|
if apiKey.IsEmpty then
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
TTask.Run(
|
FModelList :=
|
||||||
procedure
|
TFuture<TArray<string>>.Construct(
|
||||||
var
|
FModelList.Done,
|
||||||
client: IGeminiClient;
|
function: TArray<string>
|
||||||
modelList: TArray<string>;
|
var
|
||||||
begin
|
client: IGeminiClient;
|
||||||
client := TGeminiClient.Create(apiKey);
|
begin
|
||||||
modelList := client.ListModels;
|
client := TGeminiClient.Create(apiKey);
|
||||||
|
Result := client.ListModels;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
TThread.Queue(
|
FNeedModelListUpdate := TFlag.CreateObserver(FModelList.Done.Signal);
|
||||||
nil,
|
|
||||||
procedure
|
|
||||||
var
|
|
||||||
mName: string;
|
|
||||||
begin
|
|
||||||
if Length(modelList) = 0 then
|
|
||||||
begin
|
|
||||||
FMarkdown.Append(sLineBreak + '*System: No models found.*' + sLineBreak);
|
|
||||||
exit;
|
|
||||||
end;
|
|
||||||
|
|
||||||
ModelsComboBox.BeginUpdate;
|
|
||||||
try
|
|
||||||
ModelsComboBox.Items.Clear;
|
|
||||||
for mName in modelList do
|
|
||||||
ModelsComboBox.Items.Add(mName);
|
|
||||||
|
|
||||||
if ModelsComboBox.Items.Count > 0 then
|
|
||||||
begin
|
|
||||||
ModelsComboBox.ItemIndex := ModelsComboBox.Items.IndexOf(FallbackModel);
|
|
||||||
if ModelsComboBox.ItemIndex < 0 then
|
|
||||||
ModelsComboBox.ItemIndex := 0;
|
|
||||||
end;
|
|
||||||
finally
|
|
||||||
ModelsComboBox.EndUpdate;
|
|
||||||
end;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
end
|
|
||||||
);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TForm1.UploadCacheButtonClick(Sender: TObject);
|
procedure TForm1.UploadCacheButtonClick(Sender: TObject);
|
||||||
@@ -309,46 +296,105 @@ begin
|
|||||||
// Hinweis im Browser anzeigen
|
// Hinweis im Browser anzeigen
|
||||||
FMarkdown.Append(sLineBreak + '*Creating Cache...*' + sLineBreak);
|
FMarkdown.Append(sLineBreak + '*Creating Cache...*' + sLineBreak);
|
||||||
|
|
||||||
TTask.Run(
|
FUploadCache :=
|
||||||
procedure
|
TFuture<TProc>.Construct(
|
||||||
var
|
FUploadCache.Done,
|
||||||
client: IGeminiClient;
|
function: TProc
|
||||||
cacheName: string;
|
var
|
||||||
errMsg: string;
|
client: IGeminiClient;
|
||||||
begin
|
cacheName: string;
|
||||||
try
|
errMsg: string;
|
||||||
client := TGeminiClient.Create(apiKey, modelName);
|
begin
|
||||||
cacheName := client.CreateCache(content, sysInstr, 300);
|
try
|
||||||
client.ActiveCacheName := cacheName;
|
client := TGeminiClient.Create(apiKey, modelName);
|
||||||
FCachedClient := client;
|
cacheName := client.CreateCache(content, sysInstr, 300);
|
||||||
|
client.ActiveCacheName := cacheName;
|
||||||
|
FCachedClient := client;
|
||||||
|
|
||||||
TThread.Queue(
|
Result :=
|
||||||
nil,
|
|
||||||
procedure
|
|
||||||
begin
|
|
||||||
FMarkdown.Append('**Success: Cache created!**' + sLineBreak);
|
|
||||||
FMarkdown.Append('ID: `' + cacheName + '`' + sLineBreak);
|
|
||||||
|
|
||||||
InitChat;
|
|
||||||
UploadCacheButton.Enabled := True;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
except
|
|
||||||
on E: Exception do
|
|
||||||
begin
|
|
||||||
errMsg := E.Message;
|
|
||||||
TThread.Queue(
|
|
||||||
nil,
|
|
||||||
procedure
|
procedure
|
||||||
begin
|
begin
|
||||||
FMarkdown.Append('**Cache Upload Failed:** ' + errMsg + sLineBreak);
|
FMarkdown.Append('**Success: Cache created!**' + sLineBreak);
|
||||||
UploadCacheButton.Enabled := True;
|
FMarkdown.Append('ID: `' + cacheName + '`' + sLineBreak);
|
||||||
end
|
|
||||||
);
|
InitChat;
|
||||||
|
end;
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
errMsg := E.Message;
|
||||||
|
Result := procedure begin FMarkdown.Append('**Cache Upload Failed:** ' + errMsg + sLineBreak); end;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
FCacheUploaded := TFlag.CreateObserver(FUploadCache.Done.Signal);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm1.ApplicationEventsIdle(Sender: TObject; var Done: Boolean);
|
||||||
|
begin
|
||||||
|
if FNeedModelListUpdate.Reset then
|
||||||
|
begin
|
||||||
|
ModelsComboBox.BeginUpdate;
|
||||||
|
try
|
||||||
|
ModelsComboBox.Items.Clear;
|
||||||
|
|
||||||
|
var models := FModelList.Value;
|
||||||
|
|
||||||
|
if Length(models) = 0 then
|
||||||
|
begin
|
||||||
|
FMarkdown.Append(sLineBreak + '*System: No models found.*' + sLineBreak);
|
||||||
|
exit;
|
||||||
end;
|
end;
|
||||||
end
|
|
||||||
);
|
for var mName in models do
|
||||||
|
ModelsComboBox.Items.Add(mName);
|
||||||
|
|
||||||
|
if ModelsComboBox.Items.Count > 0 then
|
||||||
|
begin
|
||||||
|
ModelsComboBox.ItemIndex := ModelsComboBox.Items.IndexOf(FallbackModel);
|
||||||
|
if ModelsComboBox.ItemIndex < 0 then
|
||||||
|
ModelsComboBox.ItemIndex := 0;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
ModelsComboBox.EndUpdate;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if FCacheUploaded.Reset then
|
||||||
|
begin
|
||||||
|
if Assigned(FUploadCache.Value) then
|
||||||
|
FUploadCache.Value();
|
||||||
|
UploadCacheButton.Enabled := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if FResposeQueueChanged.Reset then
|
||||||
|
begin
|
||||||
|
FResponseLock.Enter;
|
||||||
|
try
|
||||||
|
while FResponseQueue.Count > 0 do
|
||||||
|
(FResponseQueue.Dequeue)();
|
||||||
|
finally
|
||||||
|
FResponseLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if FNewResposeReceived.Reset then
|
||||||
|
begin
|
||||||
|
var res := FAsking.Value;
|
||||||
|
|
||||||
|
if res.Text.StartsWith('Error') then
|
||||||
|
begin
|
||||||
|
FMarkdown.BeginBlock(btMarkdown, 'Error');
|
||||||
|
FMarkdown.Append('> ' + Res.Text);
|
||||||
|
end;
|
||||||
|
|
||||||
|
QuotaLabel.Text := Format('Input: %d | Output: %d', [res.Usage.PromptTokens, res.Usage.CandidatesTokens]);
|
||||||
|
|
||||||
|
UpdateQuotaDisplay;
|
||||||
|
AskButton.Enabled := True;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<!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>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/delphi.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 Text Blocks */
|
||||||
|
.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 .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; }
|
||||||
|
|
||||||
|
/* Markdown Specifics */
|
||||||
|
blockquote { border-left: 4px solid #0078d4; padding-left: 12px; color: #605e5c; }
|
||||||
|
|
||||||
|
/* Code Blocks & Copy Button */
|
||||||
|
.code-wrapper { position: relative; margin: 1em 0; }
|
||||||
|
pre { background: #f6f8fa; padding: 12px; border: 1px solid #d0d7de; border-radius: 6px; overflow-x: auto; margin: 0; }
|
||||||
|
code { font-family: "Cascadia Code", Consolas, monospace; font-size: 90%; }
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
position: sticky; top: 10px; float: right; z-index: 10;
|
||||||
|
margin-top: 10px; margin-bottom: -42px; margin-right: 10px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8); backdrop-filter: blur(2px);
|
||||||
|
border: 1px solid #d0d7de; border-radius: 6px;
|
||||||
|
cursor: pointer; color: #57606a; transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.copy-btn:hover { background-color: #ffffff; color: #0969da; border-color: #0969da; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.copy-btn:active { background-color: #edeff2; transform: translateY(1px); }
|
||||||
|
.copy-btn svg { fill: currentColor; }
|
||||||
|
</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 = "";
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
const chevronSvg = '<svg class="icon-svg" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
||||||
|
const iconCopy = '<svg viewBox="0 0 16 16" width="16" height="16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>';
|
||||||
|
const iconCheck = '<svg viewBox="0 0 16 16" width="16" height="16" style="color: #2da44e"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></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 addCopyButtons(container) {
|
||||||
|
const pres = container.querySelectorAll("pre");
|
||||||
|
pres.forEach(function(preBlock) {
|
||||||
|
if (preBlock.parentNode.classList.contains("code-wrapper")) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "code-wrapper";
|
||||||
|
preBlock.parentNode.insertBefore(wrapper, preBlock);
|
||||||
|
wrapper.appendChild(preBlock);
|
||||||
|
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.className = "copy-btn";
|
||||||
|
button.innerHTML = iconCopy;
|
||||||
|
button.title = "Copy to Clipboard";
|
||||||
|
|
||||||
|
button.addEventListener("click", function() {
|
||||||
|
const code = preBlock.querySelector("code");
|
||||||
|
if (!code) return;
|
||||||
|
const text = code.innerText;
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
button.innerHTML = iconCheck;
|
||||||
|
setTimeout(function() { button.innerHTML = iconCopy; }, 2000);
|
||||||
|
}, function(err) { console.error("Clipboard failed", err); });
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.insertBefore(button, preBlock);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Highlight and add Copy Buttons
|
||||||
|
if (typeof hljs !== "undefined") {
|
||||||
|
c.querySelectorAll("pre code").forEach((block) => { hljs.highlightElement(block); });
|
||||||
|
}
|
||||||
|
addCopyButtons(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>
|
||||||
@@ -9,6 +9,7 @@ uses
|
|||||||
System.Threading,
|
System.Threading,
|
||||||
System.Generics.Collections,
|
System.Generics.Collections,
|
||||||
System.JSON,
|
System.JSON,
|
||||||
|
System.Types,
|
||||||
FMX.WebBrowser;
|
FMX.WebBrowser;
|
||||||
|
|
||||||
type
|
type
|
||||||
@@ -189,206 +190,23 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TMarkdownStream.GetBaseHtml: string;
|
function TMarkdownStream.GetBaseHtml: string;
|
||||||
|
var
|
||||||
|
rs: TResourceStream;
|
||||||
|
ss: TStringStream;
|
||||||
begin
|
begin
|
||||||
Result :=
|
// Load HTML template from embedded resource (which is Myc.Api.MarkdownStream.Base.html)
|
||||||
'''
|
rs := TResourceStream.Create(HInstance, 'BASE_HTML', RT_RCDATA);
|
||||||
<!DOCTYPE html>
|
try
|
||||||
<html>
|
ss := TStringStream.Create('', TEncoding.UTF8);
|
||||||
<head>
|
try
|
||||||
<meta charset="utf-8">
|
ss.CopyFrom(rs, 0);
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
Result := ss.DataString;
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
finally
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
ss.Free;
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/delphi.min.js"></script>
|
end;
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
finally
|
||||||
|
rs.Free;
|
||||||
<style>
|
end;
|
||||||
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 Text Blocks */
|
|
||||||
.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 .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; }
|
|
||||||
|
|
||||||
/* Markdown Specifics */
|
|
||||||
blockquote { border-left: 4px solid #0078d4; padding-left: 12px; color: #605e5c; }
|
|
||||||
|
|
||||||
/* Code Blocks & Copy Button */
|
|
||||||
.code-wrapper { position: relative; margin: 1em 0; }
|
|
||||||
pre { background: #f6f8fa; padding: 12px; border: 1px solid #d0d7de; border-radius: 6px; overflow-x: auto; margin: 0; }
|
|
||||||
code { font-family: "Cascadia Code", Consolas, monospace; font-size: 90%; }
|
|
||||||
|
|
||||||
.copy-btn {
|
|
||||||
position: sticky; top: 10px; float: right; z-index: 10;
|
|
||||||
margin-top: 10px; margin-bottom: -42px; margin-right: 10px;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
width: 32px; height: 32px;
|
|
||||||
background-color: rgba(255, 255, 255, 0.8); backdrop-filter: blur(2px);
|
|
||||||
border: 1px solid #d0d7de; border-radius: 6px;
|
|
||||||
cursor: pointer; color: #57606a; transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.copy-btn:hover { background-color: #ffffff; color: #0969da; border-color: #0969da; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
||||||
.copy-btn:active { background-color: #edeff2; transform: translateY(1px); }
|
|
||||||
.copy-btn svg { fill: currentColor; }
|
|
||||||
</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 = "";
|
|
||||||
|
|
||||||
// Icons
|
|
||||||
const chevronSvg = '<svg class="icon-svg" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
||||||
const iconCopy = '<svg viewBox="0 0 16 16" width="16" height="16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>';
|
|
||||||
const iconCheck = '<svg viewBox="0 0 16 16" width="16" height="16" style="color: #2da44e"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></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 addCopyButtons(container) {
|
|
||||||
const pres = container.querySelectorAll("pre");
|
|
||||||
pres.forEach(function(preBlock) {
|
|
||||||
if (preBlock.parentNode.classList.contains("code-wrapper")) return;
|
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
|
||||||
wrapper.className = "code-wrapper";
|
|
||||||
preBlock.parentNode.insertBefore(wrapper, preBlock);
|
|
||||||
wrapper.appendChild(preBlock);
|
|
||||||
|
|
||||||
const button = document.createElement("button");
|
|
||||||
button.className = "copy-btn";
|
|
||||||
button.innerHTML = iconCopy;
|
|
||||||
button.title = "Copy to Clipboard";
|
|
||||||
|
|
||||||
button.addEventListener("click", function() {
|
|
||||||
const code = preBlock.querySelector("code");
|
|
||||||
if (!code) return;
|
|
||||||
const text = code.innerText;
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
button.innerHTML = iconCheck;
|
|
||||||
setTimeout(function() { button.innerHTML = iconCopy; }, 2000);
|
|
||||||
}, function(err) { console.error("Clipboard failed", err); });
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.insertBefore(button, preBlock);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Highlight and add Copy Buttons
|
|
||||||
if (typeof hljs !== "undefined") {
|
|
||||||
c.querySelectorAll("pre code").forEach((block) => { hljs.highlightElement(block); });
|
|
||||||
}
|
|
||||||
addCopyButtons(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;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
Reference in New Issue
Block a user