diff --git a/README.md b/README.md index f2d9072c..e1ca3e6a 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,9 @@ | ----------------------------------- | ----------------------------------- | ----------------------------------- | | [](https://highbyte.se/dotnet-6502/app) | [](#highbytedotnet6502appsilknetnative) | [](#highbytedotnet6502appsadconsole) | -| Run 6502 machine code in your own .NET apps | Machine code monitor | -| ----------------------------------- | ----------------------------------- | -| ![Code integration](doc/Screenshots/Code_integration.png 'Code integration') | ![SilkNet native app, C64 monitor](doc/Screenshots/SilkNetNative_Monitor.png 'SilkNet native app, C64 monitor') | +| C64 Basic AI code completion | Run 6502 machine code in your own .NET apps | Machine code monitor | +| ----------------------------------- | ----------------------------------- | ----------------------------------- | +| ![C64 Basic AI code completion](doc/Screenshots/WASM_C64_Basic_AI.png 'C64 Basic AI code completion') | ![Code integration](doc/Screenshots/Code_integration.png 'Code integration') | ![SilkNet native app, C64 monitor](doc/Screenshots/SilkNetNative_Monitor.png 'SilkNet native app, C64 monitor') | ## Common libraries - [`Highbyte.DotNet6502`](doc/CPU_LIBRARY.md) @@ -108,6 +108,9 @@ A console application with a only UI being a machine code monitor. +## C64 Basic AI code completion +See [here](doc/SYSTEMS_C64_AI_CODE_COMPLETION.md) + # Limitations > [!IMPORTANT] > - Correct emulation of all aspects of computers such as Commodore 64 is not likely. diff --git a/doc/SYSTEMS_C64.md b/doc/SYSTEMS_C64.md index 51af8899..b90e1fbc 100644 --- a/doc/SYSTEMS_C64.md +++ b/doc/SYSTEMS_C64.md @@ -19,12 +19,16 @@ Current capabilities - Timers - IRQ - Limited SID 6581 audio chip support -- WASM and native app UI +- WASM and native (SilkNet and SadConsole) app UI:s # C64 programs that works and how to run them See [`SYSTEMS_C64_COMPATIBLE_PRG.md`](SYSTEMS_C64_COMPATIBLE_PRG.md) +## C64 Basic AI code completion + +See [SYSTEMS_C64_AI_CODE_COMPLETION.md`](SYSTEMS_C64_AI_CODE_COMPLETION.md) + # Implementations - System logic [`Highbyte.DotNet6502.Systems.Commodore64`](../src/libraries/Highbyte.DotNet6502.Systems.Commodore64) diff --git a/doc/SYSTEMS_C64_AI_CODE_COMPLETION.md b/doc/SYSTEMS_C64_AI_CODE_COMPLETION.md new file mode 100644 index 00000000..049f308b --- /dev/null +++ b/doc/SYSTEMS_C64_AI_CODE_COMPLETION.md @@ -0,0 +1,103 @@ +

Commodore64 Basic AI Code Completion

+ +# Overview + +There is an experimental AI-powered Basic coding assistant available in the [C64 emulator ](SYSTEMS_C64.md) that can be turned on. This a work in progress. + +## Background + +> [!NOTE] +> If AI capabilities like GitHub CoPilot were available in the 80s, how could a similar experience be had in the then existing Commodore 64 Basic editor? That was the thought experiment that lead to the idea of trying to integrate a AI coding assistant inside the C64 Basic (with existing 80s UI limitations). + +## Use +> [!IMPORTANT] +> Assuming the coding assistant is [configured](#configure) correctly, the assistant can be used when writing Commodore 64 Basic programs inside the emulator. + +### Turn on/off +Use the `AI Basic` checkbox or F9 to toggle assistant on/off. + +### Code suggestions +- After a Basic line number has been entered, and then stopped typing for a short while, a suggestion for the rest of the line will be displayed in grey text. +- Accept the suggestion by pressing `Tab`. +- Ignore the suggestion by pressing any other key. + +> [!INFORMATION] +> - Suggestions only appear after you've entered a line number (that is not followed by `REM` comment statement). +> - If no suggestion appears, it's either because the AI backend could not give any suggestion, or that the request to the AI backend failed for some reason (verify that the connection works via Config UI, see [here](#configure)). + +> [!TIP] +> You can enter a Basic line with a comment (`REM` statement) and explain what you want the program to do. Then when you start typing the next several lines it will continuously suggest new code that will build up your program based on the comment. + +![C64 Basic AI code completion](Screenshots/WASM_C64_Basic_AI.png 'C64 Basic AI code completion') + + +## Configure +> [!IMPORTANT] +> The AI coding assistant is currently available in the `WebAssembly` and `SadConsole` versions of the emulator. + +### WebAssembly version +The configuration is done in the `Configuration` section -> `C64 Config` button -> `Basic AI coding assistant` section. + +#### AI backend type: `CustomEndpoint` (temporarily available) +There is a _temporarily available_ AI backend that won't require your own OpenAI API key. This is currently the default setting. +- Select `AI backend type` to `CustomEndpoint`. +- Press `Test` to verify that custom endpoint is available. +- It will be a bit slower than using OpenAI directly. + +> [!NOTE] +> The field `Custom endpoint API key` is currently pre-populated with a public available key for the custom endpoint (_it's not an OpenAI key_). This is for future use. + +#### AI backend type: `OpenAI` +If you have your own OpenAI API key, you can connect to OpenAI directly. + +> [!CAUTION] +> - Use this at your own risk. Using your own OpenAI API key will use your own credit grants. It's a good idea to set a limit in OpenAI for how much can be used. +> - You can inspect the network traffic from browser (F12 devtool) to see which (and how many) requests are done to OpenAI. +> - The OpenAI API key is only stored in your browser local storage, it's not stored on any server. This can be verified in the browser with the F12 devtool. + +- Set `AI backend type` to `OpenAI`. +- Set `OpenAI API key` to the OpenAI API key. +- Press `Test` to verify that OpenAI API key works against OpenAI API. + +#### AI backend type: `OpenAISelfHostedCodeLlama` +If you host your own AI model with [Ollama](https://ollama.com/), you can use a local `CodeLlama-code` model as source for the C64 Basic AI assistant. + +> [!IMPORTANT] +> - With Ollama installed, download a CodeLlama-code model for example `codellama:13b-code` or `codellama:7b-code`. The larger the model the better, as long as your machine can handle it. Other types of models (non CodeLlama-code) may not work. +> - Ex. download model: `ollama pull codellama:13b-code` +> - Make sure Ollama (or any proxy in front of it) has configured [CORS Settings](https://medium.com/dcoderai/how-to-handle-cors-settings-in-ollama-a-comprehensive-guide-ee2a5a1beef0) to allow requests from the site running the WebAssembly version of the Emulator (or all *). + +- Set `AI backend type` to `OpenAISelfHostedCodeLlama`. +- Set `Self-hosted OpenAI compatible endpoint (Ollama)` to the self-hosted endpoint. The default is `http://localhost:11434/api` +- Set `Model name` to a locally installed CodeLlama-code model, for example `codellama:13b-code` or `codellama:7b-code`. Other non CodeLlama-code models may not work. +- Optionally set `Self-hosted API key (optional)` if a API key is required to access the self-hosted endpoint (for example if Open WebUI is used as a proxy in front of Ollama endpoint). +- Press `Test` to verify that OpenAI API key works against OpenAI API. + + +#### AI backend type: `None` +If you want to disable the coding assistant. +- Set `AI backend type` to `None`. + +![C64 Basic AI code completion](Screenshots/WASM_C64_Basic_AI_Config.png 'C64 Basic AI code completion') + +### SadConsole version +Configure `CodingAssistant` section in `appsettings.json`. + +Using OpenAI: +- `CodingAssistantType:OpenAI:CodingAssistantType`: `OpenAI` +- `CodingAssistantType:OpenAI:ApiKey`: Your own OpenAI API key +- `CodingAssistantType:OpenAI:DeploymentName`: The OpenAI model (default: `gpt-4o`) + +Using self-hosted OpenAI API compatible LLM (Ollama with CodeLlama-code model): +- `CodingAssistantType:OpenAISelfHostedCodeLlama:CodingAssistantType`: `OpenAI` +- `CodingAssistantType:OpenAISelfHostedCodeLlama:EndPoint`: The local Ollama HTTP endpoint (ex: `http://localhost:11434/api`) +- `CodingAssistantType:OpenAISelfHostedCodeLlama:DeploymentName`: A local CodeLlama-code model (ex: `codellama:13b-code` or `codellama:7b-code`.) +- `CodingAssistantType:OpenAISelfHostedCodeLlama:ApiKey`: Optional. May be required if Open WebUI proxy is in front of Ollama. + +Using custom AI backend: +TODO + +Using no assistant: +- `CodingAssistantType:OpenAI:CodingAssistantType`: `None` + +In the emulator UI, use the `C64 Config` -> Basic AI assistant -> `Test` button to verify the connection. diff --git a/doc/SYSTEMS_C64_COMPATIBLE_PRG.md b/doc/SYSTEMS_C64_COMPATIBLE_PRG.md index 59cf2b11..710d9839 100644 --- a/doc/SYSTEMS_C64_COMPATIBLE_PRG.md +++ b/doc/SYSTEMS_C64_COMPATIBLE_PRG.md @@ -1,6 +1,6 @@

Compatible C64 programs

-A list of applications that seem to work decently with the C64 emulator. +A list of applications that seem to work decently with the [C64 emulator](SYSTEMS_C64.md). > **Limitations:**
> - The C64 emulator currently lacks support for the C64 disk and tape drives. Therefore programs must be loaded from the emulator menu (or monitor) as **.prg** files from the host OS file system. Also, any loaded .prg file that tries to access the C64 disk or tape drive most likely will not work (hang). diff --git a/doc/Screenshots/SadConsole_C64_Basic.png b/doc/Screenshots/SadConsole_C64_Basic.png index 678a7942..dba7723c 100644 Binary files a/doc/Screenshots/SadConsole_C64_Basic.png and b/doc/Screenshots/SadConsole_C64_Basic.png differ diff --git a/doc/Screenshots/WASM_C64_Basic.png b/doc/Screenshots/WASM_C64_Basic.png index 905c6e0b..62c6998a 100644 Binary files a/doc/Screenshots/WASM_C64_Basic.png and b/doc/Screenshots/WASM_C64_Basic.png differ diff --git a/doc/Screenshots/WASM_C64_Basic_AI.png b/doc/Screenshots/WASM_C64_Basic_AI.png new file mode 100644 index 00000000..65b1b548 Binary files /dev/null and b/doc/Screenshots/WASM_C64_Basic_AI.png differ diff --git a/doc/Screenshots/WASM_C64_Basic_AI_Config.png b/doc/Screenshots/WASM_C64_Basic_AI_Config.png new file mode 100644 index 00000000..c7294062 Binary files /dev/null and b/doc/Screenshots/WASM_C64_Basic_AI_Config.png differ diff --git a/doc/Screenshots/WASM_C64_LastNinja.png b/doc/Screenshots/WASM_C64_LastNinja.png index 29fb363e..feb0ce01 100644 Binary files a/doc/Screenshots/WASM_C64_LastNinja.png and b/doc/Screenshots/WASM_C64_LastNinja.png differ diff --git a/doc/Screenshots/WASM_C64_Monitor.png b/doc/Screenshots/WASM_C64_Monitor.png index 6e01f61a..f032ce20 100644 Binary files a/doc/Screenshots/WASM_C64_Monitor.png and b/doc/Screenshots/WASM_C64_Monitor.png differ diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs index f88f3c6c..ab99f3c1 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs @@ -140,13 +140,15 @@ private void DrawUIItems() // AI coding assistant selection var codingAssistantLabel = CreateLabel("Basic AI assistant: ", 1, romDownloadLinkTextBox.Bounds.MaxExtentY + 3); var codingAssistantValue = CreateLabel($"{C64HostConfig.CodeSuggestionBackendType}", codingAssistantLabel.Bounds.MaxExtentX + 1, codingAssistantLabel.Position.Y); + codingAssistantValue.TextColor = Controls.GetThemeColors().White; var codingAssistantInfoLabel = new Label(Width - 10) { Name = "codingAssistantInfoLabel", Position = (1, codingAssistantValue.Bounds.MaxExtentY + 1), IsEnabled = false, - DisplayText = "Set AI assistant in appsetting.json", + DisplayText = "Set AI assistant in appsetting.json.", + TextColor = Controls.GetThemeColors().Appearance_ControlDisabled.Foreground }; Controls.Add(codingAssistantInfoLabel); @@ -159,7 +161,10 @@ private void DrawUIItems() { try { - var codeSuggestionBackend = CodeSuggestionConfigurator.CreateCodeSuggestion(C64HostConfig.CodeSuggestionBackendType, _configuration, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION); + var codeSuggestionBackend = CodeSuggestionConfigurator.CreateCodeSuggestion(C64HostConfig.CodeSuggestionBackendType, _configuration, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION, C64BasicCodingAssistant.CODE_COMPLETION_ADDITIONAL_SYSTEM_INSTRUCTION); + codingAssistantInfoLabel.DisplayText = "Testing..."; + codingAssistantInfoLabel.TextColor = Color.White; + await codeSuggestionBackend.CheckAvailability(); if (codeSuggestionBackend.IsAvailable) { @@ -180,6 +185,14 @@ private void DrawUIItems() }; Controls.Add(codingAssistantTestButton); + var openBasicAIHelpURLButton = new Button("Help") + { + Name = "openBasicAIHelpURLButton", + Position = (codingAssistantTestButton.Bounds.MaxExtentX, codingAssistantInfoLabel.Position.Y), + }; + openBasicAIHelpURLButton.Click += (s, e) => OpenURL("https://github.com/highbyte/dotnet-6502/blob/master/doc/SYSTEMS_C64_AI_CODE_COMPLETION.md"); + Controls.Add(openBasicAIHelpURLButton); + //ComboBox codingAssistantComboBox = new ComboBox(codingAssistantLabel.Bounds.MaxExtentX + 1, codingAssistantLabel.Position.Y, 6, Enum.GetNames().ToArray()) //{ diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs index e46d50f5..be3a68ac 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs @@ -303,7 +303,7 @@ private void SetControlStates() public async Task ToggleBasicAIAssistant() { var c64aiBasicAssistantCheckbox = Controls["c64aiBasicAssistantCheckbox"] as CheckBox; - await SetBasicAIAssistant(!c64aiBasicAssistantCheckbox.IsSelected); + c64aiBasicAssistantCheckbox.IsSelected = !c64aiBasicAssistantCheckbox.IsSelected; } private async Task SetBasicAIAssistant(bool enabled) diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/InfoConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/InfoConsole.cs index 43974c2a..8734d8e7 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/InfoConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/InfoConsole.cs @@ -191,6 +191,14 @@ Label CreateLabel(string text, int col, int row, string? name = null) CreateLabel("Change text color 9-16", colTab1, row, Controls.ThemeColors.ControlHostForeground); CreateLabel("C= + numbers 1-8", colTab2, row, Controls.ThemeColors.ControlHostForeground); CreateLabel("LeftCtrl + numbers 1-8", colTab3, row, Controls.ThemeColors.ControlHostForeground); + row++; + CreateLabel("AI Basic: accept suggestion", colTab1, row, Controls.ThemeColors.ControlHostForeground); + CreateLabel("CTRL", colTab2, row, Controls.ThemeColors.ControlHostForeground); + CreateLabel("Tab", colTab3, row, Controls.ThemeColors.ControlHostForeground); + row++; + CreateLabel("AI Basic: ignore suggestion", colTab1, row, Controls.ThemeColors.ControlHostForeground); + CreateLabel("Any other key than CTRL", colTab2, row, Controls.ThemeColors.ControlHostForeground); + CreateLabel("Any other key than Tab", colTab3, row, Controls.ThemeColors.ControlHostForeground); Label CreateLabel(string text, int col, int row, Color? textColor = null, string? name = null) { diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs index 937cd925..0c55157a 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs @@ -501,6 +501,7 @@ public void DisableMonitor() _monitorStatusConsole.Disable(); OnMonitorStateChange(monitorEnabled: false); } + public void EnableMonitor(ExecEvaluatorTriggerResult? execEvaluatorTriggerResult = null) { _monitorConsole.Enable(execEvaluatorTriggerResult); @@ -589,17 +590,24 @@ private async Task HandleUIKeyboardInput() // ToggleLogs(); if (keyboard.IsKeyPressed(Keys.F11)) + { + keyboard.Clear(); ToggleInfo(); + } - if (keyboard.IsKeyPressed(Keys.F12) && (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused)) + if (keyboard.IsKeyReleased(Keys.F12) && (EmulatorState == EmulatorState.Running || EmulatorState == EmulatorState.Paused)) { + keyboard.Clear(); ToggleMonitor(); } - if (keyboard.IsKeyPressed(Keys.F9) && EmulatorState == EmulatorState.Running) + if (keyboard.IsKeyReleased(Keys.F9) && EmulatorState == EmulatorState.Running) { + keyboard.Clear(); if (_systemMenuConsole is C64MenuConsole c64MenuConsole) + { await c64MenuConsole.ToggleBasicAIAssistant(); + } } } } diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64Setup.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64Setup.cs index 0d334fb6..0d0a5d8c 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64Setup.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SystemSetup/C64Setup.cs @@ -84,7 +84,7 @@ NAudioAudioHandlerContext audioHandlerContext var renderer = new C64SadConsoleRenderer(c64, renderContext); - ICodeSuggestion codeSuggestion = CodeSuggestionConfigurator.CreateCodeSuggestion(c64HostConfig.CodeSuggestionBackendType, _configuration, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION, defaultToNoneIdConfigError: true); + ICodeSuggestion codeSuggestion = CodeSuggestionConfigurator.CreateCodeSuggestion(c64HostConfig.CodeSuggestionBackendType, _configuration, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION, C64BasicCodingAssistant.CODE_COMPLETION_ADDITIONAL_SYSTEM_INSTRUCTION , defaultToNoneIdConfigError: true); var c64BasicCodingAssistant = new C64BasicCodingAssistant(c64, codeSuggestion, _loggerFactory); var inputHandler = new C64SadConsoleInputHandler(c64, inputHandlerContext, _loggerFactory, c64BasicCodingAssistant, c64HostConfig.BasicAIAssistantDefaultEnabled); diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json index 69eeb371..ef2b47fa 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json @@ -69,8 +69,15 @@ //"Endpoint": "https://YOUR_ACCOUNT.openai.azure.com/" }, + "OpenAISelfHostedCodeLlama": { + "Endpoint": "http://localhost:11434/api", + //"DeploymentName": "codellama:7b-code", // Works sometimes (must be a CodeLlama:xxx-code model to work). + "DeploymentName": "codellama:13b-code" // Works ok (must be a CodeLlama:xxx-code model to work) + //"ApiKey": "[SET IN DOTNET USER SECRETS]" // API key may not be required for self-hosted + }, + "CustomEndpoint": { - // Set to true to use special endpoint that encapsulates code completion requests and forwards to OpenAI API (no OpenAI key is required on client, instead a API key for the custom endpoint) + // A custom endpoint that encapsulates code completion requests and forwards to OpenAI API (no OpenAI key is required on client, instead a API key for the custom endpoint) "Endpoint": "", // dotnet user-secrets set "CodingAssistant:CustomEndpoint:ApiKey" "[MY API KEY]" diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs index 456b4944..d929c0b7 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64Setup.cs @@ -206,7 +206,12 @@ public static async Task GetCodeSuggestionImplementation(C64Hos if (c64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.OpenAI) { var openAIApiConfig = await GetOpenAIConfig(localStorageService); - codeSuggestion = new OpenAICodeSuggestion(openAIApiConfig, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION); + codeSuggestion = OpenAICodeSuggestion.CreateOpenAICodeSuggestionForOpenAI(openAIApiConfig, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION, C64BasicCodingAssistant.CODE_COMPLETION_ADDITIONAL_SYSTEM_INSTRUCTION); + } + else if (c64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.OpenAISelfHostedCodeLlama) + { + var openAIApiConfig = await GetOpenAISelfHostedCodeLlamaConfig(localStorageService); + codeSuggestion = OpenAICodeSuggestion.CreateOpenAICodeSuggestionForCodeLlama(openAIApiConfig, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION, C64BasicCodingAssistant.CODE_COMPLETION_ADDITIONAL_SYSTEM_INSTRUCTION); } else if (c64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.CustomEndpoint) { @@ -236,31 +241,46 @@ public static async Task GetCodeSuggestionImplementation(C64Hos public static async Task GetOpenAIConfig(ILocalStorageService localStorageService) { var apiKey = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:ApiKey"); + var deploymentName = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:DeploymentName"); if (string.IsNullOrEmpty(deploymentName)) - deploymentName = "gpt-4o"; // Default to a model that works well + deploymentName = "gpt-4o"; // Default to a OpenAI model that works well - var endpoint = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:Endpoint"); - Uri.TryCreate(endpoint, UriKind.Absolute, out var endPointUri); - - var selfHosted = await localStorageService.GetItemAsync($"{ApiConfig.CONFIG_SECTION}:SelfHosted"); + // For future use: Endpoint can be set if OpenAI is accessed via Azure endpoint. + //var endpoint = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:Endpoint"); + //Uri.TryCreate(endpoint, UriKind.Absolute, out var endPointUri); var apiConfig = new ApiConfig() { ApiKey = apiKey, // Api key for OpenAI (required), Azure OpenAI (required), or SelfHosted (optional). DeploymentName = deploymentName, // AI model name - Endpoint = endPointUri, // Used if using Azure OpenAI, or SelfHosted - SelfHosted = selfHosted, // Set to true to use self-hosted OpenAI API compatible endpoint. + //Endpoint = endPointUri, // Used if using Azure OpenAI + SelfHosted = false, }; return apiConfig; } - public static async Task SaveOpenAICodingAssistantConfigToLocalStorage(ILocalStorageService localStorageService, ApiConfig apiConfig) + public static async Task GetOpenAISelfHostedCodeLlamaConfig(ILocalStorageService localStorageService) { - await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:ApiKey", apiConfig.ApiKey); - await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:DeploymentName", apiConfig.DeploymentName); - await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:Endpoint", apiConfig.Endpoint != null ? apiConfig.Endpoint.OriginalString : ""); - await localStorageService.SetItemAsync($"{ApiConfig.CONFIG_SECTION}:SelfHosted", apiConfig.SelfHosted); + var apiKey = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:ApiKey"); + if (apiKey == string.Empty) + apiKey = null; + var deploymentName = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:DeploymentName"); + if (string.IsNullOrEmpty(deploymentName)) + deploymentName = "codellama:13b-code"; // Default to a Ollama CodeLlama-code model that seems to work OK (but not as good as OpenAI gpt-4o) + var endpoint = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:Endpoint"); + if (string.IsNullOrEmpty(endpoint)) + endpoint = "http://localhost:11434/api"; // Default to local Ollama + Uri.TryCreate(endpoint, UriKind.Absolute, out var endPointUri); + + var apiConfig = new ApiConfig() + { + ApiKey = apiKey, // Optional for Self-hosted model. + DeploymentName = deploymentName, // AI CodeLlama-code model name (ex: codellama:13b-code, codellama:7b-code) + Endpoint = endPointUri, // Self-hosted OpenAI API compatible endpoint (for example Ollama) + SelfHosted = true // Set to true to use self-hosted OpenAI API compatible endpoint. + }; + return apiConfig; } public static async Task GetCustomAIEndpointConfig(ILocalStorageService localStorageService) @@ -282,9 +302,22 @@ public static async Task GetCustomAIEndpointConfig(ILoca return apiConfig; } + public static async Task SaveOpenAICodingAssistantConfigToLocalStorage(ILocalStorageService localStorageService, ApiConfig apiConfig) + { + await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:ApiKey", apiConfig.ApiKey ?? string.Empty); + //await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:Endpoint", apiConfig.Endpoint != null ? apiConfig.Endpoint.OriginalString : string.Empty); + } + + public static async Task SaveOpenAISelfHostedCodeLlamaCodingAssistantConfigToLocalStorage(ILocalStorageService localStorageService, ApiConfig apiConfig) + { + await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:ApiKey", apiConfig.ApiKey ?? string.Empty); + await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:DeploymentName", apiConfig.DeploymentName ?? string.Empty); + await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:Endpoint", apiConfig.Endpoint != null ? apiConfig.Endpoint.OriginalString : string.Empty); + } + public static async Task SaveCustomCodingAssistantConfigToLocalStorage(ILocalStorageService localStorageService, CustomAIEndpointConfig customAIEndpointConfig) { - await localStorageService.SetItemAsStringAsync($"{CustomAIEndpointConfig.CONFIG_SECTION}:ApiKey", customAIEndpointConfig.ApiKey); - await localStorageService.SetItemAsStringAsync($"{CustomAIEndpointConfig.CONFIG_SECTION}:Endpoint", customAIEndpointConfig.Endpoint != null ? customAIEndpointConfig.Endpoint.OriginalString : ""); + await localStorageService.SetItemAsStringAsync($"{CustomAIEndpointConfig.CONFIG_SECTION}:ApiKey", customAIEndpointConfig.ApiKey ?? string.Empty); + await localStorageService.SetItemAsStringAsync($"{CustomAIEndpointConfig.CONFIG_SECTION}:Endpoint", customAIEndpointConfig.Endpoint != null ? customAIEndpointConfig.Endpoint.OriginalString : string.Empty); } } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs index efd07ea7..61c4c717 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/WasmMonitor.cs @@ -190,11 +190,16 @@ public void LoadBinaryFromUser(byte[] fileData) WriteOutput($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); - // Set PC to start of loaded file. - Cpu.PC = loadedAtAddress; - if (_lastTriggeredAfterLoadCallback != null) + { + // If _lastTriggeredAfterLoadCallback is set, don't assume binary (do not set Program Counter to load address automatically). _lastTriggeredAfterLoadCallback(this, loadedAtAddress, fileLength); + } + else + { + // Assume binary is loaded, set Program Counter to loaded address. + Cpu.PC = loadedAtAddress; + } DisplayStatus(); diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor index 2dd90b65..17c49042 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor @@ -11,232 +11,282 @@ @using static Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral.C64Joystick; @using static Highbyte.DotNet6502.App.WASM.Pages.Index -

ROMs

-

The C64 system requires the following types of ROM files: Kernal, Basic, and Character generator.

-

- Use existing C64 ROM files on your computer, or download them to your computer from the internet.

-

- Upload the ROM files from your computer to this emulator with the button below.

-
-
-
- -

- -
-
-

General settings

-
-
-
Renderer:
-
- + + +
+ Video/audio +
+
+ @*

General settings

*@ +
+
+
Renderer:
+
+ +
+
+
+
Audio enabled (experimental):
+
-
-
-
Audio enabled (experimental):
-
-
-
-
-

Joystick

- @{ - var gamepadToJoystickMap = C64HostConfig.InputConfig.GamePadToC64JoystickMap[C64HostConfig.InputConfig.CurrentJoystick]; - } -
-
-
Select current joystick
-
- -
-
-
-
Joystick @C64HostConfig.InputConfig.CurrentJoystick action
-
Gamepad button
-
+ + +
+ Joystick settings +
+
+

Joystick

@{ - foreach (var mapKey in gamepadToJoystickMap) - { -
-
@string.Join(",", mapKey.Value)
-
@string.Join(",", mapKey.Key)
+ var gamepadToJoystickMap = C64HostConfig.InputConfig.GamePadToC64JoystickMap[C64HostConfig.InputConfig.CurrentJoystick]; + } +
+
+
Select current joystick
+
+
+
+
+
Joystick @C64HostConfig.InputConfig.CurrentJoystick action
+
Gamepad button
+
+ @{ + foreach (var mapKey in gamepadToJoystickMap) + { +
+
@string.Join(",", mapKey.Value)
+
@string.Join(",", mapKey.Key)
+
+ } } - } +
-
- -
-

Joystick keyboard

- @{ - var keyToJoystickMap = C64SystemConfig.KeyboardJoystickMap; - } -
-
-
Enabled
-
-
+
-
-
Select current keyboard joystick
-
- +

Joystick keyboard

+ @{ + var keyToJoystickMap = C64SystemConfig.KeyboardJoystickMap; + } +
+
+
Enabled
+
-
-
-
Joystick @C64SystemConfig.KeyboardJoystick action
-
Key
-
- @{ - foreach (var mapKey in keyToJoystickMap.GetMap(C64SystemConfig.KeyboardJoystick)) - { -
-
@string.Join(",", mapKey.Value)
-
@string.Join(",", mapKey.Key)
+
+
Select current keyboard joystick
+
+
+
+ +
+
Joystick @C64SystemConfig.KeyboardJoystick action
+
Key
+
+ @{ + foreach (var mapKey in keyToJoystickMap.GetMap(C64SystemConfig.KeyboardJoystick)) + { +
+
@string.Join(",", mapKey.Value)
+
@string.Join(",", mapKey.Key)
+
+ } } - } +
+
-
+
+
+ Basic AI coding assistant -
-
-

Basic AI coding assistant

-
-
-
Assistant type
-
- -
-
- @if (@C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.OpenAI) - { +
+
+ @*

Basic AI coding assistant

*@ +
-
OpenAI API key
+
AI backend type
- @if (_openAIApiConfig != null) - { - - } +
- } - @if (@C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.CustomEndpoint) - { -
+ @if (@C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.OpenAI) + { +
+
OpenAI API key
+
+ @if (_openAIApiConfig != null) + { + + } +
+
+ } -
Custom AI endpoint
-
- @if (_customEndpointAIApiConfig != null) - { - @_customEndpointAIApiConfig.Endpoint - } + @if (@C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.OpenAISelfHostedCodeLlama) + { +
+
Self-hosted OpenAI compatible endpoint (Ollama)
+
+ @if (_openAISelfHostedCodeLlamaAIApiConfig != null) + { + + } +
-
-
-
Custom endpoint API key
-
- @if (_customEndpointAIApiConfig != null) - { - - } + +
+
Model name
+
+ @if (_openAISelfHostedCodeLlamaAIApiConfig != null) + { + + } +
-
- } -
-
-
- @if (!string.IsNullOrEmpty(_aiBackendValidationMessage)) - { - @_aiBackendValidationMessage - } -
+ +
+
Self-hosted API key (optional)
+
+ @if (_openAISelfHostedCodeLlamaAIApiConfig != null) + { + + } +
+
+ } + + @if (@C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.CustomEndpoint) + { +
+ +
Custom AI endpoint
+
+ @if (_customEndpointAIApiConfig != null) + { + @_customEndpointAIApiConfig.Endpoint + } +
+
+
+
Custom endpoint API key
+
+ @if (_customEndpointAIApiConfig != null) + { + + } +
+
+ } + + @if (@C64HostConfig.CodeSuggestionBackendType != CodeSuggestionBackendTypeEnum.None) + { +
+
+
+ @if (!string.IsNullOrEmpty(_aiBackendValidationMessage)) + { + @_aiBackendValidationMessage + } +
+
+ }
-
- +

@code { + [Inject] public ILocalStorageService LocalStorage { get; set; } = default!; + [Inject] + public IJSRuntime Js { get; set; } = default!; [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = default!; @@ -246,21 +296,28 @@ public C64HostConfig C64HostConfig => (C64HostConfig)HostSystemConfig; public C64SystemConfig C64SystemConfig => C64HostConfig.SystemConfig; - protected override async Task OnInitializedAsync() { _openAIApiConfig = await C64Setup.GetOpenAIConfig(LocalStorage); + _openAISelfHostedCodeLlamaAIApiConfig = await C64Setup.GetOpenAISelfHostedCodeLlamaConfig(LocalStorage); _customEndpointAIApiConfig = await C64Setup.GetCustomAIEndpointConfig(LocalStorage); - BlazoredModal.SetTitle("C64 config"); - } + // If ROMs aren't loadad, expand HTML details element for roms (setting property open to true) + if (LoadedROMCount < _requiredNumberOfRomFiles) + await Js.InvokeVoidAsync("setOpen", "roms_section", true); + } private void UnloadROMs() => C64HostConfig.SystemConfig.ROMs = new List(); private ApiConfig _openAIApiConfig = default!; private InputText _openAIApiKeyInputText = default!; + private ApiConfig _openAISelfHostedCodeLlamaAIApiConfig = default!; + private InputText _openAISelfHostedCodeLlamaEndpointInputText = default!; + private InputText _openAISelfHostedCodeLlamaModelNameInputText = default!; + private InputText _openAISelfHostedCodeLlamaApiKeyInputText = default!; + private CustomAIEndpointConfig _customEndpointAIApiConfig = default!; private InputText _customEndpointAIApiKeyInputText = default!; @@ -270,6 +327,10 @@ { await C64Setup.SaveOpenAICodingAssistantConfigToLocalStorage(LocalStorage, _openAIApiConfig); } + if (C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.OpenAISelfHostedCodeLlama) + { + await C64Setup.SaveOpenAISelfHostedCodeLlamaCodingAssistantConfigToLocalStorage(LocalStorage, _openAISelfHostedCodeLlamaAIApiConfig); + } else if (C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.CustomEndpoint) { await C64Setup.SaveCustomCodingAssistantConfigToLocalStorage(LocalStorage, _customEndpointAIApiConfig); @@ -280,7 +341,7 @@ private bool _isLoadingC64Roms; private long _maxC64RomFileSize = 1024 * 8; - private int _maxC64AllowedRomFiles = 3; + private int _requiredNumberOfRomFiles = 3; private Dictionary GetLoadedRoms() { @@ -306,7 +367,7 @@ _isLoadingC64Roms = true; _validationMessage = ""; - foreach (var file in e.GetMultipleFiles(_maxC64AllowedRomFiles)) + foreach (var file in e.GetMultipleFiles(_requiredNumberOfRomFiles)) { try { @@ -372,8 +433,12 @@ ICodeSuggestion codeSuggestion; if(C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.OpenAI) { - codeSuggestion = new OpenAICodeSuggestion(_openAIApiConfig, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION); + codeSuggestion = OpenAICodeSuggestion.CreateOpenAICodeSuggestionForOpenAI(_openAIApiConfig, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION, C64BasicCodingAssistant.CODE_COMPLETION_ADDITIONAL_SYSTEM_INSTRUCTION); } + else if(C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.OpenAISelfHostedCodeLlama) + { + codeSuggestion = OpenAICodeSuggestion.CreateOpenAICodeSuggestionForCodeLlama(_openAISelfHostedCodeLlamaAIApiConfig, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION, C64BasicCodingAssistant.CODE_COMPLETION_ADDITIONAL_SYSTEM_INSTRUCTION); + } else if(C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.CustomEndpoint) { codeSuggestion = new CustomAIEndpointCodeSuggestion(_customEndpointAIApiConfig, C64BasicCodingAssistant.CODE_COMPLETION_LANGUAGE_DESCRIPTION); diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor index b80450f7..fdea14e2 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor @@ -12,69 +12,79 @@ {
- - - + +

- - - - -

Load/save files

- - - - -

Assembly example files

-
- (info) + +

 

+
+ Load/Save +

Load/save files (info)

+ + + + +

Assembly example files

+
+ + +
+ +

Basic example files

+
+ + +
+ +
+ @_latestFileError +
+
+ +

 

+
+ Configuration + + + - -
- -

Basic example files

-
- + - -
-

Misc.

- - -

- - +
+ +
-
+ -
- @_latestFileError -
-
-
- + +