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, Myc.Api.Gemini, FMX.TabControl; type TForm1 = class(TForm) AskButton: TButton; Edit1: TEdit; ModelsComboBox: TComboBox; QuotaLabel: TLabel; AskMemo: TMemo; ResetChatButton: TButton; TabControl1: TTabControl; ChatTabItem: TTabItem; CacheTabItem: TTabItem; SysInstrTabItem: TTabItem; ChatMemo: TMemo; CacheMemo: TMemo; SystemInstructionMemo: TMemo; UploadCacheButton: TButton; ThinkingCheckBox: TCheckBox; GlobalQuoteLabel: TLabel; procedure FormCreate(Sender: TObject); procedure AskButtonClick(Sender: TObject); procedure Edit1Exit(Sender: TObject); procedure ResetChatButtonClick(Sender: TObject); procedure UploadCacheButtonClick(Sender: TObject); private FChat: IGeminiChat; // Holds the client configured with the active cache FCachedClient: IGeminiClient; procedure InitChat; procedure UpdateQuotaDisplay; public procedure GetAvailableModels; end; var Form1: TForm1; implementation uses System.Threading, Myc.System.Quota; // Einbindung für den globalen Zähler {$R *.fmx} const FallbackModel = 'gemini-3-flash-preview'; procedure TForm1.FormCreate(Sender: TObject); begin // Load default instructions if file exists if FileExists('T:\Myc\KI\gemini.md') then SystemInstructionMemo.Lines.LoadFromFile('T:\Myc\KI\gemini.md', TEncoding.UTF8); // Initialen Global-Stand anzeigen UpdateQuotaDisplay; GetAvailableModels; InitChat; end; procedure TForm1.InitChat; begin if Assigned(FCachedClient) then begin FChat := FCachedClient.StartChat; ChatMemo.Lines.Add('--- New Chat Session (Cached Content Active) ---'); end else begin FChat := nil; // Lazy init in AskButton ChatMemo.Lines.Add('--- New Chat Session ---'); 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); // 1. System Instructions (Ohne Hack, purer Text) if not SystemInstructionMemo.Text.Trim.IsEmpty then client.SystemInstruction := SystemInstructionMemo.Text; // 2. Native Thinking aktivieren // Die API-Unit entscheidet, ob das Modell es unterstützt. // Falls ja, wird der 'thinking' Parameter gesendet. client.EnableThinking := ThinkingCheckBox.IsChecked; FChat := client.StartChat; end; AskButton.Enabled := False; ChatMemo.Lines.Add(sLineBreak + 'User: "' + prompt + '"'); ChatMemo.Lines.Add('Gemini: '); AskMemo.Text := ''; TTask.Run( procedure var CurrentChat: IGeminiChat; resultData: TGeminiResult; begin CurrentChat := FChat; if CurrentChat = nil then exit; // --- STREAMING --- resultData := CurrentChat.SendMessageStream( prompt, procedure(const TextChunk: string; IsThought: Boolean) begin TThread.Queue( nil, procedure begin if IsThought then begin // Visuelle Kennzeichnung für Gedanken // (Hier einfach in eckigen Klammern, besser wäre Farbe) ChatMemo.Text := ChatMemo.Text + ' [Think: ' + TextChunk + '] '; end else begin // Normale Antwort ChatMemo.Text := ChatMemo.Text + TextChunk; end; ChatMemo.GoToTextEnd; end ); end ); // Abschluss im Main Thread TThread.Queue( nil, procedure begin if resultData.Text.StartsWith('Error') then ChatMemo.Lines.Add(resultData.Text); // Update Labels (Session & Global Registry) 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 GetAvailableModels; end; procedure TForm1.GetAvailableModels; var apiKey: string; begin apiKey := Edit1.Text.Trim; if apiKey.IsEmpty then exit; ChatMemo.Lines.Add('Fetching available models...'); 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 ChatMemo.Lines.Add('No models found.'); exit; end; ModelsComboBox.BeginUpdate; try ModelsComboBox.Items.Clear; for mName in modelList do ModelsComboBox.Items.Add(mName); // Versuche Fallback zu finden (ggf. ohne Emoji suffix match) // Vereinfacht: Wähle ersten Index if ModelsComboBox.Items.Count > 0 then begin ModelsComboBox.ItemIndex := ModelsComboBox.Items.IndexOf(FallbackModel); if ModelsComboBox.ItemIndex < 0 then ModelsComboBox.ItemIndex := 0; end; ChatMemo.Lines.Add(Format('Loaded %d models.', [Length(modelList)])); 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; // Keine künstlichen Prompts mehr, nur die reinen Instruktionen sysInstr := SystemInstructionMemo.Text; if content.Trim.IsEmpty then begin ChatMemo.Lines.Add('Error: Cache content is empty.'); exit; end; // Remove Emoji suffix if present from ComboBox for API call if ModelsComboBox.ItemIndex > -1 then modelName := ModelsComboBox.Items[ModelsComboBox.ItemIndex].Split([' '])[0] else modelName := FallbackModel; UploadCacheButton.Enabled := False; ChatMemo.Lines.Add('Creating Cache (this may take a moment)...'); TTask.Run( procedure var client: IGeminiClient; cacheName: string; errMsg: string; begin try // 1. Client erstellen client := TGeminiClient.Create(apiKey, modelName); // 2. Cache erstellen (Content + SysInstr) // Hinweis: Native Thinking ist ein Parameter der GENERIERUNG, nicht des Caches. // Daher laden wir den Cache "neutral" hoch. cacheName := client.CreateCache(content, sysInstr, 300); // 3. Cache aktivieren client.ActiveCacheName := cacheName; // 4. Client speichern // Wenn der User später "Ask" klickt, wird dort EnableThinking gesetzt FCachedClient := client; TThread.Queue( nil, procedure begin ChatMemo.Lines.Add('Success: Cache created!'); ChatMemo.Lines.Add('ID: ' + cacheName); InitChat; UploadCacheButton.Enabled := True; end ); except on E: Exception do begin errMsg := E.Message; TThread.Queue( nil, procedure begin ChatMemo.Lines.Add('Cache Upload Failed:'); ChatMemo.Lines.Add(errMsg); UploadCacheButton.Enabled := True; end ); end; end; end ); end; end.