mirror of
https://github.com/ritonioz/minecraft-ai-companion-mod-EMVs12-Project.git
synced 2026-06-20 12:15:01 +02:00
Added self-hosting
This commit is contained in:
@@ -6,7 +6,7 @@ minecraft_version=1.20.1
|
|||||||
yarn_mappings=1.20.1+build.10
|
yarn_mappings=1.20.1+build.10
|
||||||
loader_version=0.18.4
|
loader_version=0.18.4
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version=1.0-SNAPSHOT
|
mod_version=1.0.1+1.20.1
|
||||||
maven_group=AiCompanion
|
maven_group=AiCompanion
|
||||||
archives_base_name=aicompanion2-0
|
archives_base_name=aicompanion2-0
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ public class AiChatSession {
|
|||||||
private final String apiBaseUrl;
|
private final String apiBaseUrl;
|
||||||
private final String apiKey;
|
private final String apiKey;
|
||||||
private final String model;
|
private final String model;
|
||||||
|
private final String apiPath; // null = auto-detect
|
||||||
|
|
||||||
// OpenAI-format message history: alternating user/assistant
|
// OpenAI-format message history: alternating user/assistant
|
||||||
private final List<String[]> history = new ArrayList<>(); // [role, content]
|
private final List<String[]> history = new ArrayList<>(); // [role, content]
|
||||||
private final List<String> displayLines = new ArrayList<>();
|
private final List<String> displayLines = new ArrayList<>();
|
||||||
|
|
||||||
public AiChatSession(String apiBaseUrl, String apiKey, String model) {
|
public AiChatSession(String apiBaseUrl, String apiKey, String model, String apiPath) {
|
||||||
this.apiBaseUrl = apiBaseUrl;
|
this.apiBaseUrl = apiBaseUrl;
|
||||||
this.apiKey = apiKey;
|
this.apiKey = apiKey;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
@@ -62,6 +63,15 @@ public class AiChatSession {
|
|||||||
messages.append("]");
|
messages.append("]");
|
||||||
|
|
||||||
String json = "{\"model\":\"" + jsonEscape(model) + "\",\"messages\":" + messages + ",\"stream\":false}";
|
String json = "{\"model\":\"" + jsonEscape(model) + "\",\"messages\":" + messages + ",\"stream\":false}";
|
||||||
|
|
||||||
|
if (apiPath != null) {
|
||||||
|
// User configured a specific path — use it directly, no fallback
|
||||||
|
HttpResult result = postChatCompletion(apiPath, json);
|
||||||
|
if (result.status == 200) return extractAssistantContent(result.body);
|
||||||
|
return formatHttpError(result.status, result.body, apiPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect: try /api/chat/completions, fall back to /v1/chat/completions
|
||||||
HttpResult primary = postChatCompletion("/api/chat/completions", json);
|
HttpResult primary = postChatCompletion("/api/chat/completions", json);
|
||||||
if (primary.status == 200) {
|
if (primary.status == 200) {
|
||||||
return extractAssistantContent(primary.body);
|
return extractAssistantContent(primary.body);
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package AiCompanion.aicompanion2_0.client;
|
|||||||
|
|
||||||
import AiCompanion.aicompanion2_0.AIEntity;
|
import AiCompanion.aicompanion2_0.AIEntity;
|
||||||
import AiCompanion.aicompanion2_0.Aicompanion2_0;
|
import AiCompanion.aicompanion2_0.Aicompanion2_0;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
import net.fabricmc.api.ClientModInitializer;
|
import net.fabricmc.api.ClientModInitializer;
|
||||||
|
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
|
||||||
|
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||||
import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry;
|
import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry;
|
||||||
@@ -177,9 +180,10 @@ public class AiCompanionClient implements ClientModInitializer {
|
|||||||
private static void openChatScreen(MinecraftClient client) {
|
private static void openChatScreen(MinecraftClient client) {
|
||||||
if (currentSession == null) {
|
if (currentSession == null) {
|
||||||
currentSession = new AiChatSession(
|
currentSession = new AiChatSession(
|
||||||
Aicompanion2_0.getApiBaseUrl(),
|
ClientConfig.getBaseUrl(),
|
||||||
ClientConfig.getApiKey(),
|
ClientConfig.getApiKey(),
|
||||||
Aicompanion2_0.getModel()
|
ClientConfig.getModel(),
|
||||||
|
ClientConfig.getApiPath()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
client.setScreen(new AiChatScreen(currentSession));
|
client.setScreen(new AiChatScreen(currentSession));
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ public class ClientConfig {
|
|||||||
|
|
||||||
private static final Path CONFIG_PATH = Path.of("config", "aicompanion2_0_client.properties");
|
private static final Path CONFIG_PATH = Path.of("config", "aicompanion2_0_client.properties");
|
||||||
private static final Path SHARED_CONFIG_PATH = Path.of("config", "aicompanion2_0.properties");
|
private static final Path SHARED_CONFIG_PATH = Path.of("config", "aicompanion2_0.properties");
|
||||||
|
|
||||||
private static String apiKey = null;
|
private static String apiKey = null;
|
||||||
|
private static String baseUrl = null;
|
||||||
|
private static String model = null;
|
||||||
|
private static String apiPath = null;
|
||||||
|
private static boolean loaded = false;
|
||||||
|
|
||||||
public static String getApiKey() {
|
public static String getApiKey() {
|
||||||
load();
|
load();
|
||||||
@@ -32,12 +37,53 @@ public class ClientConfig {
|
|||||||
return apiKey != null && !apiKey.isBlank();
|
return apiKey != null && !apiKey.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getBaseUrl() {
|
||||||
|
if (!loaded) load();
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getModel() {
|
||||||
|
if (!loaded) load();
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getApiPath() {
|
||||||
|
if (!loaded) load();
|
||||||
|
return apiPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSetupDone() {
|
||||||
|
if (!loaded) load();
|
||||||
|
return baseUrl != null && !baseUrl.isBlank() && model != null && !model.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setProviderConfig(String url, String mdl, String key, String path) {
|
||||||
|
baseUrl = url;
|
||||||
|
model = mdl;
|
||||||
|
apiKey = key != null ? key : "";
|
||||||
|
apiPath = (path != null && !path.isBlank()) ? path : null;
|
||||||
|
loaded = true;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
private static void load() {
|
private static void load() {
|
||||||
|
loaded = true;
|
||||||
try {
|
try {
|
||||||
// Prefer shared config so GUI and /ai frage use the same key source.
|
apiKey = loadProp(SHARED_CONFIG_PATH, "api.key");
|
||||||
apiKey = loadApiKeyFrom(SHARED_CONFIG_PATH);
|
|
||||||
if (apiKey == null || apiKey.isBlank()) {
|
if (apiKey == null || apiKey.isBlank()) {
|
||||||
apiKey = loadApiKeyFrom(CONFIG_PATH);
|
apiKey = loadProp(CONFIG_PATH, "api.key");
|
||||||
|
}
|
||||||
|
baseUrl = loadProp(SHARED_CONFIG_PATH, "api.baseUrl");
|
||||||
|
if (baseUrl == null || baseUrl.isBlank()) {
|
||||||
|
baseUrl = loadProp(CONFIG_PATH, "api.baseUrl");
|
||||||
|
}
|
||||||
|
model = loadProp(SHARED_CONFIG_PATH, "api.model");
|
||||||
|
if (model == null || model.isBlank()) {
|
||||||
|
model = loadProp(CONFIG_PATH, "api.model");
|
||||||
|
}
|
||||||
|
apiPath = loadProp(SHARED_CONFIG_PATH, "api.path");
|
||||||
|
if (apiPath == null || apiPath.isBlank()) {
|
||||||
|
apiPath = loadProp(CONFIG_PATH, "api.path");
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
apiKey = null;
|
apiKey = null;
|
||||||
@@ -60,10 +106,19 @@ public class ClientConfig {
|
|||||||
try (FileInputStream in = new FileInputStream(path.toFile())) {
|
try (FileInputStream in = new FileInputStream(path.toFile())) {
|
||||||
props.load(in);
|
props.load(in);
|
||||||
}
|
}
|
||||||
return props.getProperty("api.key", null);
|
return props.getProperty(key, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void saveApiKeyTo(Path path, String comment) throws IOException {
|
private static void save() {
|
||||||
|
try {
|
||||||
|
saveTo(CONFIG_PATH, "AI Companion Client Config");
|
||||||
|
saveTo(SHARED_CONFIG_PATH, "AI Companion Shared Config");
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println("[aicompanion2_0] Konnte Client-Config nicht speichern.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveTo(Path path, String comment) throws IOException {
|
||||||
Files.createDirectories(path.getParent());
|
Files.createDirectories(path.getParent());
|
||||||
Properties props = new Properties();
|
Properties props = new Properties();
|
||||||
if (Files.exists(path)) {
|
if (Files.exists(path)) {
|
||||||
@@ -72,6 +127,9 @@ public class ClientConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
props.setProperty("api.key", apiKey != null ? apiKey : "");
|
props.setProperty("api.key", apiKey != null ? apiKey : "");
|
||||||
|
if (baseUrl != null) props.setProperty("api.baseUrl", baseUrl);
|
||||||
|
if (model != null) props.setProperty("api.model", model);
|
||||||
|
if (apiPath != null) props.setProperty("api.path", apiPath);
|
||||||
try (FileOutputStream out = new FileOutputStream(path.toFile())) {
|
try (FileOutputStream out = new FileOutputStream(path.toFile())) {
|
||||||
props.store(out, comment);
|
props.store(out, comment);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,360 @@
|
|||||||
|
package AiCompanion.aicompanion2_0.client;
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
|
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||||
|
import net.minecraft.client.gui.widget.TextFieldWidget;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ProviderSetupScreen extends Screen {
|
||||||
|
|
||||||
|
private boolean configStep = false;
|
||||||
|
private boolean advancedExpanded = false;
|
||||||
|
private String provider = "ollama";
|
||||||
|
|
||||||
|
// Field values preserved across reinit() calls
|
||||||
|
private String savedUrl = "";
|
||||||
|
private String savedModel = "";
|
||||||
|
private String savedKey = "";
|
||||||
|
private String savedPath = "";
|
||||||
|
|
||||||
|
// Model fetch state
|
||||||
|
private enum FetchState { IDLE, LOADING, SUCCESS, ERROR }
|
||||||
|
private FetchState fetchState = FetchState.IDLE;
|
||||||
|
private List<String> fetchedModels = new ArrayList<>();
|
||||||
|
private String fetchError = null;
|
||||||
|
private int modelScrollOffset = 0;
|
||||||
|
|
||||||
|
private TextFieldWidget urlField;
|
||||||
|
private TextFieldWidget modelField;
|
||||||
|
private TextFieldWidget keyField;
|
||||||
|
private TextFieldWidget pathField;
|
||||||
|
|
||||||
|
private final Runnable onSuccess;
|
||||||
|
|
||||||
|
// Layout constants (relative to baseY)
|
||||||
|
private static final int URL_FIELD_Y = 10;
|
||||||
|
private static final int KEY_LABEL_Y = 37;
|
||||||
|
private static final int KEY_FIELD_Y = 47;
|
||||||
|
private static final int MODEL_LABEL_Y = 74;
|
||||||
|
private static final int MODEL_CONTENT_Y = 86;
|
||||||
|
private static final int ITEM_HEIGHT = 14;
|
||||||
|
private static final int MAX_VISIBLE = 4;
|
||||||
|
|
||||||
|
public ProviderSetupScreen(Runnable onSuccess) {
|
||||||
|
super(Text.literal("AI Companion Setup"));
|
||||||
|
this.onSuccess = onSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
int cx = width / 2;
|
||||||
|
int baseY = height / 2 - 75;
|
||||||
|
|
||||||
|
if (!configStep) {
|
||||||
|
int cy = height / 2;
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("Ollama (Local)"), btn -> goToConfig("ollama"))
|
||||||
|
.dimensions(cx - 155, cy, 100, 20).build());
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("Open-WebUI"), btn -> goToConfig("openwebui"))
|
||||||
|
.dimensions(cx - 50, cy, 100, 20).build());
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("Custom"), btn -> goToConfig("custom"))
|
||||||
|
.dimensions(cx + 55, cy, 100, 20).build());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Config step ---
|
||||||
|
|
||||||
|
// URL field
|
||||||
|
urlField = new TextFieldWidget(textRenderer, cx - 150, baseY + URL_FIELD_Y, 300, 20, Text.literal("http://..."));
|
||||||
|
urlField.setMaxLength(200);
|
||||||
|
urlField.setText(savedUrl.isEmpty() ? defaultUrl() : savedUrl);
|
||||||
|
addDrawableChild(urlField);
|
||||||
|
|
||||||
|
// Key field
|
||||||
|
keyField = new TextFieldWidget(textRenderer, cx - 150, baseY + KEY_FIELD_Y, 300, 20, Text.literal("API Key (optional)"));
|
||||||
|
keyField.setMaxLength(200);
|
||||||
|
keyField.setText(savedKey);
|
||||||
|
addDrawableChild(keyField);
|
||||||
|
|
||||||
|
// Fetch Models button (right side of model row header)
|
||||||
|
String fetchLabel = switch (fetchState) {
|
||||||
|
case LOADING -> "§7Fetching...";
|
||||||
|
case SUCCESS -> "↻ Refetch";
|
||||||
|
case ERROR -> "↻ Retry";
|
||||||
|
default -> "Fetch Models";
|
||||||
|
};
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal(fetchLabel), btn -> {
|
||||||
|
if (fetchState != FetchState.LOADING) {
|
||||||
|
saveCurrentValues();
|
||||||
|
fetchModels();
|
||||||
|
}
|
||||||
|
}).dimensions(cx + 5, baseY + MODEL_LABEL_Y, 145, 12).build());
|
||||||
|
|
||||||
|
// Model content area: clickable list OR text field
|
||||||
|
boolean hasModelList = fetchState == FetchState.SUCCESS && !fetchedModels.isEmpty();
|
||||||
|
int listHeight;
|
||||||
|
|
||||||
|
if (hasModelList) {
|
||||||
|
int visibleCount = Math.min(fetchedModels.size(), MAX_VISIBLE);
|
||||||
|
int endIdx = Math.min(fetchedModels.size(), modelScrollOffset + visibleCount);
|
||||||
|
listHeight = visibleCount * ITEM_HEIGHT;
|
||||||
|
|
||||||
|
for (int i = modelScrollOffset; i < endIdx; i++) {
|
||||||
|
String m = fetchedModels.get(i);
|
||||||
|
boolean selected = m.equals(savedModel);
|
||||||
|
final String model = m;
|
||||||
|
addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.literal((selected ? "§a▶ " : " ") + m),
|
||||||
|
btn -> { savedModel = model; reinit(); }
|
||||||
|
).dimensions(cx - 150, baseY + MODEL_CONTENT_Y + (i - modelScrollOffset) * ITEM_HEIGHT, 290, ITEM_HEIGHT - 2).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up/down scroll when list is longer than visible
|
||||||
|
if (fetchedModels.size() > MAX_VISIBLE) {
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("▲"),
|
||||||
|
btn -> { modelScrollOffset = Math.max(0, modelScrollOffset - 1); reinit(); }
|
||||||
|
).dimensions(cx + 143, baseY + MODEL_CONTENT_Y, 7, ITEM_HEIGHT - 2).build());
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("▼"),
|
||||||
|
btn -> { modelScrollOffset = Math.min(fetchedModels.size() - MAX_VISIBLE, modelScrollOffset + 1); reinit(); }
|
||||||
|
).dimensions(cx + 143, baseY + MODEL_CONTENT_Y + ITEM_HEIGHT, 7, ITEM_HEIGHT - 2).build());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Manual text input fallback
|
||||||
|
modelField = new TextFieldWidget(textRenderer, cx - 150, baseY + MODEL_CONTENT_Y, 300, 20, Text.literal("Model name..."));
|
||||||
|
modelField.setMaxLength(100);
|
||||||
|
modelField.setText(savedModel.isEmpty() ? defaultModel() : savedModel);
|
||||||
|
addDrawableChild(modelField);
|
||||||
|
listHeight = 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positions below model area
|
||||||
|
int afterModelY = baseY + MODEL_CONTENT_Y + listHeight + 5;
|
||||||
|
|
||||||
|
// Advanced toggle
|
||||||
|
String advLabel = advancedExpanded ? "Advanced ▲" : "Advanced ▼";
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("§8" + advLabel), btn -> {
|
||||||
|
saveCurrentValues();
|
||||||
|
advancedExpanded = !advancedExpanded;
|
||||||
|
reinit();
|
||||||
|
}).dimensions(cx + 55, afterModelY, 95, 12).build());
|
||||||
|
|
||||||
|
int buttonsY;
|
||||||
|
if (advancedExpanded) {
|
||||||
|
pathField = new TextFieldWidget(textRenderer, cx - 150, afterModelY + 18, 300, 20, Text.literal("/api/chat/completions"));
|
||||||
|
pathField.setMaxLength(200);
|
||||||
|
pathField.setText(savedPath.isEmpty() ? defaultPath() : savedPath);
|
||||||
|
addDrawableChild(pathField);
|
||||||
|
buttonsY = afterModelY + 45;
|
||||||
|
} else {
|
||||||
|
buttonsY = afterModelY + 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("< Back"), btn -> {
|
||||||
|
configStep = false;
|
||||||
|
advancedExpanded = false;
|
||||||
|
fetchState = FetchState.IDLE;
|
||||||
|
fetchedModels = new ArrayList<>();
|
||||||
|
savedUrl = savedModel = savedKey = savedPath = "";
|
||||||
|
reinit();
|
||||||
|
}).dimensions(cx - 155, buttonsY, 70, 20).build());
|
||||||
|
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("Confirm"), btn -> confirm())
|
||||||
|
.dimensions(cx - 75, buttonsY, 150, 20).build());
|
||||||
|
|
||||||
|
urlField.setFocused(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCurrentValues() {
|
||||||
|
if (urlField != null) savedUrl = urlField.getText();
|
||||||
|
if (modelField != null) savedModel = modelField.getText();
|
||||||
|
if (keyField != null) savedKey = keyField.getText();
|
||||||
|
if (pathField != null) savedPath = pathField.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void goToConfig(String p) {
|
||||||
|
this.provider = p;
|
||||||
|
this.configStep = true;
|
||||||
|
reinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reinit() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
this.init(client, client.getWindow().getScaledWidth(), client.getWindow().getScaledHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultUrl() {
|
||||||
|
return switch (provider) {
|
||||||
|
case "ollama" -> "http://localhost:11434";
|
||||||
|
case "openwebui" -> "https://ai.cametendo.org";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultModel() {
|
||||||
|
return switch (provider) {
|
||||||
|
case "openwebui" -> "minecraft-helper";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultPath() {
|
||||||
|
return switch (provider) {
|
||||||
|
case "ollama" -> "/v1/chat/completions";
|
||||||
|
case "openwebui" -> "/api/chat/completions";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Model fetching ---
|
||||||
|
|
||||||
|
private void fetchModels() {
|
||||||
|
fetchState = FetchState.LOADING;
|
||||||
|
fetchError = null;
|
||||||
|
String url = savedUrl.isEmpty() ? defaultUrl() : savedUrl;
|
||||||
|
String key = savedKey;
|
||||||
|
reinit();
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<String> models = doFetchModels(url, key);
|
||||||
|
if (models.isEmpty()) {
|
||||||
|
fetchState = FetchState.ERROR;
|
||||||
|
fetchError = "No models found at " + url;
|
||||||
|
} else {
|
||||||
|
fetchedModels = models;
|
||||||
|
fetchState = FetchState.SUCCESS;
|
||||||
|
modelScrollOffset = 0;
|
||||||
|
if (savedModel.isEmpty()) savedModel = models.get(0);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
fetchState = FetchState.ERROR;
|
||||||
|
fetchError = e.getMessage() != null ? e.getMessage() : "Connection failed";
|
||||||
|
}
|
||||||
|
MinecraftClient.getInstance().execute(this::reinit);
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> doFetchModels(String baseUrl, String apiKey) throws Exception {
|
||||||
|
// Try Ollama native: GET /api/tags → {"models": [{"name": "..."}]}
|
||||||
|
try {
|
||||||
|
List<String> models = fetchFromPath(baseUrl, "/api/tags", apiKey, "name");
|
||||||
|
if (!models.isEmpty()) return models;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// Try OpenAI-compat: GET /v1/models → {"data": [{"id": "..."}]}
|
||||||
|
return fetchFromPath(baseUrl, "/v1/models", apiKey, "id");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> fetchFromPath(String baseUrl, String path, String apiKey, String nameField) throws Exception {
|
||||||
|
URL url = URI.create(baseUrl + path).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
if (apiKey != null && !apiKey.isBlank()) {
|
||||||
|
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = conn.getResponseCode();
|
||||||
|
if (status != 200) throw new Exception("HTTP " + status + " from " + path);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) sb.append(line);
|
||||||
|
}
|
||||||
|
conn.disconnect();
|
||||||
|
|
||||||
|
// Simple field extraction: find all occurrences of "nameField":"value"
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
String search = "\"" + nameField + "\":\"";
|
||||||
|
String body = sb.toString();
|
||||||
|
int idx = 0;
|
||||||
|
while ((idx = body.indexOf(search, idx)) >= 0) {
|
||||||
|
int start = idx + search.length();
|
||||||
|
int end = body.indexOf("\"", start);
|
||||||
|
if (end > start) result.add(body.substring(start, end));
|
||||||
|
idx = end + 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Confirm ---
|
||||||
|
|
||||||
|
private void confirm() {
|
||||||
|
saveCurrentValues();
|
||||||
|
String url = savedUrl.trim();
|
||||||
|
String model = savedModel.trim();
|
||||||
|
String key = savedKey.trim();
|
||||||
|
String path = advancedExpanded ? savedPath.trim() : "";
|
||||||
|
if (url.isEmpty() || model.isEmpty()) return;
|
||||||
|
ClientConfig.setProviderConfig(url, model, key.isEmpty() ? null : key, path.isEmpty() ? null : path);
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
close();
|
||||||
|
onSuccess.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||||
|
if (configStep && (keyCode == 257 || keyCode == 335)) { // Enter
|
||||||
|
confirm();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||||
|
renderBackground(context);
|
||||||
|
int cx = width / 2;
|
||||||
|
int baseY = height / 2 - 75;
|
||||||
|
|
||||||
|
if (!configStep) {
|
||||||
|
int cy = height / 2;
|
||||||
|
context.drawCenteredTextWithShadow(textRenderer, "§6AI Companion Setup", cx, cy - 30, 0xFFFFFF);
|
||||||
|
context.drawCenteredTextWithShadow(textRenderer, "§fChoose your AI provider:", cx, cy - 15, 0xAAAAAA);
|
||||||
|
} else {
|
||||||
|
String title = switch (provider) {
|
||||||
|
case "ollama" -> "§6Configure Ollama";
|
||||||
|
case "openwebui" -> "§6Configure Open-WebUI";
|
||||||
|
default -> "§6Custom Configuration";
|
||||||
|
};
|
||||||
|
context.drawCenteredTextWithShadow(textRenderer, title, cx, baseY - 5, 0xFFFFFF);
|
||||||
|
context.drawTextWithShadow(textRenderer, "§7Server URL:", cx - 150, baseY, 0xAAAAAA);
|
||||||
|
context.drawTextWithShadow(textRenderer, "§7API Key §8(optional):", cx - 150, baseY + KEY_LABEL_Y, 0xAAAAAA);
|
||||||
|
context.drawTextWithShadow(textRenderer, "§7Model:", cx - 150, baseY + MODEL_LABEL_Y, 0xAAAAAA);
|
||||||
|
|
||||||
|
// Fetch status / error below model label
|
||||||
|
if (fetchState == FetchState.LOADING) {
|
||||||
|
context.drawTextWithShadow(textRenderer, "§7Loading...", cx - 150, baseY + MODEL_CONTENT_Y + 4, 0xAAAAAA);
|
||||||
|
} else if (fetchState == FetchState.ERROR && fetchError != null) {
|
||||||
|
context.drawTextWithShadow(textRenderer, "§c" + fetchError, cx - 150, baseY + MODEL_CONTENT_Y + 4, 0xFF5555);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced path label
|
||||||
|
if (advancedExpanded) {
|
||||||
|
boolean hasModelList = fetchState == FetchState.SUCCESS && !fetchedModels.isEmpty();
|
||||||
|
int listHeight = hasModelList ? Math.min(fetchedModels.size(), MAX_VISIBLE) * ITEM_HEIGHT : 22;
|
||||||
|
int afterModelY = baseY + MODEL_CONTENT_Y + listHeight + 5;
|
||||||
|
context.drawTextWithShadow(textRenderer, "§7API Path:", cx - 150, afterModelY + 8, 0xAAAAAA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.render(context, mouseX, mouseY, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldPause() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "aicompanion2_0",
|
"id": "aicompanion2_0",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1+1.20.1",
|
||||||
"name": "AI Companion 2.0",
|
"name": "AI Companion 2.0",
|
||||||
"description": "AI Chatbot powered by Ollama",
|
"description": "AI Chatbot powered by Ollama",
|
||||||
"authors": ["Du"],
|
"authors": ["Du"],
|
||||||
|
|||||||
Reference in New Issue
Block a user