diff --git a/GeminiTest/GeminiTest.dpr b/GeminiTest/GeminiTest.dpr index 3b14376..a911ad8 100644 --- a/GeminiTest/GeminiTest.dpr +++ b/GeminiTest/GeminiTest.dpr @@ -5,7 +5,8 @@ uses FMX.Forms, MainUnit in 'MainUnit.pas' {Form1}, Myc.Api.Gemini in 'Myc.Api.Gemini.pas', - Myc.System.Quota in 'Myc.System.Quota.pas'; + Myc.System.Quota in 'Myc.System.Quota.pas', + Myc.Api.MarkdownStream in 'Myc.Api.MarkdownStream.pas'; {$R *.res} diff --git a/GeminiTest/GeminiTest.dproj b/GeminiTest/GeminiTest.dproj index 5b1aeaa..f16be4e 100644 --- a/GeminiTest/GeminiTest.dproj +++ b/GeminiTest/GeminiTest.dproj @@ -132,6 +132,7 @@ + Base @@ -175,6 +176,12 @@ true + + + GeminiTest.exe + true + + GeminiTest.exe @@ -1139,14 +1146,6 @@ - False - False - False - False - False - False - False - False False True diff --git a/GeminiTest/MainUnit.fmx b/GeminiTest/MainUnit.fmx index 9610ee3..10b5165 100644 --- a/GeminiTest/MainUnit.fmx +++ b/GeminiTest/MainUnit.fmx @@ -11,6 +11,7 @@ object Form1: TForm1 DesignerMasterStyle = 0 object AskButton: TButton Anchors = [akRight, akBottom] + Default = True Position.X = 800.000000000000000000 Position.Y = 577.000000000000000000 TabOrder = 1 @@ -85,16 +86,14 @@ object Form1: TForm1 StyleLookup = '' TabOrder = 0 Text = 'Chat' - object ChatMemo: TMemo - Touch.InteractiveGestures = [Pan, LongTap, DoubleTap] - DataDetectorTypes = [] + ExplicitSize.cx = 69.000000000000000000 + ExplicitSize.cy = 26.000000000000000000 + object ChatBrowser: TWebBrowser Align = Client Size.Width = 906.000000000000000000 Size.Height = 535.000000000000000000 Size.PlatformDefault = False - TabOrder = 1 - Viewport.Width = 902.000000000000000000 - Viewport.Height = 531.000000000000000000 + WindowsEngine = EdgeOnly end end object CacheTabItem: TTabItem @@ -108,6 +107,8 @@ object Form1: TForm1 StyleLookup = '' TabOrder = 0 Text = 'Cache' + ExplicitSize.cx = 53.000000000000000000 + ExplicitSize.cy = 26.000000000000000000 object CacheMemo: TMemo Touch.InteractiveGestures = [Pan, LongTap, DoubleTap] DataDetectorTypes = [] @@ -131,6 +132,8 @@ object Form1: TForm1 StyleLookup = '' TabOrder = 0 Text = 'SysInstr' + ExplicitSize.cx = 61.000000000000000000 + ExplicitSize.cy = 26.000000000000000000 object SystemInstructionMemo: TMemo Touch.InteractiveGestures = [Pan, LongTap, DoubleTap] DataDetectorTypes = [] diff --git a/GeminiTest/MainUnit.pas b/GeminiTest/MainUnit.pas index 595d392..14d03ae 100644 --- a/GeminiTest/MainUnit.pas +++ b/GeminiTest/MainUnit.pas @@ -20,8 +20,10 @@ uses FMX.StdCtrls, FMX.Edit, FMX.ListBox, + FMX.TabControl, + FMX.WebBrowser, Myc.Api.Gemini, - FMX.TabControl; + Myc.Api.MarkdownStream; type TForm1 = class(TForm) @@ -32,15 +34,15 @@ type AskMemo: TMemo; ResetChatButton: TButton; TabControl1: TTabControl; - ChatTabItem: TTabItem; CacheTabItem: TTabItem; SysInstrTabItem: TTabItem; - ChatMemo: TMemo; 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); @@ -48,8 +50,8 @@ type procedure UploadCacheButtonClick(Sender: TObject); private FChat: IGeminiChat; - // Holds the client configured with the active cache FCachedClient: IGeminiClient; + FMarkdown: IMarkdownStream; procedure InitChat; procedure UpdateQuotaDisplay; public @@ -63,37 +65,44 @@ implementation uses System.Threading, - Myc.System.Quota; // Einbindung für den globalen Zähler + Myc.System.Quota; {$R *.fmx} const - FallbackModel = 'gemini-3-flash-preview'; + 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); - // Initialen Global-Stand anzeigen 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; - ChatMemo.Lines.Add('--- New Chat Session (Cached Content Active) ---'); + FMarkdown.Append('*--- New Chat Session ---* (Cached Content Active)'); end else begin - FChat := nil; // Lazy init in AskButton - ChatMemo.Lines.Add('--- New Chat Session ---'); + FMarkdown.Append('*--- New Chat Session ---*'); + FChat := nil; end; end; @@ -126,33 +135,39 @@ begin 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 := ''; + // --- 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( @@ -163,19 +178,30 @@ begin nil, procedure begin - if IsThought then + // Statuswechsel oder allererster Chunk -> Neuer Block + if FirstChunk or (IsThought <> LastWasThought) 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; + 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; - ChatMemo.GoToTextEnd; + // Inhalt anhängen + if IsThought then + FMarkdown.Append(TextChunk) // Wird im selben Markdown-Block gerendert + else + FMarkdown.Append(TextChunk); end ); end @@ -187,12 +213,13 @@ begin procedure begin if resultData.Text.StartsWith('Error') then - ChatMemo.Lines.Add(resultData.Text); + begin + FMarkdown.BeginBlock(btMarkdown, 'Error'); + FMarkdown.Append('> ' + resultData.Text); + end; - // Update Labels (Session & Global Registry) QuotaLabel.Text := Format('Input: %d | Output: %d', [resultData.Usage.PromptTokens, resultData.Usage.CandidatesTokens]); UpdateQuotaDisplay; - AskButton.Enabled := True; end ); @@ -202,7 +229,8 @@ end; procedure TForm1.Edit1Exit(Sender: TObject); begin - GetAvailableModels; + if ModelsComboBox.ItemIndex < 0 then + GetAvailableModels; end; procedure TForm1.GetAvailableModels; @@ -213,8 +241,6 @@ begin if apiKey.IsEmpty then exit; - ChatMemo.Lines.Add('Fetching available models...'); - TTask.Run( procedure var @@ -232,27 +258,22 @@ begin begin if Length(modelList) = 0 then begin - ChatMemo.Lines.Add('No models found.'); + 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); - // 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; @@ -271,24 +292,22 @@ var 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.'); + FMarkdown.Append(sLineBreak + '**Error:** Cache content is empty.' + sLineBreak); 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)...'); + // Hinweis im Browser anzeigen + FMarkdown.Append(sLineBreak + '*Creating Cache...*' + sLineBreak); TTask.Run( procedure @@ -298,33 +317,22 @@ begin 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); + FMarkdown.Append('**Success: Cache created!**' + sLineBreak); + FMarkdown.Append('ID: `' + cacheName + '`' + sLineBreak); InitChat; UploadCacheButton.Enabled := True; end ); - except on E: Exception do begin @@ -333,8 +341,7 @@ begin nil, procedure begin - ChatMemo.Lines.Add('Cache Upload Failed:'); - ChatMemo.Lines.Add(errMsg); + FMarkdown.Append('**Cache Upload Failed:** ' + errMsg + sLineBreak); UploadCacheButton.Enabled := True; end );