diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 65ae5ec..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.DS_Store -/zig-cache/ -/zig-out/ -/node_modules/ -/src/app/dist/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 81bc6f3..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "llama.cpp"] - path = llama.cpp - url = https://github.com/ggerganov/llama.cpp diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 76f5567..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "editor.formatOnSave": true, - - "[javascript][json][jsonc][typescript][typescriptreact][javascriptreact][html][css][markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[swift][c][cpp]": { - "editor.tabSize": 4 - }, - "[sql]": { - "editor.defaultFormatter": "adpyke.vscode-sql-formatter" - } -} diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 2f8a332..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,161 +0,0 @@ -MIT License - -Copyright (c) 2023 Kamil Tomšík - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -**Open-Source Acknowledgment** - -This Software, "Ava PLS", incorporates and uses open-source software components. -The use of these components is acknowledged herein, and their respective -licenses are included below. By using Ava PLS, you acknowledge and agree to -respect and comply with the terms and conditions of the following open-source -licenses. - -**List of Open-Source Software Components** - -1. [Zig language](https://ziglang.org/) - MIT -2. [llama.cpp](https://github.com/ggerganov/llama.cpp) - MIT -3. [Preact](https://preactjs.com/) - MIT -4. [Twind](https://twind.style/) - MIT -5. [Lucide](https://github.com/lucide-icons/lucide) - MIT - ---- - -**Zig License** - -The MIT License (Expat) - -Copyright (c) Zig contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ---- - -**llamma.cpp License** - -MIT License - -Copyright (c) 2023 Georgi Gerganov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -**Preact License** - -The MIT License (MIT) - -Copyright (c) 2015-present Jason Miller - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -**Twind License** - -MIT License - -Copyright (c) 2022 [these people](https://github.com/tw-in-js/twind/graphs/contributors) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -**Lucide License** - -ISC License - -Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 3e51db6..0000000 --- a/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Ava PLS - -Air-gapped Virtual Assistant / Personal Language Server\ -[Website](https://avapls.com) | [Twitter](https://twitter.com/cztomsik) | [Discord](https://discord.gg/C47qUJPkkf) - -https://github.com/cztomsik/ava/assets/3526922/790dd1a2-5e59-4a63-a05a-f255b5677269 - -https://github.com/cztomsik/ava/assets/3526922/22dce230-3d91-476d-83b7-22ddcc41fb87 - -https://github.com/cztomsik/ava/assets/3526922/64f16a97-6575-4006-bb81-c46e1f5cfcaa - -https://github.com/cztomsik/ava/assets/3526922/1dcf38a5-cfc9-4b20-9f2e-deb15145d964 - -## Tech stack - -- Zig, C++, Swift UI, SQLite -- Preact, Preact Signals, Twind - -## Local Development - -Make sure you have: - -- [Zig 0.12.0-dev.1769+bf5ab5451](https://ziglang.org/download/) -- [Node.js 20.5.1](https://nodejs.org/) - - only needed for fetching dependencies -- Xcode (for macOS) -- pkg-config (`brew install pkg-config`) - -```bash -npm install -npm run watch -zig build && ./zig-out/bin/ava_aarch64 # or ./zig-out/bin/ava_x86_64 -``` - -## Headless mode (works on Linux!) - -It is now possible to build Ava in headless mode. This will start a server -and you can connect to it using a web browser. - -This is useful if you want to deploy Ava somewhere and connect to it remotely, -or if you are using Linux, because we don't have Qt/GTK support yet. - -```bash -zig build -Dheadless=true && ./zig-out/bin/ava_aarch64 # or ./zig-out/bin/ava_x86_64 -``` - -## macOS 12.6+ (Monterey) - -Xcode is needed because of Swift UI - -``` -sudo xcode-select -switch /Applications/Xcode.app -``` - -## Production build - -```bash -./src/macos/create_dmg.sh -``` - -Or on Windows: - -```bash -./src/windows/create_zip.sh -``` - -## License - -MIT - -## Contributing - -Bug reports and pull requests are welcome but if you want to do a bigger change, please open an issue first to discuss it. diff --git a/_gui.zig b/_gui.zig deleted file mode 100644 index 93283ab..0000000 --- a/_gui.zig +++ /dev/null @@ -1,9 +0,0 @@ -// Zig has recently disallowed usage of @embedFile outside of the package path, -// which is where the root source file is located. We need to @embedFile -// both README.md and zig-out/app/main.js so this is a workaround for that. -// -// On top of that, we can easily switch between headless and GUI mode just by -// changing the root source file. -pub usingnamespace @import("src/main.zig"); - -pub usingnamespace if (@import("builtin").os.tag == .windows) @import("src/windows/winmain.zig") else struct {}; diff --git a/_headless.zig b/_headless.zig deleted file mode 100644 index 696f63b..0000000 --- a/_headless.zig +++ /dev/null @@ -1,8 +0,0 @@ -// Zig has recently disallowed usage of @embedFile outside of the package path, -// which is where the root source file is located. We need to @embedFile -// both README.md and zig-out/app/main.js so this is a workaround for that. -// -// On top of that, we can easily switch between headless and GUI mode just by -// changing the root source file. -pub usingnamespace @import("src/main.zig"); -pub usingnamespace @import("src/cli.zig"); diff --git a/build.zig b/build.zig deleted file mode 100644 index b83edba..0000000 --- a/build.zig +++ /dev/null @@ -1,99 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) !void { - // const target_query = b.standardTargetOptionsQueryOnly(.{}); - // if (target.query.os_tag == .macos and target.query.os_version_min == null) { - // target.query.os_version_min = .{ .semver = try std.SemanticVersion.parse("12.6.0") }; - // std.log.debug("Setting macOS deployment target to {}", .{target.query.os_version_min.?}); - // } - - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - const headless = b.option(bool, "headless", "Build headless webserver") orelse false; - - const options = .{ - .name = "ava", - .root_source_file = .{ .path = if (headless) "_headless.zig" else "_gui.zig" }, - .target = target, - .optimize = optimize, - - // For some reason, linux binaries are huge. Strip them in release mode. - .strip = optimize != .Debug, - }; - - if (headless) { - try buildExe(b, b.addExecutable(options)); - } else switch (target.result.os.tag) { - .macos => try buildExe(b, @import("src/macos/BuildMacos.zig").create(b, options)), - .windows => try buildExe(b, @import("src/windows/BuildWindows.zig").create(b, options)), - else => return error.UnsupportedOs, - } -} - -fn buildExe(b: *std.Build, exe: anytype) !void { - exe.addIncludePath(.{ .path = "llama.cpp" }); - - const sqlite = b.dependency("ava-sqlite", .{ .bundle = exe.rootModuleTarget().os.tag != .macos }); - exe.root_module.addImport("ava-sqlite", sqlite.module("ava-sqlite")); - if (@hasField(@TypeOf(exe.*), "sdk")) sqlite.module("ava-sqlite").addSystemIncludePath(.{ .path = b.fmt("{s}/usr/include", .{exe.sdk}) }); - - try addLlama(b, exe); - - const suffix = if (exe.rootModuleTarget().os.tag == .windows) ".exe" else ""; - const bin = b.addInstallBinFile(exe.getEmittedBin(), b.fmt("ava_{s}{s}", .{ @tagName(exe.rootModuleTarget().cpu.arch), suffix })); - b.getInstallStep().dependOn(&bin.step); -} - -fn addLlama(b: *std.Build, exe: anytype) !void { - const cflags = try flags(b, exe, &.{"-std=c11"}); - const cxxflags = try flags(b, exe, &.{"-std=c++11"}); - - const sources: []const []const u8 = &.{ - "ggml.c", - "ggml-alloc.c", - "ggml-backend.c", - "ggml-quants.c", - "ggml-metal.m", - "llama.cpp", - }; - - for (sources) |f| { - const is_cpp = std.mem.endsWith(u8, f, ".cpp"); - if (std.mem.endsWith(u8, f, ".m") and exe.rootModuleTarget().os.tag != .macos) continue; - - const o = b.addObject(.{ - .name = std.fs.path.basename(f), - .target = exe.root_module.resolved_target.?, - .optimize = .ReleaseFast, // always optimize llama.cpp - }); - - o.defineCMacro("_GNU_SOURCE", null); - o.addIncludePath(.{ .path = "llama.cpp" }); - o.addCSourceFile(.{ .file = .{ .path = b.pathJoin(&.{ "llama.cpp", f }) }, .flags = if (is_cpp) cxxflags else cflags }); - if (is_cpp) o.linkLibCpp() else o.linkLibC(); - if (@hasField(@TypeOf(exe.*), "sdk")) exe.applySDK(o); - exe.addObject(o); - } - - if (exe.rootModuleTarget().os.tag == .macos) { - exe.linkFramework("Foundation"); - exe.linkFramework("Metal"); - exe.linkFramework("MetalKit"); - - // Copy the *.metal file so that it can be loaded at runtime - const copy_metal_step = b.addInstallBinFile(.{ .path = "llama.cpp/ggml-metal.metal" }, "ggml-metal.metal"); - b.getInstallStep().dependOn(©_metal_step.step); - } -} - -fn flags(b: *std.Build, exe: anytype, prefix: []const []const u8) ![]const []const u8 { - var res = std.ArrayList([]const u8).init(b.allocator); - try res.appendSlice(prefix); - try res.appendSlice(&.{ "-fPIC", "-Ofast", "-DNDEBUG", "-DGGML_USE_K_QUANTS" }); - - if (exe.rootModuleTarget().os.tag == .macos) { - try res.appendSlice(&.{ "-DGGML_USE_METAL", "-DGGML_METAL_NDEBUG" }); - } - - return res.items; -} diff --git a/build.zig.zon b/build.zig.zon deleted file mode 100644 index b7fa504..0000000 --- a/build.zig.zon +++ /dev/null @@ -1,14 +0,0 @@ -.{ - .name = "ava", - .version = "0.0.1", - - .dependencies = .{ - .@"ava-sqlite" = .{ - .url = "https://github.com/cztomsik/ava-sqlite/archive/c48ac06.tar.gz", - .hash = "122049ceedb98046f5d3362145b5f67a93d52d33a7a5357885795971be2f5bffcd45", - }, - }, - .paths = .{ - "", - }, -} diff --git a/include/ava.h b/include/ava.h deleted file mode 100644 index 8b9a84c..0000000 --- a/include/ava.h +++ /dev/null @@ -1,13 +0,0 @@ -// API for the AVA HTTP server - -#ifdef __cplusplus -extern "C" { -#endif - -int ava_start(); -int ava_stop(); -int ava_get_port(); - -#ifdef __cplusplus -} -#endif diff --git a/website/index.html b/index.html similarity index 100% rename from website/index.html rename to index.html diff --git a/llama.cpp b/llama.cpp deleted file mode 160000 index 1912211..0000000 --- a/llama.cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 191221178f51b6e81122c5bda0fd79620e547d07 diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 310ae76..0000000 --- a/package-lock.json +++ /dev/null @@ -1,566 +0,0 @@ -{ - "name": "ava", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ava", - "hasInstallScript": true, - "dependencies": { - "@preact/signals": "^1.1.5", - "@twind/core": "^1.1.3", - "@twind/preset-radix-ui": "^1.0.7", - "@twind/preset-tailwind": "^1.1.4", - "lucide": "^0.284.0", - "preact": "^10.16.0" - }, - "devDependencies": { - "esbuild": "^0.18.20", - "typescript": "^5.0.2", - "undom": "^0.4.0" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@preact/signals": { - "version": "1.1.5", - "license": "MIT", - "dependencies": { - "@preact/signals-core": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - }, - "peerDependencies": { - "preact": "10.x" - } - }, - "node_modules/@preact/signals-core": { - "version": "1.3.1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/@twind/core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@twind/core/-/core-1.1.3.tgz", - "integrity": "sha512-/B/aNFerMb2IeyjSJy3SJxqVxhrT77gBDknLMiZqXIRr4vNJqiuhx7KqUSRzDCwUmyGuogkamz+aOLzN6MeSLw==", - "funding": [ - { - "type": "Open Collective", - "url": "https://opencollective.com/twind" - }, - { - "type": "Github Sponsor", - "url": "https://github.com/sponsors/tw-in-js" - }, - { - "type": "Ko-fi", - "url": "https://ko-fi.com/twind" - } - ], - "dependencies": { - "csstype": "^3.1.1" - }, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "typescript": "^4.8.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@twind/preset-radix-ui": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@twind/preset-radix-ui/-/preset-radix-ui-1.0.7.tgz", - "integrity": "sha512-EyQtZ/PqTA4Cf5qOc8Gjy56ZwZMd5Nk+Zsm4ZRE8b7fdiMhH3FbCSXjCUeFluSgaIhdMbaVHV2AOa6Z0RBSkBQ==", - "funding": [ - { - "type": "Open Collective", - "url": "https://opencollective.com/twind" - }, - { - "type": "Github Sponsor", - "url": "https://github.com/sponsors/tw-in-js" - }, - { - "type": "Ko-fi", - "url": "https://ko-fi.com/twind" - } - ], - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "@twind/core": "^1.1.0", - "typescript": "^4.8.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@twind/preset-tailwind": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@twind/preset-tailwind/-/preset-tailwind-1.1.4.tgz", - "integrity": "sha512-zv85wrP/DW4AxgWrLfH7kyGn/KJF3K04FMLVl2AjoxZGYdCaoZDkL8ma3hzaKQ+WGgBFRubuB/Ku2Rtv/wjzVw==", - "funding": [ - { - "type": "Open Collective", - "url": "https://opencollective.com/twind" - }, - { - "type": "Github Sponsor", - "url": "https://github.com/sponsors/tw-in-js" - }, - { - "type": "Ko-fi", - "url": "https://ko-fi.com/twind" - } - ], - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "@twind/core": "^1.1.0", - "typescript": "^4.8.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/lucide": { - "version": "0.284.0", - "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.284.0.tgz", - "integrity": "sha512-wn58Gg9Vo2R5Jno32QC91muetNw5BtwY9rni5j9KNxJwvwWl2etDQng1n5hnnHfgJg8rl+FJkwCEqTEm2g152w==" - }, - "node_modules/preact": { - "version": "10.16.0", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/typescript": { - "version": "5.1.6", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undom": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/undom/-/undom-0.4.0.tgz", - "integrity": "sha512-8azrUL2oaprTIKl//+dbx9BMDt3kW1TyBfRpYdemK1zBJe63AUu6O28nzv/Fx1maJAtkwlD8a6312MqomDYFOg==", - "dev": true - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 393655b..0000000 --- a/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "private": true, - "name": "ava", - "module": "main.tsx", - "type": "module", - "scripts": { - "postinstall": "git submodule update --init --recursive", - "bundle": "esbuild ./src/app/main.tsx --bundle --outdir=./zig-out/app", - "build": "npm run check && npm test && npm run bundle -- --minify --define:DEV=false --define:NEXT=false", - "watch": "npm run bundle -- --sourcemap=inline --watch --define:DEV=true --define:NEXT=true", - "check": "tsc --noEmit", - "test": "node --loader ./src/app/_test-util/loader.js --test ./src/app/*/*.test.*" - }, - "prettier": { - "semi": false, - "arrowParens": "avoid", - "printWidth": 125 - }, - "dependencies": { - "@preact/signals": "^1.1.5", - "@twind/core": "^1.1.3", - "@twind/preset-radix-ui": "^1.0.7", - "@twind/preset-tailwind": "^1.1.4", - "lucide": "^0.284.0", - "preact": "^10.16.0" - }, - "devDependencies": { - "esbuild": "^0.18.20", - "typescript": "^5.0.2", - "undom": "^0.4.0" - }, - "overrides": { - "typescript": "^5.0.2" - } -} diff --git a/website/screenshot.png b/screenshot.png similarity index 100% rename from website/screenshot.png rename to screenshot.png diff --git a/src/api.zig b/src/api.zig deleted file mode 100644 index 21d7f0b..0000000 --- a/src/api.zig +++ /dev/null @@ -1,11 +0,0 @@ -const std = @import("std"); - -pub usingnamespace @import("api/chat.zig"); -pub usingnamespace @import("api/download.zig"); -pub usingnamespace @import("api/find-models.zig"); -pub usingnamespace @import("api/generate.zig"); -pub usingnamespace @import("api/log.zig"); -pub usingnamespace @import("api/models.zig"); -pub usingnamespace @import("api/prompts.zig"); -pub usingnamespace @import("api/proxy.zig"); -pub usingnamespace @import("api/system-info.zig"); diff --git a/src/api/chat.zig b/src/api/chat.zig deleted file mode 100644 index ba8a3da..0000000 --- a/src/api/chat.zig +++ /dev/null @@ -1,83 +0,0 @@ -const db = @import("../db.zig"); -const server = @import("../server.zig"); - -pub fn @"GET /chat"(ctx: *server.Context) !void { - var stmt = try db.query( - \\SELECT id, name, - \\(SELECT content FROM ChatMessage WHERE chat_id = Chat.id ORDER BY id DESC LIMIT 1) as last_message - \\FROM Chat ORDER BY id DESC - , .{}); - defer stmt.deinit(); - - return ctx.sendJson(stmt.iterator(struct { id: u32, name: []const u8, last_message: ?[]const u8 })); -} - -pub fn @"POST /chat"(ctx: *server.Context) !void { - const data = try ctx.readJson(struct { - name: []const u8, - prompt: ?[]const u8, - }); - - var stmt = try db.query("INSERT INTO Chat (name, prompt) VALUES (?, ?) RETURNING *", .{ data.name, data.prompt }); - defer stmt.deinit(); - - try ctx.sendJson(try stmt.read(db.Chat)); -} - -pub fn @"GET /chat/:id"(ctx: *server.Context, id: u32) !void { - var stmt = try db.query("SELECT * FROM Chat WHERE id = ?", .{id}); - defer stmt.deinit(); - - return ctx.sendJson(try stmt.read(db.Chat)); -} - -pub fn @"PUT /chat/:id"(ctx: *server.Context, id: u32, data: db.Chat) !void { - try db.exec("UPDATE Chat SET name = ?, prompt = ? WHERE id = ?", .{ data.name, data.prompt, id }); - return ctx.noContent(); -} - -pub fn @"GET /chat/:id/messages"(ctx: *server.Context, id: u32) !void { - var stmt = try db.query("SELECT * FROM ChatMessage WHERE chat_id = ? ORDER BY id", .{id}); - defer stmt.deinit(); - - return ctx.sendJson(stmt.iterator(db.ChatMessage)); -} - -pub fn @"POST /chat/:id/messages"(ctx: *server.Context, id: u32) !void { - const data = try ctx.readJson(struct { - role: []const u8, - content: []const u8, - }); - - var stmt = try db.query("INSERT INTO ChatMessage (chat_id, role, content) VALUES (?, ?, ?) RETURNING *", .{ id, data.role, data.content }); - defer stmt.deinit(); - - try ctx.sendJson(try stmt.read(db.ChatMessage)); -} - -pub fn @"GET /chat/:id/messages/:message_id"(ctx: *server.Context, id: u32, message_id: u32) !void { - var stmt = try db.query("SELECT * FROM ChatMessage WHERE id = ? AND chat_id = ?", .{ message_id, id }); - defer stmt.deinit(); - - return ctx.sendJson(try stmt.read(db.ChatMessage)); -} - -pub fn @"PUT /chat/:id/messages/:message_id"(ctx: *server.Context, id: u32, message_id: u32) !void { - const data = try ctx.readJson(struct { - role: []const u8, - content: []const u8, - }); - - try db.exec("UPDATE ChatMessage SET role = ?, content = ? WHERE id = ? AND chat_id = ?", .{ data.role, data.content, message_id, id }); - return ctx.noContent(); -} - -pub fn @"DELETE /chat/:id/messages/:message_id"(ctx: *server.Context, id: u32, message_id: u32) !void { - try db.exec("DELETE FROM ChatMessage WHERE id = ? AND chat_id = ?", .{ message_id, id }); - return ctx.noContent(); -} - -pub fn @"DELETE /chat/:id"(ctx: *server.Context, id: u32) !void { - try db.exec("DELETE FROM Chat WHERE id = ?", .{id}); - return ctx.noContent(); -} diff --git a/src/api/download.zig b/src/api/download.zig deleted file mode 100644 index a11d9ba..0000000 --- a/src/api/download.zig +++ /dev/null @@ -1,59 +0,0 @@ -const builtin = @import("builtin"); -const std = @import("std"); -const server = @import("../server.zig"); -const util = @import("../util.zig"); - -pub fn @"POST /download"(ctx: *server.Context) !void { - const url = try ctx.readJson([]const u8); - inline for (.{ "Content-Type", "Content-Length", "Host", "Referer", "Origin" }) |h| { - _ = ctx.res.request.headers.delete(h); - } - - var client: std.http.Client = .{ .allocator = ctx.arena }; - defer client.deinit(); - - if (builtin.target.os.tag == .windows) { - try client.ca_bundle.rescan(ctx.arena); - const start = client.ca_bundle.bytes.items.len; - try client.ca_bundle.bytes.appendSlice(ctx.arena, @embedFile("../windows/amazon1.cer")); - try client.ca_bundle.parseCert(ctx.arena, @intCast(start), std.time.timestamp()); - } - - var req = try client.open(.GET, try std.Uri.parse(url), ctx.res.request.headers, .{}); - defer req.deinit(); - - try req.send(.{}); - try req.wait(); - - if (req.response.status != .ok) { - return ctx.sendJson(.{ .@"error" = try std.fmt.allocPrint(ctx.arena, "Invalid status code: `{d}`", .{req.response.status}) }); - } - - const content_type = req.response.headers.getFirstValue("Content-Type") orelse ""; - if (!std.mem.eql(u8, content_type, "binary/octet-stream")) { - return ctx.sendJson(.{ .@"error" = try std.fmt.allocPrint(ctx.arena, "Invalid content type: `{s}`", .{content_type}) }); - } - - const path = try util.getWritableHomePath(ctx.arena, &.{ "models", std.fs.path.basename(url) }); - const tmp_path = try std.fmt.allocPrint(ctx.arena, "{s}.part", .{path}); - var file = try std.fs.createFileAbsolute(tmp_path, .{}); - defer file.close(); - errdefer std.fs.deleteFileAbsolute(tmp_path) catch {}; - - var reader = req.reader(); - var writer = file.writer(); - - // connection buffer seems to be 80KB so let's do two reads per write - var buf: [160 * 1024]u8 = undefined; - var progress: usize = 0; - while (reader.readAll(&buf)) |n| { - try writer.writeAll(buf[0..n]); - if (n < buf.len) break; - - progress += n; - try ctx.sendJson(.{ .progress = progress }); - } else |_| return ctx.sendJson(.{ .@"error" = "Failed to download the model" }); - - try std.fs.renameAbsolute(tmp_path, path); - try ctx.sendJson(.{ .path = path }); -} diff --git a/src/api/find-models.zig b/src/api/find-models.zig deleted file mode 100644 index a15d4ed..0000000 --- a/src/api/find-models.zig +++ /dev/null @@ -1,30 +0,0 @@ -const std = @import("std"); -const server = @import("../server.zig"); - -pub fn @"POST /find-models"(ctx: *server.Context) !void { - var models_found = std.ArrayList(struct { path: []const u8, size: ?u64 }).init(ctx.arena); - - const path = try ctx.readJson([]const u8); - - var dir = try std.fs.openDirAbsolute(path, .{ .iterate = true }); - defer dir.close(); - - var walker = try dir.walk(ctx.arena); - defer walker.deinit(); - - while (try walker.next()) |entry| switch (entry.kind) { - .file => if (std.mem.endsWith(u8, entry.basename, ".gguf")) { - const file = try dir.openFile(entry.path, .{ .mode = .read_only }); - defer file.close(); - - try models_found.append(.{ - .path = try std.fs.path.join(ctx.arena, &.{ path, entry.path }), - .size = (try file.stat()).size, - }); - }, - .directory => _ = if (walker.stack.items.len > 3) walker.stack.pop(), - else => {}, - }; - - return ctx.sendJson(models_found.items); -} diff --git a/src/api/generate.zig b/src/api/generate.zig deleted file mode 100644 index 2b642a2..0000000 --- a/src/api/generate.zig +++ /dev/null @@ -1,41 +0,0 @@ -const std = @import("std"); -const server = @import("../server.zig"); -const db = @import("../db.zig"); -const llama = @import("../llama.zig"); - -const GenerateParams = struct { - model_id: u32, - prompt: []const u8, - max_tokens: u32 = 2048, - trim_first: bool = false, - sampling: llama.SamplingParams = .{}, -}; - -pub fn @"POST /generate"(ctx: *server.Context, params: GenerateParams) !void { - try ctx.sendJson(.{ .status = "Waiting for the model..." }); - const model_path = try db.getString(ctx.arena, "SELECT path FROM Model WHERE id = ?", .{params.model_id}); - var cx = try llama.Pool.get(model_path, 60_000); - defer llama.Pool.release(cx); - - try ctx.sendJson(.{ .status = "Reading the history..." }); - try cx.prepare(params.prompt, ¶ms.sampling); - - while (cx.n_past < cx.tokens.items.len) { - try ctx.sendJson(.{ .status = try std.fmt.allocPrint(ctx.arena, "Reading the history... ({}/{})", .{ cx.n_past, cx.tokens.items.len }) }); - _ = try cx.evalOnce(); - } - - // TODO: send enums/unions - try ctx.sendJson(.{ .status = "" }); - - var tokens: u32 = 0; - - while (try cx.generate(¶ms.sampling)) |content| { - try ctx.sendJson(.{ - .content = if (tokens == 0 and params.trim_first) std.mem.trimLeft(u8, content, " \t\n\r") else content, - }); - - tokens += 1; - if (tokens >= params.max_tokens) break; - } -} diff --git a/src/api/log.zig b/src/api/log.zig deleted file mode 100644 index 82b87c5..0000000 --- a/src/api/log.zig +++ /dev/null @@ -1,6 +0,0 @@ -const server = @import("../server.zig"); -const util = @import("../util.zig"); - -pub fn @"GET /log"(ctx: *server.Context) !void { - try ctx.sendChunk(try util.Logger.dump(ctx.arena)); -} diff --git a/src/api/models.zig b/src/api/models.zig deleted file mode 100644 index a34731e..0000000 --- a/src/api/models.zig +++ /dev/null @@ -1,49 +0,0 @@ -const std = @import("std"); -const db = @import("../db.zig"); -const server = @import("../server.zig"); -const util = @import("../util.zig"); - -pub fn @"GET /models"(ctx: *server.Context) !void { - var stmt = try db.query("SELECT * FROM Model ORDER BY id", .{}); - defer stmt.deinit(); - - var rows = std.ArrayList(struct { id: u32, name: []const u8, path: []const u8, imported: bool, size: ?u64 }).init(ctx.arena); - var it = stmt.iterator(db.Model); - while (try it.next()) |m| { - try rows.append(.{ - .id = m.id, - .name = try ctx.arena.dupe(u8, m.name), - .path = try ctx.arena.dupe(u8, m.path), - .imported = m.imported, - .size = util.getFileSize(m.path) catch null, - }); - } - - return ctx.sendJson(rows.items); -} - -pub fn @"POST /models"(ctx: *server.Context) !void { - const data = try ctx.readJson(struct { - name: []const u8, - path: []const u8, - imported: bool = false, - }); - - var stmt = try db.query("INSERT INTO Model (name, path, imported) VALUES (?, ?, ?) RETURNING *", .{ data.name, data.path, data.imported }); - defer stmt.deinit(); - - try ctx.sendJson(try stmt.read(db.Model)); -} - -pub fn @"PUT /models/:id"(ctx: *server.Context, id: u32, data: db.Model) !void { - try db.exec("UPDATE Model SET name = ?, path = ? WHERE id = ?", .{ data.name, data.path, id }); - return ctx.noContent(); -} - -pub fn @"DELETE /models/:id"(ctx: *server.Context, id: []const u8) !void { - const path = try db.getString(ctx.arena, "SELECT path FROM Model WHERE id = ?", .{id}); - const imported = try db.get(bool, "SELECT imported FROM Model WHERE id = ?", .{id}); - try db.exec("DELETE FROM Model WHERE id = ?", .{id}); - if (!imported) std.fs.deleteFileAbsolute(path) catch {}; - return ctx.noContent(); -} diff --git a/src/api/prompts.zig b/src/api/prompts.zig deleted file mode 100644 index 6cd6c00..0000000 --- a/src/api/prompts.zig +++ /dev/null @@ -1,26 +0,0 @@ -const db = @import("../db.zig"); -const server = @import("../server.zig"); - -pub fn @"GET /prompts"(ctx: *server.Context) !void { - var stmt = try db.query("SELECT * FROM Prompt ORDER BY id", .{}); - defer stmt.deinit(); - - return ctx.sendJson(stmt.iterator(db.Prompt)); -} - -pub fn @"POST /prompts"(ctx: *server.Context) !void { - const data = try ctx.readJson(struct { - name: []const u8, - prompt: []const u8, - }); - - var stmt = try db.query("INSERT INTO Prompt (name, prompt) VALUES (?, ?) RETURNING *", .{ data.name, data.prompt }); - defer stmt.deinit(); - - try ctx.sendJson(try stmt.read(db.Prompt)); -} - -pub fn @"DELETE /prompts/:id"(ctx: *server.Context, id: u32) !void { - try db.exec("DELETE FROM Prompt WHERE id = ?", .{id}); - return ctx.noContent(); -} diff --git a/src/api/proxy.zig b/src/api/proxy.zig deleted file mode 100644 index e51c92f..0000000 --- a/src/api/proxy.zig +++ /dev/null @@ -1,40 +0,0 @@ -const std = @import("std"); -const server = @import("../server.zig"); - -pub fn @"POST /proxy"(ctx: *server.Context) !void { - const url = try ctx.readJson([]const u8); - - inline for (.{ "Accept-Encoding", "Content-Type", "Content-Length", "Host", "Referer", "Origin" }) |h| { - _ = ctx.res.request.headers.delete(h); - } - try ctx.res.request.headers.append("Accept-Encoding", "gzip, deflate"); - - var client: std.http.Client = .{ .allocator = ctx.arena }; - defer client.deinit(); - - var req = try client.open(.GET, try std.Uri.parse(url), ctx.res.request.headers, .{}); - defer req.deinit(); - - try req.send(.{}); - try req.wait(); - - ctx.res.status = req.response.status; - ctx.res.headers = try req.response.headers.clone(ctx.arena); - ctx.res.transfer_encoding = .chunked; - _ = ctx.res.headers.delete("Transfer-Encoding"); - _ = ctx.res.headers.delete("Content-Encoding"); - _ = ctx.res.headers.delete("Content-Length"); - _ = ctx.res.headers.delete("Link"); // Otherwise browser will try to preload non-existent resources - try ctx.res.send(); - - var buf: [512]u8 = undefined; - var written: usize = 0; - - while (true) { - if (written == req.response.content_length) break; - const n = try req.read(buf[0..]); - if (n == 0) break; - try ctx.res.writeAll(buf[0..n]); - written += n; - } -} diff --git a/src/api/system-info.zig b/src/api/system-info.zig deleted file mode 100644 index cbc8b54..0000000 --- a/src/api/system-info.zig +++ /dev/null @@ -1,47 +0,0 @@ -const builtin = @import("builtin"); -const std = @import("std"); -const server = @import("../server.zig"); - -pub fn @"GET /system-info"(ctx: *server.Context) !void { - const user_home = try std.process.getEnvVarOwned(ctx.arena, if (builtin.target.os.tag == .windows) "USERPROFILE" else "HOME"); - const user_downloads = try std.fs.path.join(ctx.arena, &.{ user_home, "Downloads" }); - - return ctx.sendJson(.{ - .os = builtin.os.tag, - .os_version = try getOsVersion(ctx.arena), - .arch = builtin.cpu.arch, - .cpu_count = std.Thread.getCpuCount() catch 0, - .total_system_memory = std.process.totalSystemMemory() catch 0, - .user_home = user_home, - .user_downloads = user_downloads, - }); -} - -fn getOsVersion(allocator: std.mem.Allocator) ![]const u8 { - if (comptime builtin.os.tag == .macos) { - var f = try std.fs.openFileAbsolute("/System/Library/CoreServices/.SystemVersionPlatform.plist", .{}); - defer f.close(); - - var contents = try f.readToEndAlloc(allocator, 1024); - defer allocator.free(contents); - - if (std.mem.indexOf(u8, contents, "ProductVersion")) |a| { - if (std.mem.indexOf(u8, contents[a..], "")) |b| { - if (std.mem.indexOf(u8, contents[a + b ..], "")) |c| { - return allocator.dupe(u8, contents[a + b + 8 .. a + b + c]); - } - } - } - } - - if (comptime builtin.os.tag == .windows) { - var info: std.os.windows.RTL_OSVERSIONINFOW = undefined; - info.dwOSVersionInfoSize = @sizeOf(@TypeOf(info)); - - if (std.os.windows.ntdll.RtlGetVersion(&info) == .SUCCESS) { - return std.fmt.allocPrint(allocator, "{d}.{d}", .{ info.dwMajorVersion, info.dwMinorVersion }); - } - } - - return "unknown"; -} diff --git a/src/app/App.tsx b/src/app/App.tsx deleted file mode 100644 index 4cd2ed2..0000000 --- a/src/app/App.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ErrorBoundary, ModalBackdrop } from "./_components" -import { Sidebar } from "./Sidebar" -import { router } from "./router" - -const gridTemplate = ` - "sidebar list header header" auto - "sidebar list main details" 1fr - "sidebar list footer footer" auto / min-content min-content 1fr min-content -` - -export const App = () => { - return ( - -
- - -
- - -
- ) -} diff --git a/src/app/Sidebar.tsx b/src/app/Sidebar.tsx deleted file mode 100644 index 9ec05c0..0000000 --- a/src/app/Sidebar.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { ModelSelect, NavLink, Resizable, SearchField } from "./_components" - -export const Sidebar = () => { - return ( - - {false ? :
} - - - Chat - Quick Tools - {NEXT && Workflows} - Playground - - - Settings - - - Twitter - Discord - - - - - ) -} - -const SidebarHeader = ({ title, class: className = "" }) => ( -

{title}

-) - -const SidebarLink = props => ( - -) diff --git a/src/app/_components/Alert.tsx b/src/app/_components/Alert.tsx deleted file mode 100644 index c613c34..0000000 --- a/src/app/_components/Alert.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const Alert = ({ class: className = "", children }) => { - return ( - - ) -} diff --git a/src/app/_components/AutoGrowTextarea.tsx b/src/app/_components/AutoGrowTextarea.tsx deleted file mode 100644 index 294ec3a..0000000 --- a/src/app/_components/AutoGrowTextarea.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export const AutoGrowTextarea = ({ class: className = "", ...props }) => ( -
- -
- -
- {variableNames.map(name => ( -
-
{name}
- (data.value = { ...data.value, [name]: e.target!.value })} - /> -
- ))} - -
- (showPrompt.value = v)} /> -
- -
- - - -
-
- - - - ) -} - -const PromptSelect = ({ value, onChange }) => { - const { data, loading } = useApi("prompts") - - const handleChange = e => { - const id = +e.target.value - const arr = id > 0 ? data : examples - - onChange(arr?.find(p => p.id === id)) - } - - return ( - - ) -} - -const examples = [ - { - id: -1, - name: "Story Writing", - prompt: dedent` - The following is an excerpt from a story about Bob, the cat, flying to space. - `, - }, - - { - id: -2, - name: "Top 10 Movies To Watch", - prompt: dedent` - The top 10 movies to watch in 2025 are: - `, - }, - - { - id: -3, - name: "Question Answering", - prompt: dedent` - Q: What is the capital of France? - A: Paris. - Q: {{question}} - A: - `, - }, - - // TODO: Writing technical documentation, product announcement, etc. - { - id: -4, - name: "Copywriting", - prompt: dedent` - User needs help with copywriting. - - ASSISTANT: Hello. - USER: Write a blog post{{#subject}} about {{subject}}{{/subject}}. - ASSISTANT: Ok, here is a draft of the post. - `, - }, -] diff --git a/src/app/playground/SettingsModal.tsx b/src/app/playground/SettingsModal.tsx deleted file mode 100644 index 2029245..0000000 --- a/src/app/playground/SettingsModal.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Button, Checkbox, Field, Form, FormGrid, Modal, Range } from "../_components" - -export const SettingsModal = ({ sampleOptions, onClose, onSave }) => { - return ( - -
- - - - - - - - - - - - (sampleOptions.add_bos = v)} - /> - (sampleOptions.stop_eos = v)} - /> - (sampleOptions.json = v)} /> - -
- - -
-
-
- ) -} diff --git a/src/app/quick-tools/CreateTool.tsx b/src/app/quick-tools/CreateTool.tsx deleted file mode 100644 index f43699c..0000000 --- a/src/app/quick-tools/CreateTool.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Page } from "../_components" -import { router } from "../router" -import { ToolForm } from "./ToolForm" -import { examples } from "./_examples" - -export const CreateTool = () => { - const tool = { - name: "New Tool", - prompt: "", - } - - const createTool = async (tool: any) => { - const id = examples.length + 1 - examples.push({ id, ...tool }) - router.navigate(`/quick-tools/${id}`) - } - - return ( - - - - - - - - ) -} diff --git a/src/app/quick-tools/EditTool.tsx b/src/app/quick-tools/EditTool.tsx deleted file mode 100644 index ba9b683..0000000 --- a/src/app/quick-tools/EditTool.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Page } from "../_components" -import { ToolForm } from "./ToolForm" -import { err } from "../_util" -import { examples } from "./_examples" -import { router } from "../router" - -export const EditTool = ({ params: { id } }) => { - const tool = examples.find(t => t.id === +id) ?? err("Tool not found") - - const handleSubmit = data => { - Object.assign(tool, data) - router.navigate(`/quick-tools/${id}`) - } - - return ( - - - - - - - - ) -} diff --git a/src/app/quick-tools/QuickTool.tsx b/src/app/quick-tools/QuickTool.tsx deleted file mode 100644 index 39190fc..0000000 --- a/src/app/quick-tools/QuickTool.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { PenSquare } from "lucide" -import { AutoScroll, Button, Form, FormGrid, GenerationProgress, IconButton, Markdown, Page, Field } from "../_components" -import { useGenerate } from "../_hooks" -import { err, parseVars, template, humanize } from "../_util" -import { examples } from "./_examples" - -export const QuickTool = ({ params: { id } }) => { - const { generate, result, ...progress } = useGenerate() - - const tool = examples.find(t => t.id === +id) ?? err("Tool not found") - const variableNames = parseVars(tool.prompt) - - const handleSubmit = data => generate({ prompt: template(tool.prompt, data) }) - - return ( - <> - - - - - -
-

{tool.description}

- - - {variableNames.map(name => ( - <> - - {name.endsWith("text") ? : } - - ))} - -
- - - - -
-

Result

-
- {result.value === "" &&

Click generate to see the result

} - - - - -
-
- - - ) -} diff --git a/src/app/quick-tools/QuickTools.tsx b/src/app/quick-tools/QuickTools.tsx deleted file mode 100644 index 051a566..0000000 --- a/src/app/quick-tools/QuickTools.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Plus } from "lucide" -import { Alert, Button, IconButton, Link, Page, Table } from "../_components" -import { examples } from "./_examples" - -export const QuickTools = () => { - return ( - - - - - - - - This feature is experimental.
- No changes are saved to the database -
- - - - - - - - - - - {examples.map(t => ( - - - - - - ))} - -
NameDescription
- - {t.name} - - {t.description} - -
-
-
- ) -} diff --git a/src/app/quick-tools/ToolForm.tsx b/src/app/quick-tools/ToolForm.tsx deleted file mode 100644 index 8edb10f..0000000 --- a/src/app/quick-tools/ToolForm.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Button, Form, FormGrid, Field } from "../_components" - -export const ToolForm = ({ tool, onSubmit }) => { - return ( -
- - - - - - - - - - -
-
- - -
- - - ) -} diff --git a/src/app/quick-tools/_examples.ts b/src/app/quick-tools/_examples.ts deleted file mode 100644 index 4c26c58..0000000 --- a/src/app/quick-tools/_examples.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { dedent } from "../_util" - -export const examples = [ - { - id: 1, - name: "Fix Grammar", - description: "Correct grammar in the provided text", - prompt: dedent` - USER: Correct grammar in the following text. Do not explain. - \`\`\` - {{text}} - \`\`\`{{#extra}} - {{extra}}{{/extra}} - ASSISTANT: Sure! Here's the corrected version without any explanation: - `, - }, - - { - id: 2, - name: "Summarize", - description: "Summarize the provided text", - prompt: dedent` - USER: Summarize the following text. Do not explain. - \`\`\` - {{text}} - \`\`\`{{#extra}} - {{extra}}{{/extra}} - ASSISTANT: Sure! Here's the summary without any explanation: - `, - }, - - { - id: 3, - name: "Vacation Planner", - description: "Plan a vacation", - prompt: dedent` - USER: Help me plan a vacation, please. - {{#destination}}I want to go to {{destination}}.{{/destination}} - {{#days}}I want to go there for {{days}} days.{{/days}} - {{#month}}I want to go there in {{month}}.{{/month}} - {{#people}}There will be {{people}} people.{{/people}} - {{#budget}}My budget is {{budget}} dollars.{{/budget}} - {{#interests}}I am interested in {{interests}}.{{/interests}} - ASSISTANT: Sure! Here is the plan for your vacation: - `, - }, - - { - id: 4, - name: "Suggest a Present", - description: "Suggest a present for someone", - prompt: dedent` - USER: I need a present for someone. I want to spend {{budget}} dollars. - {{#interests}}They are interested in {{interests}}.{{/interests}} - {{#age}}They are {{age}} years old.{{/age}} - {{#extra}}{{extra}}{{/extra}} - ASSISTANT: Here is the present I suggest: - `, - }, - - { - id: 5, - name: "Quick Dinner", - description: "Suggest a recipe for a quick dinner", - prompt: dedent` - USER: I need a recipe for a quick dinner{{#people}} for {{people}} people{{/people}}. - {{#cuisine}}I want {{cuisine}} cuisine.{{/cuisine}} - {{#ingredients}}I have {{ingredients}}.{{/ingredients}} - {{#extra}}{{extra}}{{/extra}} - ASSISTANT: Sure! Here is the recipe: - `, - }, -] as any - -// TODO: googlefu - -// { -// id: -2, -// name: "Movies To Watch", -// prompt: dedent` -// A chat with an assistant about movies to watch. -// USER: I need a list of movies to watch. I like {{genre}} movies. I like {{actor}} movies. -// ASSISTANT: Here is the list of movies you should watch: -// `, -// }, - -// if (NEXT) { -// examples.push( -// { -// id: -5, -// name: "Rephrase", -// description: "", -// prompt: dedent` -// USER: Rephrase the following text so it looks more professional: -// {{text}} -// ASSISTANT: -// `, -// }, - -// { -// id: -7, -// name: "Writing Style", -// description: "Suggest style improvements", -// prompt: dedent` -// USER: Given the following text, suggest improvements to the style: -// {{text}} -// ASSISTANT: The following improvements can be made: -// `, -// }, - -// { -// id: -8, -// name: "Interview Prep", -// description: "Prepare for an interview", -// prompt: dedent` -// USER: I need to prepare for an interview for a {{position}} position at {{company}}. The company industry is {{industry}}. -// ASSISTANT: Here are the questions you should prepare for: -// `, -// }, - -// { -// id: -9, -// name: "Write an E-mail", -// description: "Write e-mail", -// prompt: dedent` -// USER: I need to write an e-mail to {{name}} about {{about}} because {{reason}}\nASSISTANT: -// `, -// }, - -// { -// id: -10, -// name: "Write a Reply", -// description: "Write e-mail reply", -// prompt: dedent` -// USER: I got this email from {{name}}: -// {{email}} -// Rephrase this reply in a more polite way: -// {{reply}} -// ASSISTANT: I think you could write something like this: -// `, -// }, - -// { -// id: -12, -// name: "Buy or Sell", -// description: "Stock market advice", -// prompt: dedent` -// USER: Given the following text about a stock, should I buy or sell? -// {{text}} -// ASSISTANT: Based on the given text, it is difficult to provide a definitive answer. However, given that investor's risk tolerance is high, I would suggest -// `, -// }, - -// { -// id: -13, -// name: "Regex Help", -// description: "Write a regular expression", -// prompt: dedent` -// USER: I need a help writing regular expression in JavaScript. Specifically, I want to write regex for {{whatToMatch}}... - -// For example, the following should match: -// {{example1}} -// {{example2}} - -// Only answer with regex and unit-tests. - -// ASSISTANT: You can use the following regular expression: -// `, -// }, - -// { -// id: -14, -// name: "Total Rewrite", -// description: "Reimplement given code in another language", -// prompt: dedent` -// USER: I need to reimplement the following code in {{language}}: -// {{code}} -// ASSISTANT: Here is the code in {{language}}: -// `, -// } -// ) -// } diff --git a/src/app/router.ts b/src/app/router.ts deleted file mode 100644 index add224e..0000000 --- a/src/app/router.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { signal } from "@preact/signals" -import { Chat } from "./chat/Chat" -import { Playground } from "./playground/Playground" -import { QuickTools } from "./quick-tools/QuickTools" -import { CreateTool } from "./quick-tools/CreateTool" -import { QuickTool } from "./quick-tools/QuickTool" -import { EditTool } from "./quick-tools/EditTool" -import { Workflows } from "./workflow/Workflows" -import { Workflow } from "./workflow/Workflow" -import { Models } from "./settings/Models" -import { System } from "./settings/System" -import { Api } from "./settings/Api" -import { License } from "./settings/License" - -const routes = [ - { path: "/chat", component: Chat }, - { path: "/chat/:id", component: Chat }, - { path: "/quick-tools", component: QuickTools }, - { path: "/quick-tools/new", component: CreateTool }, - { path: "/quick-tools/:id", component: QuickTool }, - { path: "/quick-tools/:id/edit", component: EditTool }, - NEXT && { path: "/workflows", component: Workflows }, - NEXT && { path: "/workflows/:id", component: Workflow }, - { path: "/playground", component: Playground }, - { path: "/settings", component: Models }, - { path: "/settings/system", component: System }, - { path: "/settings/license", component: License }, - { path: "/settings/api", component: Api }, -].filter(Boolean) as Array<{ path; component }> - -const current = signal({ route: routes[0], params: {} as any }) - -const patternCache: Record = {} - -export const router = { - routes, - - navigate(path: string, replace = false) { - history[replace ? "replaceState" : "pushState"]({}, "", path) - this.onChange() - }, - - match(pattern: string) { - const regex = - patternCache[pattern] || - (patternCache[pattern] = new RegExp(`^${pattern.replace(/:(\w+)/g, "(?<$1>[^/]+)").replace(/\*/g, "(?.*)")}$`)) - return location.pathname.match(regex) - }, - - onChange() { - for (const r of routes) { - const match = this.match(r.path) - if (match) { - current.value = { route: r, params: match.groups ?? {} } - return - } - } - - this.navigate(routes[0].path, true) - }, - - get currentRoute() { - return current.value.route - }, - - get params() { - return current.value.params - }, -} - -addEventListener("popstate", () => router.onChange()) -router.onChange() diff --git a/src/app/settings/Api.tsx b/src/app/settings/Api.tsx deleted file mode 100644 index 62097b7..0000000 --- a/src/app/settings/Api.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Alert, Markdown } from "../_components" -import { API_URL } from "../_hooks" -import { SettingsPage } from "./SettingsPage" - -export const Api = () => { - return ( - - - This feature is experimental.
- The API is not yet stable and may change in the future. -
- - -
- ) -} diff --git a/src/app/settings/DownloadModal.tsx b/src/app/settings/DownloadModal.tsx deleted file mode 100644 index e206b52..0000000 --- a/src/app/settings/DownloadModal.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Button, Modal } from "../_components" -import { basename, humanSize } from "../_util" - -export const DownloadModal = ({ url, size, progress, onCancel }) => { - const percent = (progress.value / size) * 100 - - return ( - -
- Downloading {basename(url)} - {percent.toFixed(2)}% -
- -
-
-
- -
- - {humanSize(progress.value)} / {humanSize(size)} - - -
-
- ) -} diff --git a/src/app/settings/License.tsx b/src/app/settings/License.tsx deleted file mode 100644 index 4b5ecf9..0000000 --- a/src/app/settings/License.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useSignal } from "@preact/signals" -import { SettingsPage } from "./SettingsPage" -import { useEffect } from "preact/hooks" -import { Markdown } from "../_components" - -export const License = () => { - const license = useSignal("Loading...") - - useEffect(() => { - fetch("/LICENSE.md") - .then(r => r.text()) - .then(text => (license.value = text)) - }, []) - - return ( - -

License

-
- -
- -

Open Source acknowledgement

-

- This Software, incorporates and uses open-source software components. The use of these components is - acknowledged, and their respective licenses are included in the LICENSES.md file which is shown above. -

-
- ) -} diff --git a/src/app/settings/ModelCatalog.tsx b/src/app/settings/ModelCatalog.tsx deleted file mode 100644 index 4a9ee9b..0000000 --- a/src/app/settings/ModelCatalog.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Button, Table } from "../_components" -import { useApi } from "../_hooks" -import { basename, humanSize } from "../_util" -import { downloadModel } from "./download" - -export const ModelCatalog = () => { - const { data: models = [] } = useApi("models") - - return ( - <> -

Catalog

- - - - - - - - - - - {catalog.map(m => ( - - - - - - - ))} - -
ModelUploaderSize
{basename(m.url.slice(0, -5))}{m.url.split("/")[3]}{humanSize(m.size)} - {models.find(({ name }) => name === basename(m.url).slice(0, -5)) ? ( - Installed - ) : ( - - )} -
- -

- - Different models may have different licenses. Always check the license of each model before using it. - -

- - ) -} - -const catalog = [ - { - url: "https://huggingface.co/TheBloke/WizardLM-13B-V1.2-GGUF/resolve/main/wizardlm-13b-v1.2.Q4_K_M.gguf", - size: 7865956224, - }, - { - url: "https://huggingface.co/TheBloke/MythoMax-L2-13B-GGUF/resolve/main/mythomax-l2-13b.Q4_K_M.gguf", - size: 7865956224, - }, - { - url: "https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/resolve/main/zephyr-7b-beta.Q4_K_M.gguf", - size: 4368438976, - }, - { - url: "https://huggingface.co/TheBloke/Airoboros-L2-7B-2.2-GGUF/resolve/main/airoboros-l2-7b-2.2.Q4_K_M.gguf", - size: 4081004256, - }, - { - url: "https://huggingface.co/TheBloke/rocket-3B-GGUF/resolve/main/rocket-3b.Q4_K_M.gguf", - size: 1708595136, - }, -] diff --git a/src/app/settings/ModelImporter.tsx b/src/app/settings/ModelImporter.tsx deleted file mode 100644 index eb18d28..0000000 --- a/src/app/settings/ModelImporter.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Alert, Button } from "../_components" -import { useApi, useLocalStorage } from "../_hooks" -import { useEffect } from "preact/hooks" -import { basename } from "../_util" - -export const ModelImporter = () => { - const { data: system } = useApi("system-info") - const { data: models, post } = useApi("models") - const path = useLocalStorage("importer.path", "") - - useEffect(() => { - if (system && !path.value) path.value = system.user_downloads - }, [system]) - - const handleImport = async () => { - // TODO: use getApiContext() but make sure the invalidate() does not do GET afterwards - const res = await fetch("/api/find-models", { method: "POST", body: JSON.stringify(path.value) }) - const data = await res.json() - - // Import only models that are not already there - for (const { path } of data) { - if (!models?.find((m: any) => m.path === path)) { - await post({ name: basename(path), path, imported: true }) - } - } - } - - return ( - - Import models -

If you already have some GGUF files, you can import them here:

-
- (path.value = e.target!.value)} /> - -
-
- ) -} diff --git a/src/app/settings/Models.tsx b/src/app/settings/Models.tsx deleted file mode 100644 index 96fb0ce..0000000 --- a/src/app/settings/Models.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Button, Table } from "../_components" -import { SettingsPage } from "./SettingsPage" -import { DownloadModal } from "./DownloadModal" -import { useApi, useConfirm } from "../_hooks" -import { humanSize } from "../_util" -import { abortCurrent, current } from "./download" -import { ModelImporter } from "./ModelImporter" -import { ModelCatalog } from "./ModelCatalog" - -export const Models = () => { - const { data: models = [], del } = useApi("models") - - const handleDelete = useConfirm("Are you sure you want to delete this model?", del) - - return ( - - {current.value && } - - - -

Installed models

- - - - - - - - - - {!models.length && ( - - - - )} - - {models.map(m => ( - - - - - - ))} - -
ModelSize
No models installed. Download one from the catalog below.
{m.name}{humanSize(m.size)} - {m.imported ? ( - - ) : ( - - )} -
- - -
- ) -} diff --git a/src/app/settings/SettingsPage.tsx b/src/app/settings/SettingsPage.tsx deleted file mode 100644 index 440fb6c..0000000 --- a/src/app/settings/SettingsPage.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { NavLink, Page, Tabs } from "../_components" - -export const SettingsPage = ({ children }) => { - return ( - - - - - Models - - System - API - License - - - - {children} - - ) -} diff --git a/src/app/settings/System.tsx b/src/app/settings/System.tsx deleted file mode 100644 index a71ac94..0000000 --- a/src/app/settings/System.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Value } from "../_components" -import { useApi } from "../_hooks" -import { SettingsPage } from "./SettingsPage" - -export const System = () => { - const { data: system } = useApi("system-info") - const { data: log } = useApi("log") - - return ( - -

System info

- -
- - - - -
- -

Log

-
{log}
-
- ) -} diff --git a/src/app/settings/download.ts b/src/app/settings/download.ts deleted file mode 100644 index f7e2b27..0000000 --- a/src/app/settings/download.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { effect, signal, Signal } from "@preact/signals" -import { getApiContext } from "../_hooks" -import { basename, jsonLines } from "../_util" - -type DownloadItem = { url: string; size: number } - -export const queue = signal([]) -export const current = signal<{ url: string; size: number; progress: Signal; ctrl: AbortController } | null>(null) -export const downloadModel = (item: DownloadItem) => (queue.value = [...queue.value, item]) - -const startDownload = async ({ url, size }) => { - const ctrl = new AbortController() - const progress = signal(0) - - current.value = { - url, - size, - progress, - ctrl, - } - - try { - const res = await fetch("/api/download", { - method: "POST", - body: JSON.stringify(url), - signal: ctrl.signal, - }) - - for await (const d of jsonLines(res.body!.getReader())) { - if ("error" in d) { - throw new Error(`Unexpected error: ${d.error}`) - } - - if ("progress" in d) { - progress.value = d.progress - } - - if ("path" in d) { - await getApiContext("models").post({ name: basename(url.slice(0, -5)), path: d.path }) - } - } - } catch (e) { - if (e.code !== DOMException.ABORT_ERR) { - throw e - } - } finally { - current.value = null - } -} - -export const abortCurrent = () => { - current.value?.ctrl.abort() - current.value = null -} - -effect(() => { - if (!current.value && queue.value.length) { - const [next, ...rest] = queue.value - queue.value = rest - startDownload(next) - } -}) diff --git a/src/app/theme.tsx b/src/app/theme.tsx deleted file mode 100644 index dd088f9..0000000 --- a/src/app/theme.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { options } from "preact" -import { tw, install, css, TwindUserConfig } from "@twind/core" -import presetTailwind, { TailwindTheme } from "@twind/preset-tailwind/base" -import * as radix from "@twind/preset-radix-ui/colors" -import darkColor from "@twind/preset-radix-ui/darkColor" - -const colors = { - ...radix, - primary: radix.blue, - primaryDark: radix.blueDark, - warning: radix.yellow, - warningDark: radix.yellowDark, - neutral: radix.sand, - neutralDark: radix.slateDark, -} - -const autoprefix = ({ stringify }) => ({ - stringify: (prop, value, ctx) => { - const prefix = CSS.supports(`${prop}: unset`) ? "" : "-webkit-" - return prefix + stringify(prop, value, ctx) - }, -}) - -const globals = () => ({ - preflight: css` - input:not([type="checkbox"]):not([type="range"]), - select, - textarea, - .form-control { - @apply inline-block appearance-none text(ellipsis neutral-12) px-2 py-[5px] bg-neutral-1 border(1 neutral-8) rounded-md resize-none; - } - - :focus { - @apply !border-transparent outline(& [3px] primary-7); - } - - th { - text-align: left; - } - `, -}) - -const macos = () => ({ - preflight: - "webkit" in window - ? css` - html.oof #app { - opacity: 0.7; - filter: grayscale(0.85) contrast(0.9); - } - - *:not([draggable]), - a, - button { - cursor: default; - user-select: none; - user-drag: none; - } - - a, - button { - white-space: nowrap; - } - ` - : "", -}) - -const cfg: TwindUserConfig = { - presets: [autoprefix, presetTailwind({ colors }), globals, macos], - darkColor, - rules: [ - // mini-bootstrap - ["hstack", "flex(& row) items-center"], - ["vstack", "flex(& col)"], - - // custom - ["shadow-thin", { boxShadow: "0 1px 1px 0 rgba(0, 0, 0, 0.3)" }], - ], - variants: [ - // out-of-focus (TODO: maybe opacity is enough?) - // or maybe just define few color classes and override them in dark mode - // ["oof", "&:is(.oof *)"], - ], - theme: { - fontFamily: { - sans: `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`, - }, - - fontSize: { - xs: ["12px", "1rem"], - sm: ["13px", "1rem"], - base: ["14px", "1.25rem"], - lg: ["16px", "1.5rem"], - xl: ["18px", "1.5rem"], - "2xl": ["20px", "1.5rem"], - }, - }, -} - -install(cfg, false) - -options.vnode = vnode => { - if ("class" in vnode.props) vnode.props.class = tw(vnode.props.class as any) -} - -addEventListener("blur", () => document.documentElement.classList.add("oof")) -addEventListener("focus", () => document.documentElement.classList.remove("oof")) diff --git a/src/app/workflow/Workflow.tsx b/src/app/workflow/Workflow.tsx deleted file mode 100644 index 3098128..0000000 --- a/src/app/workflow/Workflow.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { createContext } from "preact" -import { useContext } from "preact/hooks" -import { useSignal } from "@preact/signals" -import { Clock, Cloud, MousePointerSquare, Play, Repeat2, TableProperties, Wand2 } from "lucide" -import { Field, Form, FormGrid, Icon, IconButton, Page } from "../_components" -import { err, humanDuration, humanize } from "../_util" -import { examples } from "./_examples" -import { handlers, runWorkflow, stepDefaults } from "./runner" - -const SelectionContext = createContext({ selection: null, select: null } as any) - -export const Workflow = ({ params }) => { - const workflow = examples.find(w => w.id === +params.id) ?? err("Unknown workflow") // TODO: useApi() - - const ctx = { - selection: useSignal(null as any), - select: step => (ctx.selection.value = step), - } - - return ( - - - runWorkflow(workflow)} /> - - - - {!workflow.steps.length &&

Add steps from the right to build your workflow.

} - - - {workflow.steps.map(s => ( - - ))} - -
- - - - - -
- ) -} - -const Step = ({ step }) => { - const { selection, select } = useContext(SelectionContext) - const [[kind, props]] = Object.entries(step) - const [title, icon, subtitle] = renderers[kind](props) - - const selected = selection?.value === step - - return ( -
select(step)} - draggable - > -
- -
-
-

{title}

-
{subtitle}
-
-
- ) -} - -const PropsPane = ({ step }) => ( - <> -

- {step ? Object.keys(step)[0] : "No step selected"} -

- -
- {step ? ( -
console.log(data)} - onChange={data => Object.assign(step[Object.keys(step)[0]], data)} - > - - {Object.keys(step[Object.keys(step)[0]]).map(k => ( - <> - - - - ))} - -
- ) : ( -

Click on a step to edit its properties, or drag a step from the sidebar to add it to the workflow.

- )} -
- -) - -const StepCatalog = () => ( -
-

Step Catalog

- -
- {Object.keys(handlers).map(k => ( - - ))} -
-
-) - -type Renderers = { - [k in keyof typeof handlers]: (s: (typeof stepDefaults)[k]) => [string, typeof Clock, string?] -} - -const renderers: Renderers = { - wait: s => [`Wait ${humanDuration(s.duration)}`, Clock], - generate: s => ["Generate", Wand2], - instruction: s => ["Instruction", Wand2, s.instruction], - http_request: s => ["HTTP Request", Cloud, s.url], - query_selector: s => ["Query Selector", MousePointerSquare, s.selector], - // extract: s => ["Extract", TableProperties, s.fields.join(", ")], - // for_each: s => ["For Each", Repeat2], -} diff --git a/src/app/workflow/Workflows.tsx b/src/app/workflow/Workflows.tsx deleted file mode 100644 index 001eac7..0000000 --- a/src/app/workflow/Workflows.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Plus } from "lucide" -import { Alert, IconButton, Link, Page, Table } from "../_components" -import { examples } from "./_examples" -import { router } from "../router" - -export const Workflows = () => { - const createWorkflow = () => { - const id = Date.now() - examples.push({ id, name: "New Workflow", steps: [] }) - router.navigate(`/workflows/${id}`) - } - - return ( - - - - - - - - This feature is experimental.
- No changes are saved to the database -
- - - - - - - - - - {examples.map(w => ( - - - - {/* TODO: Automatic when the first action is a trigger/cron */} - - ))} - -
NameKind
- - {w.name} - - Manual
-
-
- ) -} diff --git a/src/app/workflow/_examples.ts b/src/app/workflow/_examples.ts deleted file mode 100644 index a2f52aa..0000000 --- a/src/app/workflow/_examples.ts +++ /dev/null @@ -1,36 +0,0 @@ -export const examples = [ - { - id: 1, - name: "Chuck Norris Jokes Explained", - steps: [ - { http_request: { method: "GET", url: "https://api.chucknorris.io/jokes/random" } }, - { instruction: { instruction: "Extract the value part." } }, - { instruction: { instruction: "Explain the joke, reason step by step." } }, - ], - }, - - { - id: 2, - name: "Local Llama Top Posts", - steps: [ - { http_request: { method: "GET", url: "https://old.reddit.com/r/LocalLLaMA/top/" } }, - { query_selector: { selector: ".thing" } }, - { instruction: { instruction: "Extract the title and url and respond with valid JSON." } }, - ], - }, - - { - id: 3, - name: "Scrape Hacker News Jobs", - steps: [ - { wait: { duration: 2 } }, - { http_request: { method: "GET", url: "https://hn.svelte.dev/jobs/1" } }, - { query_selector: { selector: "main article", limit: 5 } }, - { - instruction: { - instruction: "Extract the job title, company and expected skills for the role. Respond with valid JSON.", - }, - }, - ], - }, -] diff --git a/src/app/workflow/runner.ts b/src/app/workflow/runner.ts deleted file mode 100644 index b2a2af6..0000000 --- a/src/app/workflow/runner.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { signal } from "@preact/signals" -import { generate } from "../_hooks/useGenerate" -import { parseHTML } from "../_util" - -export type Step = (typeof stepDefaults)[keyof typeof stepDefaults] - -// TODO: this should be retrieved from the server -export const stepDefaults = { - wait: { duration: 1 }, - generate: {}, // TODO: opts for sampling, etc. - instruction: { instruction: "" }, - http_request: { method: "GET", url: "" }, - query_selector: { selector: "", limit: 2, clean: true }, -} - -type Handlers = { - [k in keyof typeof stepDefaults]: (opts: (typeof stepDefaults)[k], input: string) => string | Promise -} - -// TODO: this should be implemented on the server side -export const handlers: Handlers = { - wait: ({ duration }) => { - return new Promise(resolve => setTimeout(resolve, duration * 1000)) - }, - - generate: (opts, input) => { - const result = signal("") - const status = signal(null) - - return generate({ ...opts, prompt: input }, result, status) - }, - - instruction: ({ instruction, ...opts }, input) => { - return handlers.generate(opts, `ASSISTANT: ${input}\n\nUSER: ${instruction}\n\nASSISTANT: Sure!`) - }, - - http_request: ({ url }) => { - return fetch("/api/proxy", { method: "POST", body: JSON.stringify(url) }).then(res => res.text()) - }, - - query_selector: ({ selector, limit = 2, clean = true }, input) => { - return [...parseHTML(input, clean).querySelectorAll(selector)] - .slice(0, limit) - .map(el => el.innerHTML) - .join("") - }, -} - -export const runWorkflow = async workflow => { - const runStep = async (step, input) => { - for (const k in handlers) { - if (k in step) { - const res = await handlers[k](step[k], input) - - return res - } - } - - throw new Error(`No handler found for step ${JSON.stringify(step)}`) - } - - let input = null - for (const step of workflow.steps) { - input = await runStep(step, input) - } -} diff --git a/src/cli.zig b/src/cli.zig deleted file mode 100644 index 721d2e7..0000000 --- a/src/cli.zig +++ /dev/null @@ -1,26 +0,0 @@ -const std = @import("std"); -const ava = @import("main.zig"); - -// This is only used for the headless build -pub fn main() !void { - std.log.debug("Starting the server", .{}); - try ava.start(); - - const banner = - \\ - \\ /\ \ / /\ Server running - \\ /--\ \/ /--\ http://{} - \\ _____________________________________________ - \\ - \\ - ; - - std.debug.print(banner, .{ - ava.server.?.http.socket.listen_address, - }); - - ava.server.?.thread.join(); - - std.log.debug("Stopping the server", .{}); - _ = ava.ava_stop(); -} diff --git a/src/db.zig b/src/db.zig deleted file mode 100644 index 2e165fd..0000000 --- a/src/db.zig +++ /dev/null @@ -1,88 +0,0 @@ -const std = @import("std"); -const util = @import("util.zig"); -const sqlite = @import("ava-sqlite"); - -pub const Model = struct { - id: u32, - name: []const u8, - path: []const u8, - imported: bool, -}; - -pub const Prompt = struct { - id: u32, - name: []const u8, - prompt: []const u8, -}; - -pub const Chat = struct { - id: u32, - name: []const u8, - prompt: ?[]const u8, -}; - -pub const ChatMessage = struct { - id: u32, - chat_id: u32, - role: []const u8, - content: []const u8, -}; - -var db: sqlite.SQLite3 = undefined; - -pub fn init(allocator: std.mem.Allocator) !void { - const db_file = try util.getWritableHomePath(allocator, &.{"db"}); - defer allocator.free(db_file); - - db = try sqlite.SQLite3.open(db_file); - try sqlite.migrate(allocator, &db, @embedFile("db_schema.sql")); -} - -pub fn deinit() void { - db.close(); -} - -pub fn exec(sql: []const u8, args: anytype) !void { - return db.exec(sql, args); -} - -pub fn query(sql: []const u8, args: anytype) !sqlite.Statement { - return db.query(sql, args); -} - -pub fn get(comptime T: type, sql: []const u8, args: anytype) !T { - return db.get(T, sql, args); -} - -pub fn getString(allocator: std.mem.Allocator, sql: []const u8, args: anytype) ![]const u8 { - return db.getString(allocator, sql, args); -} - -// TODO: later -// pub fn insert(comptime T: type, values: anytype) !std.meta.FieldType(T, .id) { -// comptime var fields: []const u8 = ""; -// comptime var placeholders: []const u8 = ""; -// -// inline for (std.meta.fields(T), 0..) |f, i| { -// if (i != 0) { -// fields = fields ++ ", "; -// placeholders = placeholders ++ ", "; -// } -// -// fields = fields ++ f.name; -// placeholders = placeholders ++ "?"; -// } -// -// var stmt = try db.query(comptime "INSERT INTO " ++ tableName(T) ++ "(" ++ fields ++ ") VALUES(" ++ placeholders ++ ") RETURNING id", values); -// defer stmt.deinit(); -// -// return stmt.read(std.meta.FieldType(T, .id)); -// } -// -// pub fn delete(comptime T: type, id: std.meta.FieldType(T, .id)) !void { -// return exec("DELETE FROM " ++ tableName(T) ++ " WHERE id = ?", .{id}); -// } -// -// fn tableName(comptime T: type) []const u8 { -// return std.fs.path.extension(@typeName(T))[1..]; -// } diff --git a/src/db_schema.sql b/src/db_schema.sql deleted file mode 100644 index d83a8d4..0000000 --- a/src/db_schema.sql +++ /dev/null @@ -1,30 +0,0 @@ --- --- IMPORTANT: --- Table names need to be quoted in order to work with our migration tool. --- -CREATE Table "Model" ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - path TEXT NOT NULL, - imported INTEGER NOT NULL DEFAULT 0 -) STRICT; - -CREATE TABLE "Prompt" ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - prompt TEXT NOT NULL -) STRICT; - -CREATE TABLE "Chat" ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - prompt TEXT -) STRICT; - -CREATE TABLE "ChatMessage" ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - chat_id INTEGER NOT NULL, - role TEXT NOT NULL, - content TEXT NOT NULL, - FOREIGN KEY (chat_id) REFERENCES Chat(id) ON DELETE CASCADE -) STRICT; \ No newline at end of file diff --git a/src/llama.zig b/src/llama.zig deleted file mode 100644 index 4b3898c..0000000 --- a/src/llama.zig +++ /dev/null @@ -1,497 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const log = std.log.scoped(.llama); - -const c = @cImport({ - @cDefine("GGML_UNREACHABLE", "unreachable"); - @cInclude("llama.h"); -}); - -pub const Token = c.llama_token; - -pub const SamplingParams = struct { - top_k: u32 = 40, - top_p: f32 = 0.5, - temperature: f32 = 0.7, - repeat_n_last: usize = 256, - repeat_penalty: f32 = 1.05, - presence_penalty: f32 = 0, - freq_penalty: f32 = 0, - add_bos: bool = true, - stop_eos: bool = true, - stop: []const []const u8 = &.{}, - json: bool = false, -}; - -pub fn init(allocator: std.mem.Allocator) void { - const H = struct { - fn trampoline(_: c.enum_ggml_log_level, data: [*c]const u8, _: ?*anyopaque) callconv(.C) void { - log.debug("{s}", .{data}); - } - }; - - c.llama_backend_init(false); - c.llama_log_set(H.trampoline, null); - Pool.init(allocator); -} - -pub fn deinit() void { - Pool.deinit(); - c.llama_backend_free(); -} - -/// A single-model, single-context, thread-safe pool. -pub const Pool = struct { - var allocator: std.mem.Allocator = undefined; - var mutex = std.Thread.Mutex{}; - var model: ?Model = null; - var context: ?Context = null; - - /// Initializes the pool. - pub fn init(ally: std.mem.Allocator) void { - allocator = ally; - } - - /// Deinitializes the pool. - pub fn deinit() void { - if (context != null) { - context.?.deinit(); - context = null; - - model.?.deinit(); - model = null; - } - } - - /// Returns a context for the given model. The context must be released - /// after use. This function is thread-safe. Fails if the context is busy - /// for more than `timeout` milliseconds. - pub fn get(model_path: []const u8, timeout: u64) !*Context { - const start = std.time.milliTimestamp(); - - while (!mutex.tryLock()) { - if (std.time.milliTimestamp() - start > timeout) return error.ContextBusy; - std.time.sleep(100_000_000); - } - errdefer mutex.unlock(); - - // Reset if the model has changed. - if (model != null and !std.mem.eql(u8, model.?.path, model_path)) { - context.?.deinit(); - context = null; - - model.?.deinit(); - model = null; - } - - if (model == null) { - model = try Model.loadFromFile(allocator, model_path); - context = try Context.init( - allocator, - &model.?, - (std.Thread.getCpuCount() catch 8) / 2, // TODO: make this configurable (in global settings) - ); - } - - return &context.?; - } - - /// Releases the context so that it can be used by other threads. - pub fn release(ctx: *Context) void { - if (ctx == &context.?) { - mutex.unlock(); - } - } -}; - -pub const Model = struct { - allocator: std.mem.Allocator, - path: []const u8, - params: c.llama_model_params, - ptr: *c.llama_model, - - /// Loads a model from a file. - pub fn loadFromFile(allocator: std.mem.Allocator, path: []const u8) !Model { - const pathZ = try getShortPath(allocator, path); - defer allocator.free(pathZ); - var params = c.llama_model_default_params(); - - // It seems Metal never worked on Intel-based macs. - // see https://github.com/ggerganov/llama.cpp/issues/3423#issuecomment-1745511586 - // - // TODO: now even macos can do a split, so we should make it configurable (and/or detect) - // https://github.com/ggerganov/llama.cpp/blob/master/llama.cpp#L9643 - params.n_gpu_layers = if (builtin.os.tag == .macos and builtin.cpu.arch == .aarch64) 999 else 0; - log.debug("n_gpu_layers = {}", .{params.n_gpu_layers}); - - // Load the model - const ptr = c.llama_load_model_from_file(pathZ.ptr, params) orelse return error.InvalidModel; - - return .{ - .allocator = allocator, - .path = try allocator.dupe(u8, path), // save original, long path - .params = params, - .ptr = ptr, - }; - } - - /// Deinitializes the model. - pub fn deinit(self: *Model) void { - c.llama_free_model(self.ptr); - self.allocator.free(self.path); - } - - /// Returns a list of tokens for the given input. - pub fn tokenize(self: *Model, allocator: std.mem.Allocator, input: []const u8, max_tokens: usize, add_bos: bool) !std.ArrayList(Token) { - var tokens = try std.ArrayList(Token).initCapacity(allocator, max_tokens); - errdefer tokens.deinit(); - - const n_tokens = c.llama_tokenize( - self.ptr, - input.ptr, - @intCast(input.len), - tokens.items.ptr, - @intCast(max_tokens), - add_bos, - true, - ); - - if (n_tokens >= 0) { - tokens.items.len = @intCast(n_tokens); - } else { - return error.FailedToTokenize; - } - - return tokens; - } - - fn getShortPath(allocator: std.mem.Allocator, path: []const u8) ![:0]const u8 { - // llama.cpp is using fopen() and it does not support UTF-8 paths on Windows - // so what we need to do is to call GetShortPathName() to get the short path - // and it should work - if (comptime builtin.os.tag == .windows) { - const w = struct { - extern "kernel32" fn GetShortPathNameW(lpszLongPath: ?[*:0]const u16, lpszShortPath: ?[*:0]u16, cchBuffer: u32) callconv(std.os.windows.WINAPI) u32; - }; - - const wpath = try std.unicode.utf8ToUtf16LeWithNull(allocator, path); - defer allocator.free(wpath); - - var buf: [260:0]u16 = undefined; - _ = w.GetShortPathNameW(wpath, &buf, buf.len); - - return std.unicode.utf16leToUtf8AllocZ(allocator, &buf); - } - - return allocator.dupeZ(u8, path); - } -}; - -pub const Context = struct { - model: *Model, - params: c.llama_context_params, - ptr: *c.llama_context, - grammar: ?*c.llama_grammar = null, - tokens: std.ArrayList(Token), - n_past: usize = 0, - candidates: std.ArrayList(c.llama_token_data), - buf: std.ArrayList(u8), - - /// Initializes the context. - pub fn init(allocator: std.mem.Allocator, model: *Model, n_threads: usize) !Context { - var params = c.llama_context_default_params(); - params.n_ctx = 2048; // TODO: make this configurable - params.n_batch = 64; // 512; // TODO: @min(512, xxx) - params.n_threads = @intCast(n_threads); - - return .{ - .model = model, - .params = params, - .ptr = c.llama_new_context_with_model(model.ptr, params) orelse return error.UnexpectedError, - .tokens = std.ArrayList(Token).init(allocator), - .candidates = std.ArrayList(c.llama_token_data).init(allocator), - .buf = std.ArrayList(u8).init(allocator), - }; - } - - /// Deinitializes the context. - pub fn deinit(self: *Context) void { - if (self.grammar) |g| { - c.llama_grammar_free(g); - } - - c.llama_free(self.ptr); - self.tokens.deinit(); - self.candidates.deinit(); - self.buf.deinit(); - } - - /// Prepares the context for inference. - pub fn prepare(self: *Context, prompt: []const u8, params: *const SamplingParams) !void { - const tokens = try self.model.tokenize(self.tokens.allocator, prompt, @intCast(c.llama_n_ctx(self.ptr)), params.add_bos); - self.buf.shrinkRetainingCapacity(0); - - // Find the common part and set n_past accordingly. - var n_past: usize = 0; - - for (0..@min(self.tokens.items.len, tokens.items.len)) |i| { - if (self.tokens.items[i] != tokens.items[i]) break; - n_past = i; - } - - if (self.grammar) |g| { - c.llama_grammar_free(g); - self.grammar = null; - } - - if (params.json) { - self.grammar = c.llama_grammar_init(@constCast(grammar.JSON_RULES.ptr), grammar.JSON_RULES.len, 0); - } - - log.debug("{} tokens, n_past = {}", .{ tokens.items.len, n_past }); - - self.tokens.deinit(); - self.tokens = tokens; - self.n_past = n_past; - } - - /// Runs the inference. - /// Does nothing if the context is already up-to-date. - pub fn eval(self: *Context) !void { - while (try self.evalOnce() > 0) {} - } - - /// Runs one step of inference. - /// Does nothing if the context is already up-to-date. - /// Returns the number of tokens evaluated. - pub fn evalOnce(self: *Context) !usize { - const n_eval = @min( - @as(usize, @intCast(self.params.n_batch)), - self.tokens.items.len - self.n_past, - ); - - if (n_eval > 0) { - const toks = self.tokens.items[self.n_past..]; - - if (c.llama_eval( - self.ptr, - toks.ptr, - @intCast(n_eval), - @intCast(self.n_past), - ) != 0) { - return error.FailedToEval; - } - - self.n_past += n_eval; - } - - return n_eval; - } - - /// Generates next utf-8 valid chunk of text. - pub fn generate(self: *Context, params: *const SamplingParams) !?[]const u8 { - const start = self.buf.items.len; - - // Keep generating until we have valid chunk, but not more than 32 times. - for (0..32) |_| { - const token = try self.generateToken(params) orelse return null; - const piece = std.mem.span(c.llama_token_get_text(self.model.ptr, token)); - - switch (c.llama_token_get_type(self.model.ptr, token)) { - c.LLAMA_TOKEN_TYPE_NORMAL => { - // Replace \xe2\x96\x81 (Lower One Eighth Block) with space - if (c.llama_vocab_type(self.model.ptr) == c.LLAMA_VOCAB_TYPE_SPM) { - const n = std.mem.replace(u8, piece, "▁", " ", try self.buf.addManyAsSlice(piece.len)); - self.buf.items.len -= n * 2; // ▁ is 3 bytes, space is 1 byte, so we need to remove 2 bytes - } else if (c.llama_vocab_type(self.model.ptr) == c.LLAMA_VOCAB_TYPE_BPE) { - try appendBPE(&self.buf, piece); - } else { - try self.buf.appendSlice(piece); - } - }, - c.LLAMA_TOKEN_TYPE_UNKNOWN => try self.buf.appendSlice("▅"), - c.LLAMA_TOKEN_TYPE_BYTE => try self.buf.append(try std.fmt.parseInt(u8, piece[3..5], 16)), - else => {}, - } - - const chunk = self.buf.items[start..]; - - if (std.unicode.utf8ValidateSlice(chunk)) { - // Handle stop tokens. - for (params.stop) |s| { - // Stop if the chunk contains the stop suffix. - if (std.mem.indexOf(u8, chunk, s) != null) { - return null; - } - - // Current chunk might be a start of the stop suffix, so let's generate one more token. - if (std.mem.startsWith(u8, s, chunk)) { - break; - } - } else return chunk; - } - } - - // Discard the invalid chunk. - self.buf.shrinkRetainingCapacity(start); - return null; - } - - /// Generates a token using the given sampler and appends it to the context. - pub fn generateToken(self: *Context, params: *const SamplingParams) !?Token { - if (self.tokens.items.len >= c.llama_n_ctx(self.ptr)) { - // Truncate input if it's too long but keep some empty space for - // new tokens. - const cutoff: usize = @intCast(@divTrunc(c.llama_n_ctx(self.ptr), 2)); - for (self.tokens.items[cutoff..], 0..) |t, i| { - self.tokens.items[i] = t; - } - self.tokens.items.len = cutoff; - self.n_past = 0; - - log.debug("truncated input to {}", .{cutoff}); - } - - try self.eval(); - - const token = try self.sampleToken(params) orelse return null; - try self.tokens.append(token); - return token; - } - - /// Samples a token from the context. - pub fn sampleToken(self: *Context, params: *const SamplingParams) !?Token { - const logits = c.llama_get_logits(self.ptr); - try self.candidates.resize(@intCast(c.llama_n_vocab(self.model.ptr))); - - for (self.candidates.items, 0..) |*candidate, i| { - candidate.* = .{ - .id = @intCast(i), - .logit = logits[i], - .p = 0, - }; - } - - var candidates: c.llama_token_data_array = .{ - .data = self.candidates.items.ptr, - .size = self.candidates.items.len, - .sorted = false, - }; - - if (self.grammar != null) { - c.llama_sample_grammar(self.ptr, &candidates, self.grammar); - } - - // Apply repetition penalties. - const last_n = @min(self.tokens.items.len, params.repeat_n_last); - c.llama_sample_repetition_penalties(self.ptr, &candidates, &self.tokens.items[self.tokens.items.len - last_n], @intCast(last_n), params.repeat_penalty, params.presence_penalty, params.freq_penalty); - - if (params.temperature >= 0) { - c.llama_sample_top_k(self.ptr, &candidates, @intCast(params.top_k), 1); - c.llama_sample_top_p(self.ptr, &candidates, params.top_p, 1); - c.llama_sample_temperature(self.ptr, &candidates, params.temperature); - } - - const res = if (params.temperature >= 0) c.llama_sample_token(self.ptr, &candidates) else c.llama_sample_token_greedy(self.ptr, &candidates); - - if (self.grammar != null) { - c.llama_grammar_accept_token(self.ptr, self.grammar, res); - } - - if (params.stop_eos and res == c.llama_token_eos(self.model.ptr)) { - return null; - } - - return res; - } - - /// Appends a BPE piece to the buffer. - pub fn appendBPE(buf: *std.ArrayList(u8), piece: []const u8) !void { - try buf.ensureUnusedCapacity(piece.len); - var iter = (try std.unicode.Utf8View.init(piece)).iterator(); - - while (iter.nextCodepoint()) |cp| { - try buf.append(@intCast(switch (cp) { - '!'...'~', '¡'...'¬', '®'...'ÿ' => cp, - 'Ā'...'Ġ' => cp - 256, - 'ġ'...'ł' => cp - 162, - 'Ń' => 173, - else => { - log.debug("Invalid BPE? {s} cp: {}", .{ piece, cp }); - continue; - }, - })); - } - } -}; - -const grammar = struct { - const JSON_RULES = rulePtrs(&.{ - &.{ ref(1), end(0) }, - &.{ ch('{'), ref(7), ref(11), ch('}'), end(0) }, // ref(7), end(0) }, - &.{ ref(1), alt(0), ref(3), alt(0), ref(4), alt(0), ref(5), alt(0), ref(6), ref(7), end(0) }, - &.{ ch('['), ref(7), ref(15), ch(']'), ref(7), end(0) }, - &.{ ch('"'), ref(18), ch('"'), ref(7), end(0) }, - &.{ ref(19), ref(25), ref(29), ref(7), end(0) }, - &.{ ch('t'), ch('r'), ch('u'), ch('e'), alt(0), ch('f'), ch('a'), ch('l'), ch('s'), ch('e'), alt(0), ch('n'), ch('u'), ch('l'), ch('l'), end(0) }, - &.{ ref(31), end(0) }, - &.{ ref(4), ch(':'), ref(7), ref(2), ref(10), end(0) }, - &.{ ch(','), ref(7), ref(4), ch(':'), ref(7), ref(2), end(0) }, - &.{ ref(9), ref(10), alt(0), end(0) }, - &.{ ref(8), alt(0), end(0) }, - &.{ ref(2), ref(14), end(0) }, - &.{ ch(','), ref(7), ref(2), end(0) }, - &.{ ref(13), ref(14), alt(0), end(0) }, - &.{ ref(12), alt(0), end(0) }, - &.{ chNot('"'), chAlt('\\'), alt(0), ch('\\'), ref(17), end(0) }, - &.{ ch('"'), chAlt('\\'), chAlt('/'), chAlt('b'), chAlt('f'), chAlt('n'), chAlt('r'), chAlt('t'), alt(0), ch('u'), ch('0'), chRnu('9'), chAlt('a'), chRnu('f'), chAlt('A'), chRnu('F'), ch('0'), chRnu('9'), chAlt('a'), chRnu('f'), chAlt('A'), chRnu('F'), ch('0'), chRnu('9'), chAlt('a'), chRnu('f'), chAlt('A'), chRnu('F'), ch('0'), chRnu('9'), chAlt('a'), chRnu('f'), chAlt('A'), chRnu('F'), end(0) }, - &.{ ref(16), ref(18), alt(0), end(0) }, - &.{ ref(20), ref(21), end(0) }, - &.{ ch('-'), alt(0), end(0) }, - &.{ ch('0'), chRnu('9'), alt(0), ch('1'), chRnu('9'), ref(22), end(0) }, - &.{ ch('0'), chRnu('9'), ref(22), alt(0), end(0) }, - &.{ ch('.'), ref(24), end(0) }, - &.{ ch('0'), chRnu('9'), ref(24), alt(0), ch('0'), chRnu('9'), end(0) }, - &.{ ref(23), alt(0), end(0) }, - &.{ ch('e'), chAlt('E'), ref(27), ref(28), end(0) }, - &.{ ch('-'), chAlt('+'), alt(0), end(0) }, - &.{ ch('0'), chRnu('9'), ref(28), alt(0), ch('0'), chRnu('9'), end(0) }, - &.{ ref(26), alt(0), end(0) }, - &.{ ch(' '), chAlt('\u{0009}'), chAlt('\u{000A}'), ref(7), end(0) }, - &.{ ref(30), alt(0), end(0) }, - }); - - fn rulePtrs(comptime rules: []const []const c.llama_grammar_element) []const [*c]const c.llama_grammar_element { - var ptrs: [rules.len][*]const c.llama_grammar_element = undefined; - for (rules, 0..) |rule, i| ptrs[i] = rule.ptr; - return &ptrs; - } - - fn ref(i: usize) c.llama_grammar_element { - return .{ .type = c.LLAMA_GRETYPE_RULE_REF, .value = @intCast(i) }; - } - - fn end(i: usize) c.llama_grammar_element { - return .{ .type = c.LLAMA_GRETYPE_END, .value = @intCast(i) }; - } - - fn ch(v: u8) c.llama_grammar_element { - return .{ .type = c.LLAMA_GRETYPE_CHAR, .value = @intCast(v) }; - } - - fn chNot(v: u8) c.llama_grammar_element { - return .{ .type = c.LLAMA_GRETYPE_CHAR_NOT, .value = @intCast(v) }; - } - - fn chAlt(v: u8) c.llama_grammar_element { - return .{ .type = c.LLAMA_GRETYPE_CHAR_ALT, .value = @intCast(v) }; - } - - fn chRnu(v: u8) c.llama_grammar_element { - return .{ .type = c.LLAMA_GRETYPE_CHAR_RNG_UPPER, .value = @intCast(v) }; - } - - fn alt(i: usize) c.llama_grammar_element { - return .{ .type = c.LLAMA_GRETYPE_ALT, .value = @intCast(i) }; - } -}; diff --git a/src/macos/BuildMacos.zig b/src/macos/BuildMacos.zig deleted file mode 100644 index def837b..0000000 --- a/src/macos/BuildMacos.zig +++ /dev/null @@ -1,99 +0,0 @@ -const std = @import("std"); -const root = @import("../../build.zig"); -const BuildMacos = @This(); - -pub const Options = struct { - name: []const u8, - root_source_file: std.Build.LazyPath, - target: std.Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, - strip: bool, -}; - -owner: *std.Build, -object: *std.Build.Step.Compile, -root_module: *std.Build.Module, -swiftc: *std.Build.Step.Run, -out: std.Build.LazyPath, -sdk: []const u8, - -pub fn create(owner: *std.Build, options: Options) *BuildMacos { - const self = owner.allocator.create(@This()) catch @panic("OOM"); - - const object = owner.addObject(.{ - .name = options.name, - .root_source_file = options.root_source_file, - .target = options.target, - .optimize = options.optimize, - .strip = options.strip, - }); - object.bundle_compiler_rt = true; - - const swiftc = owner.addSystemCommand(&.{ - "swiftc", - if (options.optimize == .Debug) "-Onone" else "-O", - "-import-objc-header", - "include/ava.h", - "-lc++", - "-lsqlite3", - "-target", - owner.fmt("{s}-apple-macosx{}", .{ @tagName(options.target.result.cpu.arch), options.target.result.os.version_range.semver.min }), - "-o", - }); - - const out = swiftc.addOutputFileArg(options.name); - - swiftc.addArg("-Xlinker"); - swiftc.addFileArg(object.getEmittedBin()); - - if (options.optimize == .Debug) { - swiftc.addArgs(&.{ "-D", "DEBUG" }); - } - - swiftc.addFileArg(.{ .path = "src/macos/entry.swift" }); - swiftc.addFileArg(.{ .path = "src/macos/webview.swift" }); - - swiftc.step.dependOn(&object.step); - - self.* = .{ - .owner = owner, - .object = object, - .root_module = &object.root_module, - .swiftc = swiftc, - .out = out, - .sdk = std.zig.system.darwin.getSdk(owner.allocator, options.target.result) orelse @panic("No suitable SDK found"), - }; - - self.applySDK(object); - - return self; -} - -pub fn rootModuleTarget(self: *BuildMacos) std.Target { - return self.object.rootModuleTarget(); -} - -pub fn getEmittedBin(self: *BuildMacos) std.Build.LazyPath { - return self.out; -} - -pub fn addIncludePath(self: *BuildMacos, path: std.Build.LazyPath) void { - self.object.addIncludePath(path); -} - -pub fn addObject(self: *BuildMacos, object: *std.Build.Step.Compile) void { - self.swiftc.addArg("-Xlinker"); - self.swiftc.addFileArg(object.getEmittedBin()); -} - -pub fn linkFramework(self: *BuildMacos, name: []const u8) void { - self.object.linkFramework(name); -} - -pub fn applySDK(self: *BuildMacos, step: anytype) void { - std.log.debug("Using macOS SDK {s} for step {s}", .{ self.sdk, step.name }); - - step.addSystemIncludePath(.{ .path = self.owner.fmt("{s}/usr/include", .{self.sdk}) }); - step.addFrameworkPath(.{ .path = self.owner.fmt("{s}/System/Library/Frameworks", .{self.sdk}) }); - step.addLibraryPath(.{ .path = self.owner.fmt("{s}/usr/lib", .{self.sdk}) }); -} diff --git a/src/macos/Info.plist b/src/macos/Info.plist deleted file mode 100644 index 48ee638..0000000 --- a/src/macos/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - ava - CFBundleIdentifier - com.avapls.Ava-PLS - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Ava PLS - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSupportedPlatforms - - MacOSX - - CFBundleVersion - 1 - CFBundleIconFile - favicon.ico - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/src/macos/create_dmg.sh b/src/macos/create_dmg.sh deleted file mode 100755 index 610b89e..0000000 --- a/src/macos/create_dmg.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -# Variables -APP_NAME="Ava" -ZIG_OUT="$(dirname "$0")/../../zig-out" -APP_PATH="$ZIG_OUT/${APP_NAME}.app" -DMG_TMP_PATH="$ZIG_OUT/${APP_NAME}_tmp.dmg" -DMG_FINAL_PATH="$ZIG_OUT/${APP_NAME}_$(date +%Y-%m-%d).dmg" - -# Clean -rm -rf "$ZIG_OUT" - -# Build JS, x86_64, aarch64, and universal binary -npm run build \ -&& zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-macos.12.6 \ -&& zig build -Doptimize=ReleaseSafe -Dtarget=aarch64-macos.12.6 \ -&& lipo -create "$ZIG_OUT/bin/ava_aarch64" "$ZIG_OUT/bin/ava_x86_64" -output "$ZIG_OUT/bin/ava" - -if [ $? -ne 0 ]; then - echo "Build failed" - exit 1; -fi - -mkdir -p "${APP_PATH}/Contents/MacOS" \ -&& mkdir -p "${APP_PATH}/Contents/Resources" \ -&& cp ./src/macos/Info.plist "${APP_PATH}/Contents/" \ -&& cp "$ZIG_OUT/bin/ava" "${APP_PATH}/Contents/MacOS/" \ -&& cp ./src/app/favicon.ico ./llama.cpp/ggml-metal.metal "${APP_PATH}/Contents/Resources/" - -if [ $? -ne 0 ]; then - echo "Copy failed" - exit 1; -fi - -# Sign app -# Note it still needs to be notarized -codesign -fs "Developer ID Application: KAMIL TOMSIK (RYT4H286GA)" --deep --options=runtime --timestamp "${APP_PATH}" - -if [ $? -ne 0 ]; then - echo "Signing failed" - exit 1; -fi - -# Create temp DMG and perform some customizations -# Adapted from https://stackoverflow.com/questions/96882/how-do-i-create-a-nice-looking-dmg-for-mac-os-x-using-command-line-tools -hdiutil create -srcfolder "${APP_PATH}" -volname "${APP_NAME}" -fs HFS+ -fsargs "-c c=64,a=16,e=16" -format UDRW -size 512000k "${DMG_TMP_PATH}" -MOUNT_RESULT=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_TMP_PATH}") -DEVICE_PATH=$(echo "${MOUNT_RESULT}" | egrep '^/dev/' | sed 1q | awk '{print $1}') -echo ' - tell application "Finder" - tell disk "'${APP_NAME}'" - open - set current view of container window to icon view - set the bounds of container window to {400, 100, 885, 430} - set theViewOptions to the icon view options of container window - set arrangement of theViewOptions to not arranged - set icon size of theViewOptions to 72 - make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"} - set position of item "'${APP_NAME}'" of container window to {100, 100} - set position of item "Applications" of container window to {375, 100} - update without registering applications - delay 5 - close - end tell - end tell -' | osascript -hdiutil detach "${DEVICE_PATH}" - -# Create final DMG -hdiutil convert "${DMG_TMP_PATH}" -format UDZO -imagekey zlib-level=9 -o "${DMG_FINAL_PATH}" -rm "${DMG_TMP_PATH}" - -echo "DMG created at ${DMG_FINAL_PATH}" - -# Notarize (if run with --notarize) -if [ "$1" != "--notarize" ]; then exit 0; fi -xcrun notarytool submit --wait --keychain-profile "KAMIL TOMSIK" "${DMG_FINAL_PATH}" diff --git a/src/macos/entry.swift b/src/macos/entry.swift deleted file mode 100644 index a1d6d02..0000000 --- a/src/macos/entry.swift +++ /dev/null @@ -1,49 +0,0 @@ -import SwiftUI - -@main -struct Main: App { - @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - @StateObject var state = AppState.shared - - var body: some Scene { - WindowGroup { - WebViewContent(url: "http://127.0.0.1:\(state.port)/") - .frame( - minWidth: 700, idealWidth: 900, - minHeight: 480, idealHeight: 800) - .edgesIgnoringSafeArea(.top) - } - .windowStyle(.hiddenTitleBar) - } -} - -final class AppDelegate: NSObject, NSApplicationDelegate { - func applicationWillFinishLaunching(_ notification: Notification) { - NSApp.setActivationPolicy(.regular) - NSApp.activate(ignoringOtherApps: true) - - if (ava_start() != 0 || ava_get_port() < 0) { - exit(1) - } - - signal(SIGPIPE) { _ in - // Ignore SIGPIPE which is sent when the client closes the connection (which is normal) - } - - AppState.shared.port = ava_get_port() - } - - func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - true - } - - func applicationWillTerminate(_ notification: Notification) { - ava_stop() - } -} - -final class AppState: ObservableObject { - static let shared = AppState() - - @Published var port: Int32 = -1 -} diff --git a/src/macos/webview.swift b/src/macos/webview.swift deleted file mode 100644 index e32c252..0000000 --- a/src/macos/webview.swift +++ /dev/null @@ -1,119 +0,0 @@ -import SwiftUI -import WebKit - -struct WebViewContent: View { - var url: String? - - var body: some View { - (url != nil) - ?AnyView(WebView(url: url!)) - :AnyView(ProgressView()) - } -} - -struct WebView: NSViewRepresentable { - var url: String - - private let navigationDelegate = NavigationDelegate() - private let scriptMessageHandler = ScriptMessageHandler() - private let uiDelegate = WebViewUIDelegate() - - func makeNSView(context: Context) -> WKWebView { - let cfg = WKWebViewConfiguration() - cfg.userContentController.add(self.scriptMessageHandler, name: "event") - - #if DEBUG - cfg.preferences.setValue(true, forKey: "developerExtrasEnabled") - #endif - - let webview = WKWebView(frame: .zero, configuration: cfg) - webview.navigationDelegate = self.navigationDelegate - webview.uiDelegate = self.uiDelegate - - DispatchQueue.main.async { - scriptMessageHandler.window = webview.window - } - - return webview - } - - func updateNSView(_ uiView: WKWebView, context: Context) { - navigationDelegate.url = url - - uiView.load(URLRequest(url: URL(string: url)!)) - } -} - -final class ScriptMessageHandler: NSObject, WKScriptMessageHandler { - var window: NSWindow! - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - switch message.body as? String { - case "mousedown": - if !window.styleMask.contains(.fullScreen) { - window.performDrag(with:NSApp.currentEvent!) - } - case "dblclick": - window.performZoom(nil) - default: - break - } - } -} - -final class NavigationDelegate: NSObject, WKNavigationDelegate { - var url: String = "" - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - // TODO: Show initial loading indicator until this method is called - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - print("Error: \(error)") - } - - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy)->Void) { - if let url = navigationAction.request.url?.absoluteString { - if url.starts(with: "file://") { - decisionHandler(.cancel) - } else if url.starts(with: self.url) { - decisionHandler(.allow) - } else { - decisionHandler(.cancel) - NSWorkspace.shared.open(URL(string: url)!) - } - } - } -} - -final class WebViewUIDelegate: NSObject, WKUIDelegate { - func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame: WKFrameInfo, - completionHandler: @escaping (Bool) -> Void) { - let alert = NSAlert() - alert.messageText = message - alert.addButton(withTitle: "OK") - alert.addButton(withTitle: "Cancel") - alert.beginSheetModal(for: webView.window!) { response in - completionHandler(response == .alertFirstButtonReturn) - } - } - - func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame: WKFrameInfo, - completionHandler: @escaping (String?) -> Void) { - let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) - textField.stringValue = defaultText ?? "" - let alert = NSAlert() - alert.messageText = prompt - alert.accessoryView = textField - alert.addButton(withTitle: "OK") - alert.addButton(withTitle: "Cancel") - alert.beginSheetModal(for: webView.window!) { response in - if response == .alertFirstButtonReturn { - completionHandler(textField.stringValue) - } else { - completionHandler(nil) - } - } - alert.window.makeFirstResponder(textField) - } -} \ No newline at end of file diff --git a/src/main.zig b/src/main.zig deleted file mode 100644 index 74e2f31..0000000 --- a/src/main.zig +++ /dev/null @@ -1,53 +0,0 @@ -const builtin = @import("builtin"); -const std = @import("std"); -const db = @import("db.zig"); -const llama = @import("llama.zig"); -const Server = @import("server.zig").Server; -const util = @import("util.zig"); - -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; -const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; - -pub var server: ?*Server = null; - -pub const std_options = struct { - pub const log_level = .debug; - pub const logFn = util.Logger.log; -}; - -pub fn start() !void { - if (server != null) { - return error.ServerAlreadyStarted; - } - - llama.init(allocator); - try db.init(allocator); - - server = try Server.start(allocator, "127.0.0.1", 3002); -} - -pub export fn ava_start() c_int { - start() catch |e| { - std.log.err("Unexpected error: {}", .{e}); - return 1; - }; - - server.?.thread.detach(); - - return 0; -} - -pub export fn ava_get_port() c_int { - return if (server == null) -1 else server.?.http.socket.listen_address.getPort(); -} - -pub export fn ava_stop() c_int { - if (server == null) return 1; - - server.?.deinit(); - db.deinit(); - llama.deinit(); - if (builtin.mode == .Debug) _ = gpa.deinit(); - server = null; - return 0; -} diff --git a/src/server.zig b/src/server.zig deleted file mode 100644 index 3711559..0000000 --- a/src/server.zig +++ /dev/null @@ -1,304 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const api = @import("api.zig"); -const log = std.log.scoped(.server); - -/// A context for handling a request. -pub const Context = struct { - arena: std.mem.Allocator, - res: std.http.Server.Response, - path: []const u8, - query: ?[]const u8, - - pub fn deinit(self: *Context) void { - _ = self.res.reset(); - self.res.deinit(); - - var arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.arena.ptr)); - arena.deinit(); - arena.child_allocator.destroy(arena); - } - - pub fn match(self: *const Context, pattern: []const u8) ?Params { - return Params.match(pattern, self.path); - } - - pub fn router(self: *Context, comptime routes: type) !void { - inline for (@typeInfo(routes).Struct.decls) |d| { - const method = comptime d.name[0 .. std.mem.indexOfScalar(u8, d.name, ' ') orelse unreachable]; - const pattern = d.name[method.len + 1 ..]; - - if (self.res.request.method == @field(std.http.Method, method)) { - if (self.match(pattern)) |params| { - const handler = comptime @field(api, d.name); - - var args: std.meta.ArgsTuple(@TypeOf(handler)) = undefined; - args[0] = self; - inline for (1..args.len) |i| { - const V = @TypeOf(args[i]); - args[i] = try if (comptime @typeInfo(V) == .Struct) self.readJson(V) else params.get(i - 1, V); - } - - return @call(.auto, handler, args); - } - } - } - - return error.NotFound; - } - - /// Reads the request body as JSON. - pub fn readJson(self: *Context, comptime T: type) !T { - var reader = std.json.reader(self.arena, self.res.reader()); - - return std.json.parseFromTokenSourceLeaky( - T, - self.arena, - &reader, - .{ .ignore_unknown_fields = true }, - ); - } - - /// Sends a chunk of data. Automatically sets the transfer encoding to - /// chunked if it hasn't been set yet. - pub fn sendChunk(self: *Context, chunk: []const u8) !void { - if (self.res.state == .waited) { - self.res.transfer_encoding = .chunked; - try self.res.send(); - } - - // Response.write() will always write all of the data when the transfer - // encoding is chunked - _ = try self.res.write(chunk); - } - - /// Sends a chunk of JSON. The chunk always ends with a newline. - /// Supports values, slices and iterators. - pub fn sendJson(self: *Context, body: anytype) !void { - if (self.res.state == .waited) { - try self.res.headers.append("Content-Type", "application/json"); - } - - var list = std.ArrayList(u8).init(self.arena); - var writer = list.writer(); - defer list.deinit(); - - if (comptime std.meta.hasFn(@TypeOf(body), "next")) { - var copy = body; - var i: usize = 0; - - try writer.writeAll("["); - while (try copy.next()) |item| : (i += 1) { - if (i != 0) try writer.writeAll(","); - try std.json.stringify(item, .{}, writer); - } - try writer.writeAll("]"); - } else { - try std.json.stringify(body, .{}, writer); - } - - try list.appendSlice("\r\n"); - try self.sendChunk(list.items); - } - - /// Sends a static resource. The resource is embedded in release builds. - pub fn sendResource(self: *Context, comptime path: []const u8) !void { - try self.res.headers.append("Content-Type", comptime mime(std.fs.path.extension(path)) ++ "; charset=utf-8"); - try self.noCache(); - - try self.sendChunk(if (comptime builtin.mode != .Debug) @embedFile("../" ++ path) else blk: { - var f = try std.fs.cwd().openFile(path, .{}); - defer f.close(); - break :blk try f.readToEndAlloc(self.arena, std.math.maxInt(usize)); - }); - } - - /// Sends an empty response. - pub fn noContent(self: *Context) !void { - self.res.status = .no_content; - try self.res.send(); - } - - /// Adds no-cache headers to the response. - pub fn noCache(self: *Context) !void { - try self.res.headers.append("Cache-Control", "no-cache, no-store, must-revalidate"); - try self.res.headers.append("Pragma", "no-cache"); - try self.res.headers.append("Expires", "0"); - } -}; - -/// An instance of our HTTP server. -pub const Server = struct { - allocator: std.mem.Allocator, - http: std.http.Server, - thread: std.Thread, - status: std.atomic.Value(enum(u8) { starting, started, stopping, stopped }) = .{ .raw = .starting }, - - pub fn start(allocator: std.mem.Allocator, hostname: []const u8, port: u16) !*Server { - const self = try allocator.create(Server); - errdefer allocator.destroy(self); - - var http = std.http.Server.init(.{ .reuse_address = true }); - errdefer http.deinit(); - - const address = try std.net.Address.parseIp(hostname, port); - try http.listen(address); - - self.* = .{ - .allocator = allocator, - .http = http, - .thread = try std.Thread.spawn(.{}, run, .{self}), - }; - - return self; - } - - pub fn deinit(self: *Server) void { - self.status.store(.stopping, .Release); - - if (std.net.tcpConnectToAddress(self.http.socket.listen_address)) |conn| { - conn.close(); - } else |e| log.err("stop err: {}", .{e}); - - while (self.status.load(.Acquire) == .stopping) { - std.time.sleep(100_000_000); - } - - self.http.deinit(); - self.allocator.destroy(self); - } - - fn run(self: *Server) !void { - self.status.store(.started, .Release); - defer self.status.store(.stopped, .Release); - - while (self.status.load(.Acquire) == .started) { - // accidental moving/copying would invalidate pointers inside - var arena = try self.allocator.create(std.heap.ArenaAllocator); - errdefer self.allocator.destroy(arena); - arena.* = std.heap.ArenaAllocator.init(self.allocator); - errdefer arena.deinit(); - - const res = try self.http.accept(.{ - .allocator = arena.allocator(), - .header_strategy = .{ .dynamic = 10_000 }, - }); - - // Sent from Server.deinit() to awake the thread - if (self.status.load(.Acquire) == .stopping) return; - - var thread = try std.Thread.spawn(.{}, runInThread, .{res}); - thread.detach(); - } - } - - fn runInThread(res: std.http.Server.Response) !void { - var ctx = Context{ - .arena = res.allocator, - .res = res, - .path = undefined, - .query = undefined, - }; - defer ctx.deinit(); - - // Keep it simple - try ctx.res.headers.append("Connection", "close"); - - // Wait for the request to be fully read - try ctx.res.wait(); - const uri = try std.Uri.parseWithoutScheme(ctx.res.request.target); - ctx.path = uri.path; - ctx.query = uri.query; - - defer { - if (ctx.res.state == .waited) ctx.res.send() catch {}; - ctx.res.finish() catch {}; - log.debug("{s} {s} {}", .{ @tagName(ctx.res.request.method), ctx.res.request.target, @intFromEnum(ctx.res.status) }); - } - - handleRequest(&ctx) catch |e| { - if (e == error.OutOfMemory) return e; - - log.debug("handleRequest: {}", .{e}); - - ctx.res.status = switch (e) { - error.NotFound => .not_found, - else => .internal_server_error, - }; - - ctx.sendJson(.{ .@"error" = e }) catch {}; - }; - } - - fn handleRequest(ctx: *Context) anyerror!void { - // handle API requests - if (ctx.match("/api/*")) |_| { - ctx.path = ctx.path[4..]; - return ctx.router(api); - } - - // TODO: should be .get() but it's not implemented yet - if (ctx.match("/LICENSE.md")) |_| return ctx.sendResource("LICENSE.md"); - if (ctx.match("/favicon.ico")) |_| return ctx.sendResource("src/app/favicon.ico"); - if (ctx.match("/app.js")) |_| return ctx.sendResource("zig-out/app/main.js"); - - // disable source maps in production - if (ctx.match("*.map")) |_| return ctx.sendChunk("{}"); - - // HTML5 fallback - try ctx.sendResource("src/app/index.html"); - } -}; - -const Params = struct { - matches: [16][]const u8 = undefined, - len: usize = 0, - - fn match(pattern: []const u8, path: []const u8) ?Params { - var res = Params{}; - var pattern_parts = std.mem.tokenizeScalar(u8, pattern, '/'); - var path_parts = std.mem.tokenizeScalar(u8, path, '/'); - - while (true) { - const pat = pattern_parts.next() orelse return if (pattern[pattern.len - 1] == '*' or path_parts.next() == null) res else null; - const pth = path_parts.next() orelse return null; - const dynamic = pat[0] == ':' or pat[0] == '*'; - - if (std.mem.indexOfScalar(u8, pat, '.')) |i| { - const j = (if (dynamic) std.mem.lastIndexOfScalar(u8, pth, '.') else std.mem.indexOfScalar(u8, pth, '.')) orelse return null; - - if (match(pat[i + 1 ..], pth[j + 1 ..])) |ch| { - for (ch.matches, res.len..) |s, l| res.matches[l] = s; - res.len += ch.len; - } else return null; - } - - if (!dynamic and !std.mem.eql(u8, pat, pth)) return null; - - if (pat[0] == ':') { - res.matches[res.len] = pth; - res.len += 1; - } - } - } - - fn get(self: *const Params, index: usize, comptime T: type) !T { - const s = if (index < self.len) self.matches[index] else return error.NoMatch; - - return switch (@typeInfo(T)) { - .Int => std.fmt.parseInt(T, s, 10), - else => s, - }; - } -}; - -fn mime(comptime ext: []const u8) []const u8 { - const mime_types = std.ComptimeStringMap([]const u8, .{ - .{ ".html", "text/html" }, - .{ ".css", "text/css" }, - .{ ".js", "text/javascript" }, - .{ ".md", "text/markdown" }, - }); - - return mime_types.get(ext) orelse "application/octet-stream"; -} diff --git a/src/util.zig b/src/util.zig deleted file mode 100644 index bd081e1..0000000 --- a/src/util.zig +++ /dev/null @@ -1,82 +0,0 @@ -const builtin = @import("builtin"); -const std = @import("std"); - -pub fn getHome(allocator: std.mem.Allocator) ![]const u8 { - return std.fs.getAppDataDir(allocator, "AvaPLS"); -} - -pub fn getHomePath(allocator: std.mem.Allocator, paths: []const []const u8) ![:0]const u8 { - const home = try getHome(allocator); - defer allocator.free(home); - - const arr = try allocator.alloc([]const u8, paths.len + 1); - defer allocator.free(arr); - arr[0] = home; - for (paths, 1..) |p, i| arr[i] = p; - - return std.fs.path.joinZ(allocator, arr); -} - -pub fn getWritableHomePath(allocator: std.mem.Allocator, paths: []const []const u8) ![:0]const u8 { - const res = try getHomePath(allocator, paths); - std.fs.makeDirAbsolute(std.fs.path.dirname(res).?) catch {}; - - return res; -} - -pub fn getFileSize(path: []const u8) !u64 { - const file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only }); - defer file.close(); - - return (try file.stat()).size; -} - -pub const Logger = struct { - var mutex: std.Thread.Mutex = .{}; - var file: ?std.fs.File = null; - - const PATH = &.{"log.txt"}; - - fn writer() std.fs.File.Writer { - if (file == null) { - if (comptime builtin.mode == .Debug) { - file = std.io.getStdErr(); - } else { - const path = getWritableHomePath(std.heap.page_allocator, PATH) catch @panic("Failed to get log path"); - var f = std.fs.createFileAbsolute(path, .{ .read = true }) catch @panic("Failed to open log file"); - f.setEndPos(0) catch {}; - - file = f; - } - } - - return file.?.writer(); - } - - pub fn log( - comptime level: std.log.Level, - comptime scope: @Type(.EnumLiteral), - comptime format: []const u8, - args: anytype, - ) void { - mutex.lock(); - defer mutex.unlock(); - - const t = @mod(@as(u64, @intCast(std.time.timestamp())), 86_400); - const s = @mod(t, 60); - const m = @divTrunc(@mod(t, 3_600), 60); - const h = @divTrunc(t, 3_600); - - writer().print("{:0>2}:{:0>2}:{:0>2} " ++ level.asText() ++ " " ++ @tagName(scope) ++ ": " ++ format ++ "\n", .{ h, m, s } ++ args) catch return; - } - - pub fn dump(allocator: std.mem.Allocator) ![]const u8 { - const path = try getHomePath(allocator, PATH); - defer allocator.free(path); - - var f = try std.fs.openFileAbsolute(path, .{}); - defer f.close(); - - return f.readToEndAlloc(allocator, std.math.maxInt(usize)); - } -}; diff --git a/src/windows/BuildWindows.zig b/src/windows/BuildWindows.zig deleted file mode 100644 index 2b0f6bd..0000000 --- a/src/windows/BuildWindows.zig +++ /dev/null @@ -1,39 +0,0 @@ -const std = @import("std"); - -pub const Options = struct { - name: []const u8, - root_source_file: std.Build.LazyPath, - target: std.Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, - strip: bool, -}; - -pub fn create(owner: *std.Build, options: Options) *std.Build.Step.Compile { - const download_deps = owner.addSystemCommand(&.{ "bash", "src/windows/download_deps.sh" }); - download_deps.has_side_effects = true; - - const exe = owner.addExecutable(.{ - .name = options.name, - .root_source_file = options.root_source_file, - .target = options.target, - .optimize = options.optimize, - .strip = options.strip, - .win32_manifest = .{ .path = "src/windows/winmain.manifest" }, - }); - - if (options.optimize != .Debug) { - exe.subsystem = .Windows; - } - - exe.addIncludePath(.{ .path = "include" }); - exe.linkSystemLibrary("ole32"); - exe.linkSystemLibrary("ws2_32"); - exe.linkSystemLibrary("crypt32"); - - exe.addLibraryPath(.{ .path = "zig-out/webview2_loader/x64" }); - exe.linkSystemLibrary("WebView2Loader.dll"); - - exe.step.dependOn(&download_deps.step); - - return exe; -} diff --git a/src/windows/amazon1.cer b/src/windows/amazon1.cer deleted file mode 100644 index 86b7dcd..0000000 Binary files a/src/windows/amazon1.cer and /dev/null differ diff --git a/src/windows/com.zig b/src/windows/com.zig deleted file mode 100644 index f2a183d..0000000 --- a/src/windows/com.zig +++ /dev/null @@ -1,196 +0,0 @@ -const std = @import("std"); -const c = struct { - usingnamespace std.os.windows; - - extern "ole32" fn CoTaskMemFree(pv: c.LPVOID) callconv(c.WINAPI) void; - extern "ole32" fn CoUninitialize() callconv(c.WINAPI) void; - extern "ole32" fn CoGetCurrentProcess() callconv(c.WINAPI) c.DWORD; - extern "ole32" fn CoInitialize(pvReserved: ?c.LPVOID) callconv(c.WINAPI) c.HRESULT; - extern "ole32" fn CoInitializeEx(pvReserved: ?c.LPVOID, dwCoInit: c.DWORD) callconv(c.WINAPI) c.HRESULT; -}; - -pub const ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler = Object(extern struct { - Invoke: *const fn (self: *ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, res: c.HRESULT, env: *ICoreWebView2Environment) callconv(c.WINAPI) c.HRESULT, -}); - -pub const ICoreWebView2Environment = Object(extern struct { - CreateCoreWebView2Controller: *const fn (self: *ICoreWebView2Environment, parentWin: c.HWND, handler: *ICoreWebView2CreateCoreWebView2ControllerCompletedHandler) callconv(c.WINAPI) c.HRESULT, -}); - -pub const ICoreWebView2CreateCoreWebView2ControllerCompletedHandler = Object(extern struct { - Invoke: *const fn (self: *ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, res: c.HRESULT, ctrl: *ICoreWebView2Controller) callconv(c.WINAPI) c.HRESULT, -}); - -pub const ICoreWebView2Controller = Object(extern struct { - get_IsVisible: *const anyopaque, - put_IsVisible: *const anyopaque, - get_Bounds: *const anyopaque, - put_Bounds: *const fn (self: *ICoreWebView2Controller, bounds: c.RECT) callconv(c.WINAPI) c.HRESULT, - get_ZoomFactor: *const anyopaque, - put_ZoomFactor: *const anyopaque, - add_ZoomFactorChanged: *const anyopaque, - remove_ZoomFactorChanged: *const anyopaque, - SetBoundsAndZoomFactor: *const anyopaque, - MoveFocus: *const anyopaque, - add_MoveFocusRequested: *const anyopaque, - remove_MoveFocusRequested: *const anyopaque, - add_GotFocus: *const anyopaque, - remove_GotFocus: *const anyopaque, - add_LostFocus: *const anyopaque, - remove_LostFocus: *const anyopaque, - add_AcceleratorKeyPressed: *const anyopaque, - remove_AcceleratorKeyPressed: *const anyopaque, - get_ParentWindow: *const anyopaque, - put_ParentWindow: *const anyopaque, - NotifyParentWindowPositionChanged: *const anyopaque, - Close: *const anyopaque, - get_CoreWebView2: *const fn (self: *ICoreWebView2Controller, webview: **ICoreWebView2) callconv(c.WINAPI) c.HRESULT, -}); - -pub const ICoreWebView2 = Object(extern struct { - get_Settings: *const fn (self: *ICoreWebView2, settings: **ICoreWebView2Settings) callconv(c.WINAPI) c.HRESULT, - get_Source: *const anyopaque, - Navigate: *const fn (self: *ICoreWebView2, uri: [*:0]const u16) callconv(c.WINAPI) c.HRESULT, - NavigateToString: *const anyopaque, - add_NavigationStarting: *const anyopaque, - remove_NavigationStarting: *const anyopaque, - add_ContentLoading: *const anyopaque, - remove_ContentLoading: *const anyopaque, - add_SourceChanged: *const anyopaque, - remove_SourceChanged: *const anyopaque, - add_HistoryChanged: *const anyopaque, - remove_HistoryChanged: *const anyopaque, - add_NavigationCompleted: *const anyopaque, - remove_NavigationCompleted: *const anyopaque, - add_FrameNavigationStarting: *const anyopaque, - remove_FrameNavigationStarting: *const anyopaque, - add_FrameNavigationCompleted: *const anyopaque, - remove_FrameNavigationCompleted: *const anyopaque, - add_ScriptDialogOpening: *const anyopaque, - remove_ScriptDialogOpening: *const anyopaque, - add_PermissionRequested: *const anyopaque, - remove_PermissionRequested: *const anyopaque, - add_ProcessFailed: *const anyopaque, - remove_ProcessFailed: *const anyopaque, - AddScriptToExecuteOnDocumentCreated: *const anyopaque, - RemoveScriptToExecuteOnDocumentCreated: *const anyopaque, - ExecuteScript: *const anyopaque, - CapturePreview: *const anyopaque, - Reload: *const anyopaque, - PostWebMessageAsJson: *const anyopaque, - PostWebMessageAsString: *const anyopaque, - add_WebMessageReceived: *const anyopaque, - remove_WebMessageReceived: *const anyopaque, - CallDevToolsProtocolMethod: *const anyopaque, - get_BrowserProcessId: *const anyopaque, - get_CanGoBack: *const anyopaque, - get_CanGoForward: *const anyopaque, - GoBack: *const anyopaque, - GoForward: *const anyopaque, - GetDevToolsProtocolEventReceiver: *const anyopaque, - Stop: *const anyopaque, - add_NewWindowRequested: *const anyopaque, - remove_NewWindowRequested: *const anyopaque, - add_DocumentTitleChanged: *const anyopaque, - remove_DocumentTitleChanged: *const anyopaque, - get_DocumentTitle: *const anyopaque, - AddHostObjectToScript: *const anyopaque, - RemoveHostObjectFromScript: *const anyopaque, - OpenDevToolsWindow: *const anyopaque, - add_ContainsFullScreenElementChanged: *const anyopaque, - remove_ContainsFullScreenElementChanged: *const anyopaque, - get_ContainsFullScreenElement: *const anyopaque, - add_WebResourceRequested: *const anyopaque, - remove_WebResourceRequested: *const anyopaque, - AddWebResourceRequestedFilter: *const anyopaque, - RemoveWebResourceRequestedFilter: *const anyopaque, - add_WindowCloseRequested: *const anyopaque, - remove_WindowCloseRequested: *const anyopaque, -}); - -pub const ICoreWebView2Settings = Object(extern struct { - get_IsScriptEnabled: *const anyopaque, - put_IsScriptEnabled: *const anyopaque, - get_IsWebMessageEnabled: *const anyopaque, - put_IsWebMessageEnabled: *const anyopaque, - get_AreDefaultScriptDialogsEnabled: *const anyopaque, - put_AreDefaultScriptDialogsEnabled: *const anyopaque, - get_IsStatusBarEnabled: *const anyopaque, - put_IsStatusBarEnabled: *const anyopaque, - get_AreDevToolsEnabled: *const anyopaque, - put_AreDevToolsEnabled: *const fn (self: *ICoreWebView2Settings, value: c.BOOL) callconv(c.WINAPI) c.HRESULT, - get_AreDefaultContextMenusEnabled: *const anyopaque, - put_AreDefaultContextMenusEnabled: *const anyopaque, - get_AreHostObjectsAllowed: *const anyopaque, - put_AreHostObjectsAllowed: *const anyopaque, - get_IsZoomControlEnabled: *const anyopaque, - put_IsZoomControlEnabled: *const anyopaque, - get_IsBuiltInErrorPageEnabled: *const anyopaque, - put_IsBuiltInErrorPageEnabled: *const anyopaque, -}); - -pub fn Object(comptime T: type) type { - return extern struct { - const Self = @This(); - - pub const Inner = T; - - pub const VTable = extern struct { - QueryInterface: *const anyopaque, - AddRef: *const fn (self: *Self) callconv(c.WINAPI) c.ULONG, - Release: *const fn (self: *Self) callconv(c.WINAPI) c.ULONG, - inner: T, - }; - - vtable: *const VTable, - - pub fn AddRef(self: *Self) c.ULONG { - return self.vtable.AddRef(self); - } - - pub fn Release(self: *Self) c.ULONG { - return self.vtable.Release(self); - } - - pub fn call(self: *Self, comptime fun: @TypeOf(.EnumLiteral), args: anytype) c.HRESULT { - const res: c.HRESULT = @call(.auto, @field(self.vtable.inner, @tagName(fun)), .{self} ++ args); - - if (res != c.S_OK) { - std.log.err("Failed to call COM function {s} with error {d}\n", .{ @tagName(fun), res }); - return res; - } - - return res; - } - }; -} - -pub fn Callback(comptime T: type, comptime F: std.meta.FieldType(T.Inner, .Invoke)) *T { - // I think this might be enough, because we only support "static" callbacks - const Helper = struct { - var STATIC: T = .{ - .vtable = &.{ - .QueryInterface = QueryInterface, - .AddRef = AddRef, - .Release = Release, - .inner = .{ .Invoke = F }, - }, - }; - - pub fn QueryInterface(_: *anyopaque, _: *const c.GUID, _: **anyopaque) callconv(c.WINAPI) c.HRESULT { - return c.S_OK; - } - - pub fn AddRef(_: *T) callconv(c.WINAPI) c.ULONG { - return 1; - } - - pub fn Release(_: *T) callconv(c.WINAPI) c.ULONG { - return 1; - } - }; - - return &Helper.STATIC; -} - -extern "ole32" fn CoCreateInstance(rclsid: ?*const c.GUID, pUnkOuter: ?*anyopaque, dwClsContext: c.DWORD, riid: ?*const c.GUID, ppv: **anyopaque) callconv(c.WINAPI) c.HRESULT; diff --git a/src/windows/create_zip.sh b/src/windows/create_zip.sh deleted file mode 100755 index 3edc558..0000000 --- a/src/windows/create_zip.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -ZIG_OUT="$(dirname "$0")/../../zig-out" -ZIP_FILE="$ZIG_OUT/ava_x86_64_$(date +%Y-%m-%d).zip" - -# Clean -rm -rf "$ZIG_OUT" - -# Build JS, x86_64 -npm run build \ -&& zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-windows - -if [ $? -ne 0 ]; then - echo "Build failed" - exit 1; -fi - -# Create zip -zip -j -9 "$ZIP_FILE" "$ZIG_OUT/bin/ava_x86_64.exe" "$ZIG_OUT/webview2_loader/x64/WebView2Loader.dll" - -if [ $? -ne 0 ]; then - echo "Zip failed" - exit 1; -fi - -echo "Created $ZIP_FILE" diff --git a/src/windows/download_deps.sh b/src/windows/download_deps.sh deleted file mode 100755 index 124b160..0000000 --- a/src/windows/download_deps.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -# This script downloads the dependencies for the Windows build. -# It is called automatically by the `zig build` command. - -ZIG_OUT="$(dirname "$0")/../../zig-out" -WEBVIEW_LOADER="$ZIG_OUT/webview2_loader" -SQLITE="$ZIG_OUT/sqlite" - -if (test -d "$WEBVIEW_LOADER"); then - echo "WebView2Loader found in $WEBVIEW_LOADER" -else - echo "Downloading WebView2Loader to $WEBVIEW_LOADER" - mkdir -p $WEBVIEW_LOADER - curl -sSL "https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2" | tar -xf - -C $WEBVIEW_LOADER --strip-components=2 "build/native/include" "build/native/x64" -fi diff --git a/src/windows/util.h b/src/windows/util.h deleted file mode 100644 index e9ee3b9..0000000 --- a/src/windows/util.h +++ /dev/null @@ -1,55 +0,0 @@ -// Do one tick of the message loop -int tick() { - MSG msg; - if (GetMessage(&msg, NULL, 0, 0) > 0) { - TranslateMessage(&msg); - DispatchMessage(&msg); - - return (msg.message == WM_QUIT) ? 0 : 1; - } else { - return 0; - } -} - -// Show a message box with an error message -int ShowError(LPCTSTR msg) { - MessageBox(NULL, msg, _T("Error"), NULL); - return 1; -} - -// COM wrapper for a callback -template -class Callback : public T { -public: - Callback(std::function fun): m_fun(fun) {} - - STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { - // TODO: check riid - *ppv = static_cast(this); - AddRef(); - return S_OK; - } - - STDMETHODIMP_(ULONG) AddRef() { - return InterlockedIncrement(&m_refCount); - } - - STDMETHODIMP_(ULONG) Release() { - ULONG refCount = InterlockedDecrement(&m_refCount); - if (refCount == 0) delete this; - return refCount; - } - - STDMETHODIMP_(HRESULT) Invoke(A a, B b) { - return m_fun(a, b); - } - - T* Get() { - return static_cast(this); - } - -private: - ULONG m_refCount = 1; - std::function m_fun; -}; - diff --git a/src/windows/winmain.manifest b/src/windows/winmain.manifest deleted file mode 100644 index 352c226..0000000 --- a/src/windows/winmain.manifest +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - system, unaware - - - diff --git a/src/windows/winmain.zig b/src/windows/winmain.zig deleted file mode 100644 index a39b555..0000000 --- a/src/windows/winmain.zig +++ /dev/null @@ -1,217 +0,0 @@ -const std = @import("std"); -const com = @import("com.zig"); -const util = @import("../util.zig"); -const L = std.unicode.utf8ToUtf16LeStringLiteral; -const c = struct { - usingnamespace std.os.windows; - usingnamespace std.os.windows.kernel32; - - usingnamespace @cImport({ - @cInclude("ava.h"); - }); - - const WNDCLASSEXW = extern struct { cbSize: c.UINT = @sizeOf(WNDCLASSEXW), style: c.UINT, lpfnWndProc: WNDPROC, cbClsExtra: i32 = 0, cbWndExtra: i32 = 0, hInstance: c.HINSTANCE, hIcon: ?c.HICON, hCursor: ?c.HCURSOR, hbrBackground: ?c.HBRUSH, lpszMenuName: ?[*:0]const u16, lpszClassName: [*:0]const u16, hIconSm: ?c.HICON }; - const MSG = extern struct { hWnd: ?c.HWND, message: c.UINT, wParam: c.WPARAM, lParam: c.LPARAM, time: c.DWORD, pt: c.POINT, lPrivate: c.DWORD }; - const WNDPROC = *const fn (hwnd: c.HWND, uMsg: c.UINT, wParam: c.WPARAM, lParam: c.LPARAM) callconv(c.WINAPI) c.LRESULT; - const WS_OVERLAPPEDWINDOW = 0xcf0000; - const CW_USEDEFAULT: i32 = @bitCast(@as(u32, 0x80000000)); - const SW_SHOW = 5; - const WM_QUIT = 0x0012; - const WM_DESTROY = 0x0002; - const WM_SIZE = 0x0005; - const WM_PAINT = 0x000F; - const MB_OK = 0x00000000; - const PAINTSTRUCT = extern struct { hdc: ?c.HDC, fErase: c.BOOL, rcPaint: c.RECT, fRestore: c.BOOL, fIncUpdate: c.BOOL, rgbReserved: [32]u8 }; - extern "user32" fn PostQuitMessage(nExitCode: i32) callconv(c.WINAPI) void; - extern "user32" fn MessageBoxA(hWnd: ?c.HWND, lpText: [*:0]const u8, lpCaption: [*:0]const u8, uType: c.UINT) callconv(c.WINAPI) c.INT; - extern "user32" fn DefWindowProcW(hWnd: c.HWND, Msg: c.UINT, wParam: c.WPARAM, lParam: c.LPARAM) callconv(c.WINAPI) c.LRESULT; - extern "user32" fn GetMessageW(lpMsg: *c.MSG, hWnd: ?c.HWND, wMsgFilterMin: c.UINT, wMsgFilterMax: c.UINT) callconv(c.WINAPI) c.BOOL; - extern "user32" fn TranslateMessage(lpMsg: *c.MSG) callconv(c.WINAPI) c.BOOL; - extern "user32" fn DispatchMessageW(lpMsg: *c.MSG) callconv(c.WINAPI) c.LRESULT; - extern "user32" fn RegisterClassExW(*const WNDCLASSEXW) callconv(c.WINAPI) c.ATOM; - extern "user32" fn CreateWindowExW(dwExStyle: c.DWORD, lpClassName: [*:0]const u16, lpWindowName: [*:0]const u16, dwStyle: c.DWORD, X: i32, Y: i32, nWidth: i32, nHeight: i32, hWindParent: ?c.HWND, hMenu: ?c.HMENU, hInstance: c.HINSTANCE, lpParam: ?c.LPVOID) callconv(c.WINAPI) ?c.HWND; - extern "user32" fn ShowWindow(hWnd: c.HWND, nCmdShow: i32) callconv(c.WINAPI) c.BOOL; - extern "user32" fn UpdateWindow(hWnd: c.HWND) callconv(c.WINAPI) c.BOOL; - extern "user32" fn GetClientRect(hWnd: ?c.HWND, lpRect: ?*c.RECT) callconv(c.WINAPI) c.BOOL; - extern "user32" fn GetUpdateRect(hWnd: ?c.HWND, lpRect: ?*c.RECT, erase: c.BOOL) callconv(c.WINAPI) c.BOOL; - extern "user32" fn BeginPaint(hWnd: ?c.HWND, lpPaint: ?*c.PAINTSTRUCT) callconv(c.WINAPI) c.HDC; - extern "user32" fn DrawTextA(hdc: ?c.HDC, lpchText: [*:0]const u8, cchText: i32, lprc: ?*c.RECT, format: u32) callconv(c.WINAPI) i32; - extern "user32" fn EndPaint(hWnd: ?c.HWND, lpPaint: ?*c.PAINTSTRUCT) callconv(c.WINAPI) c.BOOL; - extern "WebView2Loader.dll" fn CreateCoreWebView2EnvironmentWithOptions(browser_folder: ?c.PCWSTR, data_folder: ?c.PCWSTR, options: ?*anyopaque, handler: *com.ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler) callconv(c.WINAPI) c.HRESULT; -}; -const allocator = std.heap.page_allocator; - -// Globals -var window: c.HWND = undefined; -var webview: *com.ICoreWebView2 = undefined; -var controller: *com.ICoreWebView2Controller = undefined; -var webview_initialized = std.atomic.Value(bool).init(false); - -pub fn main() !u8 { - errdefer |e| showError(e); - - std.log.debug("Starting the server", .{}); - if (c.ava_start() > 0) return error.FailedToStartServer; - - std.log.debug("Creating the window", .{}); - try createWindow(); - - std.log.debug("Creating the webview", .{}); - try createWebView(); - - std.log.debug("Navigating to the webui", .{}); - // TODO: ava_port() - _ = webview.call(.Navigate, .{L("http://127.0.0.1:3002")}); - - std.log.debug("Entering the main loop", .{}); - while (tick() > 0) {} - - std.log.debug("Stopping the server", .{}); - _ = c.ava_stop(); - - std.log.debug("Releasing COM objects", .{}); - _ = webview.Release(); - _ = controller.Release(); - return 0; -} - -fn createWindow() !void { - const CLASS_NAME = L("AvaPLS"); - const TITLE = L("Ava PLS"); - - var wc = std.mem.zeroes(c.WNDCLASSEXW); - wc.cbSize = @sizeOf(c.WNDCLASSEXW); - wc.hInstance = @ptrCast(c.GetModuleHandleW(null) orelse return error.FailedToGetModuleHandle); - wc.lpszClassName = CLASS_NAME; - wc.lpfnWndProc = handleMessage; - - if (c.RegisterClassExW(&wc) == 0) { - return error.FailedToRegisterWindowClass; - } - - const hWnd = c.CreateWindowExW( - 0, - CLASS_NAME, - TITLE, - c.WS_OVERLAPPEDWINDOW, - c.CW_USEDEFAULT, - c.CW_USEDEFAULT, - c.CW_USEDEFAULT, - c.CW_USEDEFAULT, - null, - null, - wc.hInstance, - null, - ) orelse return error.FailedToCreateWindow; - - _ = c.ShowWindow(hWnd, c.SW_SHOW); - _ = c.UpdateWindow(hWnd); - - window = hWnd; -} - -// Window handler -fn handleMessage(hWnd: c.HWND, message: c.UINT, wParam: c.WPARAM, lParam: c.LPARAM) callconv(c.WINAPI) c.LRESULT { - switch (message) { - c.WM_QUIT => return 0, - c.WM_DESTROY => { - c.PostQuitMessage(0); - return 0; - }, - c.WM_SIZE => resize(), - c.WM_PAINT => { - if (!webview_initialized.load(.Acquire)) { - var rect: c.RECT = undefined; - if (c.GetUpdateRect(window, &rect, c.FALSE) == 0) return 0; - - var ps: c.PAINTSTRUCT = undefined; - const dc = c.BeginPaint(window, &ps); - _ = c.GetClientRect(window, &rect); - _ = c.DrawTextA(dc, "Loading...", -1, &rect, 1 | 4 | 32); - _ = c.EndPaint(window, &ps); - return 0; - } - }, - else => {}, - } - - return c.DefWindowProcW(hWnd, message, wParam, lParam); -} - -fn createWebView() !void { - const data_folder = try util.getWritableHomePath(allocator, &.{"webview"}); - defer allocator.free(data_folder); - - const data_folder_w = try std.unicode.utf8ToUtf16LeWithNull(allocator, data_folder); - defer allocator.free(data_folder_w); - - _ = c.CreateCoreWebView2EnvironmentWithOptions( - null, - data_folder_w.ptr, - null, - com.Callback(com.ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, environmentCompleted), - ); - - // Wait for the environment to be created - while (!webview_initialized.load(.Acquire) and tick() > 0) {} -} - -fn environmentCompleted(_: *com.ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, res: c.HRESULT, env: *com.ICoreWebView2Environment) callconv(c.WINAPI) c.HRESULT { - if (res != c.S_OK) { - showError(error.FailedToCreateWebViewEnvironment); - return res; - } - - return env.call(.CreateCoreWebView2Controller, .{ window, com.Callback(com.ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, controllerCompleted) }); -} - -fn controllerCompleted(_: *com.ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, res: c.HRESULT, ctrl: *com.ICoreWebView2Controller) callconv(c.WINAPI) c.HRESULT { - if (res == c.S_OK) { - controller = ctrl; - - if (ctrl.call(.get_CoreWebView2, .{&webview}) == c.S_OK) { - _ = ctrl.AddRef(); - _ = webview.AddRef(); - - // Disable dev tools - var settings: *com.ICoreWebView2Settings = undefined; - _ = webview.call(.get_Settings, .{&settings}); - _ = settings.call(.put_AreDevToolsEnabled, .{c.FALSE}); - } - } - - webview_initialized.store(true, .Release); - - std.log.debug("Resizing", .{}); - resize(); - - return res; -} - -fn resize() void { - if (!webview_initialized.load(.Acquire)) return; - var bounds: c.RECT = undefined; - _ = c.GetClientRect(window, &bounds); - _ = controller.call(.put_Bounds, .{bounds}); -} - -// Do one tick of the message loop -fn tick() c.LRESULT { - var msg: c.MSG = undefined; - - if (c.GetMessageW(&msg, null, 0, 0) >= 0) { - _ = c.TranslateMessage(&msg); - const res = c.DispatchMessageW(&msg); - - return if (msg.message == c.WM_QUIT) res else 1; - } - - return 0; -} - -// Show a message box with an error message -fn showError(err: anytype) void { - const msg = std.fmt.allocPrintZ(std.heap.page_allocator, "Unexpected error: {any}", .{err}) catch "OOM"; - - _ = c.MessageBoxA(null, msg, "Error", c.MB_OK); -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 94c3218..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "ESNext", - "lib": ["ES2023", "DOM", "DOM.Iterable"], - "skipLibCheck": true, - "allowSyntheticDefaultImports": true, - "moduleResolution": "bundler", - "noEmit": true, - "jsx": "react-jsx", - "jsxImportSource": "preact", - "strictNullChecks": true - }, - "include": ["src/app"] -}