Files
MycLib/GeminiTest/MainUnit.pas
T
Michael Schimmel 3c5d51f6a8 Gemini Test
2026-01-13 10:25:33 +01:00

401 lines
12 KiB
ObjectPascal

unit MainUnit;
interface
uses
System.SysUtils,
System.Types,
System.UITypes,
System.Classes,
System.Variants,
System.Generics.Collections,
System.SyncObjs,
FMX.Types,
FMX.Controls,
FMX.Forms,
FMX.Graphics,
FMX.Dialogs,
FMX.Memo.Types,
FMX.ScrollBox,
FMX.Memo,
FMX.Controls.Presentation,
FMX.StdCtrls,
FMX.Edit,
FMX.ListBox,
FMX.TabControl,
FMX.WebBrowser,
Myc.Signals,
Myc.Futures,
Myc.Api.Gemini,
Myc.Api.MarkdownStream,
FMX.ApplicationEvents;
type
TForm1 = class(TForm)
AskButton: TButton;
Edit1: TEdit;
ModelsComboBox: TComboBox;
QuotaLabel: TLabel;
AskMemo: TMemo;
ResetChatButton: TButton;
TabControl1: TTabControl;
CacheTabItem: TTabItem;
SysInstrTabItem: TTabItem;
CacheMemo: TMemo;
SystemInstructionMemo: TMemo;
UploadCacheButton: TButton;
ThinkingCheckBox: TCheckBox;
GlobalQuoteLabel: TLabel;
ChatTabItem: TTabItem;
ChatBrowser: TWebBrowser;
ApplicationEvents: TApplicationEvents;
procedure FormDestroy(Sender: TObject);
procedure ApplicationEventsIdle(Sender: TObject; var Done: Boolean);
procedure FormCreate(Sender: TObject);
procedure AskButtonClick(Sender: TObject);
procedure Edit1Exit(Sender: TObject);
procedure ResetChatButtonClick(Sender: TObject);
procedure UploadCacheButtonClick(Sender: TObject);
private
FChat: IGeminiChat;
FCachedClient: IGeminiClient;
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 UpdateQuotaDisplay;
public
procedure GetAvailableModels;
end;
var
Form1: TForm1;
implementation
uses
Myc.System.Quota;
{$R *.fmx}
const
FallbackModel = 'gemini-2.5-flash-lite';
procedure TForm1.FormDestroy(Sender: TObject);
begin
FResponseQueue.Free;
FResponseLock.Free;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FResponseQueue := TQueue<TProc>.Create;
FResponseLock := TCriticalSection.Create;
// 1. Markdown Stream initialisieren und mit Browser verbinden
FMarkdown := TMarkdownStream.Create;
FMarkdown.InitializeBrowser(ChatBrowser);
// Load default instructions if file exists
if FileExists('T:\Myc\KI\gemini.md') then
SystemInstructionMemo.Lines.LoadFromFile('T:\Myc\KI\gemini.md', TEncoding.UTF8);
UpdateQuotaDisplay;
GetAvailableModels;
InitChat;
end;
procedure TForm1.InitChat;
begin
// Browser komplett leeren
if Assigned(FMarkdown) then
FMarkdown.Clear;
FMarkdown.BeginBlock(btMarkdown);
if Assigned(FCachedClient) then
begin
FChat := FCachedClient.StartChat;
FMarkdown.Append('*--- New Chat Session ---* (Cached Content Active)');
end
else
begin
FMarkdown.Append('*--- New Chat Session ---*');
FChat := nil;
end;
end;
procedure TForm1.ResetChatButtonClick(Sender: TObject);
begin
InitChat;
end;
procedure TForm1.UpdateQuotaDisplay;
begin
GlobalQuoteLabel.Text := 'Total: ' + TGlobalQuota.GetTotal.ToString + ', Today: ' + TGlobalQuota.GetDailyTotal.ToString;
end;
procedure TForm1.AskButtonClick(Sender: TObject);
var
apiKey: string;
modelName: string;
prompt: string;
begin
apiKey := Edit1.Text;
prompt := AskMemo.Text;
if ModelsComboBox.ItemIndex > -1 then
modelName := ModelsComboBox.Items[ModelsComboBox.ItemIndex]
else
modelName := FallbackModel;
// --- LAZY INIT ---
if FChat = nil then
begin
var client: IGeminiClient := TGeminiClient.Create(apiKey, modelName);
if not SystemInstructionMemo.Text.Trim.IsEmpty then
client.SystemInstruction := SystemInstructionMemo.Text;
client.EnableThinking := ThinkingCheckBox.IsChecked;
FChat := client.StartChat;
end;
AskButton.Enabled := False;
AskMemo.Text := '';
// --- USER BLOCK ---
FMarkdown.BeginBlock(btRawText, 'Prompt');
// Hier nutzen wir RawText für den Prompt selbst, um Markdown-Injection des Users zu verhindern,
// oder wir bleiben im Markdown Block für schöneres Rendering (Code-Blöcke des Users).
// Da User Markdown oft erwarten:
FMarkdown.Append(prompt);
FAsking :=
TFuture<TGeminiResult>.Construct(
FAsking.Done,
function: TGeminiResult
var
CurrentChat: IGeminiChat;
// State Tracking für den Stream
LastWasThought: Boolean;
FirstChunk: Boolean;
begin
CurrentChat := FChat;
if CurrentChat = nil then
exit;
LastWasThought := False;
FirstChunk := True;
// --- STREAMING ---
Result :=
CurrentChat.SendMessageStream(
prompt,
procedure(const TextChunk: string; IsThought: Boolean)
begin
FResponseLock.Enter;
try
FResponseQueue.Enqueue(
procedure
begin
// Statuswechsel oder allererster Chunk -> Neuer Block
if FirstChunk or (IsThought <> LastWasThought) then
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
);
finally
FResponseLock.Leave;
end;
FResposeQueueChanged.Notify;
end
);
end
);
FNewResposeReceived := TFlag.CreateObserver(FAsking.Done.Signal);
end;
procedure TForm1.Edit1Exit(Sender: TObject);
begin
if ModelsComboBox.ItemIndex < 0 then
GetAvailableModels;
end;
procedure TForm1.GetAvailableModels;
var
apiKey: string;
begin
apiKey := Edit1.Text.Trim;
if apiKey.IsEmpty then
exit;
FModelList :=
TFuture<TArray<string>>.Construct(
FModelList.Done,
function: TArray<string>
var
client: IGeminiClient;
begin
client := TGeminiClient.Create(apiKey);
Result := client.ListModels;
end
);
FNeedModelListUpdate := TFlag.CreateObserver(FModelList.Done.Signal);
end;
procedure TForm1.UploadCacheButtonClick(Sender: TObject);
var
apiKey: string;
content: string;
sysInstr: string;
modelName: string;
begin
apiKey := Edit1.Text;
content := CacheMemo.Text;
sysInstr := SystemInstructionMemo.Text;
if content.Trim.IsEmpty then
begin
FMarkdown.Append(sLineBreak + '**Error:** Cache content is empty.' + sLineBreak);
exit;
end;
if ModelsComboBox.ItemIndex > -1 then
modelName := ModelsComboBox.Items[ModelsComboBox.ItemIndex].Split([' '])[0]
else
modelName := FallbackModel;
UploadCacheButton.Enabled := False;
// Hinweis im Browser anzeigen
FMarkdown.Append(sLineBreak + '*Creating Cache...*' + sLineBreak);
FUploadCache :=
TFuture<TProc>.Construct(
FUploadCache.Done,
function: TProc
var
client: IGeminiClient;
cacheName: string;
errMsg: string;
begin
try
client := TGeminiClient.Create(apiKey, modelName);
cacheName := client.CreateCache(content, sysInstr, 300);
client.ActiveCacheName := cacheName;
FCachedClient := client;
Result :=
procedure
begin
FMarkdown.Append('**Success: Cache created!**' + sLineBreak);
FMarkdown.Append('ID: `' + cacheName + '`' + sLineBreak);
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
);
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;
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.