unit MainUnit; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, 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.Api.Gemini, Myc.Api.MarkdownStream; 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; 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; procedure InitChat; procedure UpdateQuotaDisplay; public procedure GetAvailableModels; end; var Form1: TForm1; implementation uses System.Threading, Myc.System.Quota; {$R *.fmx} const FallbackModel = 'gemini-2.5-flash-lite'; procedure TForm1.FormCreate(Sender: TObject); begin // 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); TTask.Run( procedure var CurrentChat: IGeminiChat; resultData: TGeminiResult; // State Tracking für den Stream LastWasThought: Boolean; FirstChunk: Boolean; begin CurrentChat := FChat; if CurrentChat = nil then exit; LastWasThought := False; FirstChunk := True; // --- STREAMING --- resultData := CurrentChat.SendMessageStream( prompt, procedure(const TextChunk: string; IsThought: Boolean) begin TThread.Queue( nil, 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; // Inhalt anhängen 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; 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; TTask.Run( procedure var client: IGeminiClient; modelList: TArray; begin client := TGeminiClient.Create(apiKey); modelList := client.ListModels; TThread.Queue( 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; 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); TTask.Run( procedure 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; TThread.Queue( 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 begin FMarkdown.Append('**Cache Upload Failed:** ' + errMsg + sLineBreak); UploadCacheButton.Enabled := True; end ); end; end; end ); end; end.