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
);