355 lines
11 KiB
ObjectPascal
355 lines
11 KiB
ObjectPascal
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<string>;
|
|
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.
|