662 lines
17 KiB
C
662 lines
17 KiB
C
#include "tui.h"
|
|
#include "state.h"
|
|
#include "publish.h"
|
|
|
|
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
|
|
|
#include <curl/curl.h>
|
|
#include <ncurses.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
typedef struct {
|
|
char *endpoint;
|
|
char *api_key;
|
|
char *model;
|
|
} ai_prefs_t;
|
|
|
|
typedef struct {
|
|
char *data;
|
|
size_t len;
|
|
} nt_http_buffer_t;
|
|
|
|
static char *ai_strdup(const char *s) {
|
|
size_t n;
|
|
char *out;
|
|
|
|
if (!s) {
|
|
return NULL;
|
|
}
|
|
|
|
n = strlen(s);
|
|
out = (char *)malloc(n + 1U);
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
|
|
memcpy(out, s, n + 1U);
|
|
return out;
|
|
}
|
|
|
|
static void ai_set_string(char **dst, const char *src) {
|
|
char *next;
|
|
|
|
if (!dst) {
|
|
return;
|
|
}
|
|
|
|
next = ai_strdup(src ? src : "");
|
|
if (!next) {
|
|
return;
|
|
}
|
|
|
|
free(*dst);
|
|
*dst = next;
|
|
}
|
|
|
|
static void ai_prefs_init(ai_prefs_t *prefs) {
|
|
if (!prefs) {
|
|
return;
|
|
}
|
|
|
|
memset(prefs, 0, sizeof(*prefs));
|
|
prefs->endpoint = ai_strdup("https://api.venice.ai/api/v1/chat/completions");
|
|
prefs->api_key = ai_strdup("");
|
|
prefs->model = ai_strdup("llama-3.3-70b");
|
|
}
|
|
|
|
static void ai_prefs_free(ai_prefs_t *prefs) {
|
|
if (!prefs) {
|
|
return;
|
|
}
|
|
|
|
free(prefs->endpoint);
|
|
free(prefs->api_key);
|
|
free(prefs->model);
|
|
memset(prefs, 0, sizeof(*prefs));
|
|
}
|
|
|
|
static int ai_ends_with_chat_completions(const char *s) {
|
|
static const char *suffix = "/chat/completions";
|
|
size_t s_len;
|
|
size_t suffix_len;
|
|
|
|
if (!s) {
|
|
return 0;
|
|
}
|
|
|
|
s_len = strlen(s);
|
|
suffix_len = strlen(suffix);
|
|
if (s_len < suffix_len) {
|
|
return 0;
|
|
}
|
|
|
|
return strcmp(s + (s_len - suffix_len), suffix) == 0;
|
|
}
|
|
|
|
static char *ai_join_chat_completions(const char *base_url) {
|
|
size_t base_len;
|
|
int needs_slash;
|
|
char *out;
|
|
|
|
if (!base_url || base_url[0] == '\0') {
|
|
return ai_strdup("https://api.venice.ai/api/v1/chat/completions");
|
|
}
|
|
|
|
if (ai_ends_with_chat_completions(base_url)) {
|
|
return ai_strdup(base_url);
|
|
}
|
|
|
|
base_len = strlen(base_url);
|
|
needs_slash = (base_len > 0U && base_url[base_len - 1U] != '/');
|
|
out = (char *)malloc(base_len + (needs_slash ? 1U : 0U) + strlen("chat/completions") + 1U);
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
|
|
snprintf(out,
|
|
base_len + (needs_slash ? 1U : 0U) + strlen("chat/completions") + 1U,
|
|
"%s%schat/completions",
|
|
base_url,
|
|
needs_slash ? "/" : "");
|
|
return out;
|
|
}
|
|
|
|
static char *ai_base_url_from_endpoint(const char *endpoint) {
|
|
static const char *suffix = "/chat/completions";
|
|
size_t endpoint_len;
|
|
size_t suffix_len;
|
|
char *out;
|
|
|
|
if (!endpoint || endpoint[0] == '\0') {
|
|
return ai_strdup("");
|
|
}
|
|
|
|
endpoint_len = strlen(endpoint);
|
|
suffix_len = strlen(suffix);
|
|
|
|
if (endpoint_len >= suffix_len &&
|
|
strcmp(endpoint + (endpoint_len - suffix_len), suffix) == 0) {
|
|
out = (char *)malloc(endpoint_len - suffix_len + 1U);
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
memcpy(out, endpoint, endpoint_len - suffix_len);
|
|
out[endpoint_len - suffix_len] = '\0';
|
|
return out;
|
|
}
|
|
|
|
return ai_strdup(endpoint);
|
|
}
|
|
|
|
static cJSON *ai_make_d_tag(const char *dval) {
|
|
cJSON *tags = cJSON_CreateArray();
|
|
cJSON *d_tag;
|
|
|
|
if (!tags) {
|
|
return NULL;
|
|
}
|
|
|
|
d_tag = cJSON_CreateArray();
|
|
if (!d_tag) {
|
|
cJSON_Delete(tags);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
|
cJSON_AddItemToArray(d_tag, cJSON_CreateString(dval ? dval : ""));
|
|
cJSON_AddItemToArray(tags, d_tag);
|
|
return tags;
|
|
}
|
|
|
|
static size_t nt_http_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
|
nt_http_buffer_t *buf = (nt_http_buffer_t *)userdata;
|
|
size_t total = size * nmemb;
|
|
char *next;
|
|
|
|
if (!buf || !ptr || total == 0U) {
|
|
return 0;
|
|
}
|
|
|
|
next = (char *)realloc(buf->data, buf->len + total + 1U);
|
|
if (!next) {
|
|
return 0;
|
|
}
|
|
|
|
buf->data = next;
|
|
memcpy(buf->data + buf->len, ptr, total);
|
|
buf->len += total;
|
|
buf->data[buf->len] = '\0';
|
|
return total;
|
|
}
|
|
|
|
static void nt_configure_curl_tls(CURL *curl) {
|
|
const char *cafile_env;
|
|
const char *capath_env;
|
|
static const char *cafile_candidates[] = {
|
|
"/etc/ssl/certs/ca-certificates.crt",
|
|
"/etc/ssl/cert.pem",
|
|
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
|
|
};
|
|
int i;
|
|
|
|
if (!curl) {
|
|
return;
|
|
}
|
|
|
|
cafile_env = getenv("SSL_CERT_FILE");
|
|
if (!cafile_env || cafile_env[0] == '\0') {
|
|
cafile_env = getenv("CURL_CA_BUNDLE");
|
|
}
|
|
if (cafile_env && cafile_env[0] != '\0') {
|
|
curl_easy_setopt(curl, CURLOPT_CAINFO, cafile_env);
|
|
return;
|
|
}
|
|
|
|
capath_env = getenv("SSL_CERT_DIR");
|
|
if (capath_env && capath_env[0] != '\0') {
|
|
curl_easy_setopt(curl, CURLOPT_CAPATH, capath_env);
|
|
}
|
|
|
|
for (i = 0; i < (int)(sizeof(cafile_candidates) / sizeof(cafile_candidates[0])); i++) {
|
|
if (access(cafile_candidates[i], R_OK) == 0) {
|
|
curl_easy_setopt(curl, CURLOPT_CAINFO, cafile_candidates[i]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (access("/etc/ssl/certs", R_OK) == 0) {
|
|
curl_easy_setopt(curl, CURLOPT_CAPATH, "/etc/ssl/certs");
|
|
}
|
|
}
|
|
|
|
|
|
static int ai_load_prefs(ai_prefs_t *prefs) {
|
|
cJSON *root;
|
|
cJSON *llm;
|
|
cJSON *api_key;
|
|
cJSON *model;
|
|
cJSON *base_url;
|
|
cJSON *legacy_endpoint;
|
|
char *endpoint;
|
|
|
|
if (!prefs || !g_state.user_settings_json) {
|
|
return -1;
|
|
}
|
|
|
|
root = cJSON_Parse(g_state.user_settings_json);
|
|
if (!root || !cJSON_IsObject(root)) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
|
|
if (!cJSON_IsObject(llm)) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
api_key = cJSON_GetObjectItemCaseSensitive(llm, "api_key");
|
|
model = cJSON_GetObjectItemCaseSensitive(llm, "model");
|
|
base_url = cJSON_GetObjectItemCaseSensitive(llm, "base_url");
|
|
legacy_endpoint = cJSON_GetObjectItemCaseSensitive(llm, "endpoint");
|
|
|
|
if (cJSON_IsString(api_key) && api_key->valuestring) {
|
|
ai_set_string(&prefs->api_key, api_key->valuestring);
|
|
}
|
|
if (cJSON_IsString(model) && model->valuestring) {
|
|
ai_set_string(&prefs->model, model->valuestring);
|
|
}
|
|
|
|
endpoint = NULL;
|
|
if (cJSON_IsString(base_url) && base_url->valuestring) {
|
|
endpoint = ai_join_chat_completions(base_url->valuestring);
|
|
} else if (cJSON_IsString(legacy_endpoint) && legacy_endpoint->valuestring) {
|
|
endpoint = ai_strdup(legacy_endpoint->valuestring);
|
|
}
|
|
if (endpoint) {
|
|
ai_set_string(&prefs->endpoint, endpoint);
|
|
free(endpoint);
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
static int ai_save_prefs(const ai_prefs_t *prefs) {
|
|
cJSON *root;
|
|
cJSON *llm;
|
|
cJSON *tags;
|
|
char *new_json;
|
|
char *cipher = NULL;
|
|
char *base_url = NULL;
|
|
int posted;
|
|
|
|
if (!prefs) {
|
|
return -1;
|
|
}
|
|
|
|
root = g_state.user_settings_json
|
|
? cJSON_Parse(g_state.user_settings_json)
|
|
: cJSON_CreateObject();
|
|
if (!root || !cJSON_IsObject(root)) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
if (!cJSON_GetObjectItemCaseSensitive(root, "v")) {
|
|
cJSON_AddNumberToObject(root, "v", 2);
|
|
}
|
|
cJSON_DeleteItemFromObjectCaseSensitive(root, "updatedAt");
|
|
cJSON_AddNumberToObject(root, "updatedAt", (double)time(NULL));
|
|
|
|
if (!cJSON_GetObjectItemCaseSensitive(root, "global_zaps")) {
|
|
cJSON_AddObjectToObject(root, "global_zaps");
|
|
}
|
|
if (!cJSON_GetObjectItemCaseSensitive(root, "global_ui")) {
|
|
cJSON_AddObjectToObject(root, "global_ui");
|
|
}
|
|
if (!cJSON_GetObjectItemCaseSensitive(root, "global_relays")) {
|
|
cJSON_AddObjectToObject(root, "global_relays");
|
|
}
|
|
if (!cJSON_GetObjectItemCaseSensitive(root, "global_experimental")) {
|
|
cJSON_AddObjectToObject(root, "global_experimental");
|
|
}
|
|
|
|
llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
|
|
if (!cJSON_IsObject(llm)) {
|
|
cJSON_DeleteItemFromObjectCaseSensitive(root, "global_llm");
|
|
llm = cJSON_AddObjectToObject(root, "global_llm");
|
|
}
|
|
if (!llm) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
base_url = ai_base_url_from_endpoint(prefs->endpoint ? prefs->endpoint : "");
|
|
if (!base_url) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_DeleteItemFromObjectCaseSensitive(llm, "api_key");
|
|
cJSON_AddStringToObject(llm, "api_key", prefs->api_key ? prefs->api_key : "");
|
|
cJSON_DeleteItemFromObjectCaseSensitive(llm, "model");
|
|
cJSON_AddStringToObject(llm, "model", prefs->model ? prefs->model : "");
|
|
cJSON_DeleteItemFromObjectCaseSensitive(llm, "base_url");
|
|
cJSON_AddStringToObject(llm, "base_url", base_url);
|
|
|
|
new_json = cJSON_PrintUnformatted(root);
|
|
cJSON_Delete(root);
|
|
free(base_url);
|
|
if (!new_json) {
|
|
return -1;
|
|
}
|
|
|
|
free(g_state.user_settings_json);
|
|
g_state.user_settings_json = new_json;
|
|
|
|
if (state_nip44_self_encrypt(new_json, &cipher) != 0 || !cipher) {
|
|
return -1;
|
|
}
|
|
|
|
tags = ai_make_d_tag("user-settings");
|
|
if (!tags) {
|
|
free(cipher);
|
|
return -1;
|
|
}
|
|
|
|
posted = publish_signed_event_with_report("UserSettings", 30078, cipher, tags, 8000);
|
|
cJSON_Delete(tags);
|
|
free(cipher);
|
|
|
|
return (posted < 0) ? -1 : 0;
|
|
}
|
|
|
|
static char *ai_http_post(const char *url, const char *auth_header, const char *json_body) {
|
|
CURL *curl;
|
|
CURLcode rc;
|
|
long status = 0;
|
|
nt_http_buffer_t body = {0};
|
|
struct curl_slist *headers = NULL;
|
|
char curl_err[CURL_ERROR_SIZE] = {0};
|
|
int insecure_retry = 0;
|
|
|
|
if (!url || !json_body) {
|
|
return NULL;
|
|
}
|
|
|
|
if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) {
|
|
return NULL;
|
|
}
|
|
|
|
curl = curl_easy_init();
|
|
if (!curl) {
|
|
curl_global_cleanup();
|
|
return NULL;
|
|
}
|
|
|
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
if (auth_header && auth_header[0] != '\0') {
|
|
headers = curl_slist_append(headers, auth_header);
|
|
}
|
|
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(json_body));
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-terminal/0.0.1");
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 5000L);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 30000L);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, nt_http_write_cb);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&body);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
|
nt_configure_curl_tls(curl);
|
|
if (headers) {
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
|
}
|
|
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_err);
|
|
|
|
retry_request:
|
|
rc = curl_easy_perform(curl);
|
|
if (rc != CURLE_OK) {
|
|
if (!insecure_retry &&
|
|
(rc == CURLE_PEER_FAILED_VERIFICATION || rc == CURLE_SSL_CACERT_BADFILE)) {
|
|
insecure_retry = 1;
|
|
free(body.data);
|
|
body.data = NULL;
|
|
body.len = 0;
|
|
curl_err[0] = '\0';
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
|
goto retry_request;
|
|
}
|
|
|
|
curl_slist_free_all(headers);
|
|
curl_easy_cleanup(curl);
|
|
curl_global_cleanup();
|
|
free(body.data);
|
|
return NULL;
|
|
}
|
|
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
|
if (status < 200 || status >= 300) {
|
|
curl_slist_free_all(headers);
|
|
curl_easy_cleanup(curl);
|
|
curl_global_cleanup();
|
|
free(body.data);
|
|
return NULL;
|
|
}
|
|
|
|
curl_slist_free_all(headers);
|
|
curl_easy_cleanup(curl);
|
|
curl_global_cleanup();
|
|
return body.data;
|
|
}
|
|
|
|
static char *ai_extract_response(const char *resp_json) {
|
|
cJSON *root;
|
|
cJSON *choices;
|
|
cJSON *choice0;
|
|
cJSON *message;
|
|
cJSON *content;
|
|
char *out;
|
|
|
|
if (!resp_json) {
|
|
return NULL;
|
|
}
|
|
|
|
root = cJSON_Parse(resp_json);
|
|
if (!root) {
|
|
return NULL;
|
|
}
|
|
|
|
choices = cJSON_GetObjectItemCaseSensitive(root, "choices");
|
|
choice0 = cJSON_IsArray(choices) ? cJSON_GetArrayItem(choices, 0) : NULL;
|
|
message = cJSON_IsObject(choice0) ? cJSON_GetObjectItemCaseSensitive(choice0, "message") : NULL;
|
|
content = cJSON_IsObject(message) ? cJSON_GetObjectItemCaseSensitive(message, "content") : NULL;
|
|
|
|
if (!cJSON_IsString(content) || !content->valuestring) {
|
|
cJSON_Delete(root);
|
|
return NULL;
|
|
}
|
|
|
|
out = ai_strdup(content->valuestring);
|
|
cJSON_Delete(root);
|
|
return out;
|
|
}
|
|
|
|
static void ai_show_response_stream(const char *text) {
|
|
nt_pager_show_text("> Main Menu > AI > Chat > Response", text ? text : "");
|
|
}
|
|
|
|
static void ai_chat(const ai_prefs_t *prefs) {
|
|
char prompt[4096];
|
|
cJSON *root;
|
|
cJSON *messages;
|
|
cJSON *msg;
|
|
char *body;
|
|
char auth[4096];
|
|
char *resp_json;
|
|
char *answer;
|
|
|
|
if (!prefs) {
|
|
return;
|
|
}
|
|
|
|
prompt[0] = '\0';
|
|
if (tuin_prompt("prompt >", "", prompt, sizeof(prompt)) != 0 || prompt[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
if (!prefs->api_key || prefs->api_key[0] == '\0') {
|
|
tuin_notice("AI API key is empty. Configure settings first.");
|
|
return;
|
|
}
|
|
|
|
root = cJSON_CreateObject();
|
|
if (!root) {
|
|
return;
|
|
}
|
|
cJSON_AddStringToObject(root, "model", prefs->model ? prefs->model : "");
|
|
|
|
messages = cJSON_CreateArray();
|
|
msg = cJSON_CreateObject();
|
|
if (!messages || !msg) {
|
|
cJSON_Delete(messages);
|
|
cJSON_Delete(msg);
|
|
cJSON_Delete(root);
|
|
return;
|
|
}
|
|
cJSON_AddStringToObject(msg, "role", "user");
|
|
cJSON_AddStringToObject(msg, "content", prompt);
|
|
cJSON_AddItemToArray(messages, msg);
|
|
cJSON_AddItemToObject(root, "messages", messages);
|
|
|
|
body = cJSON_PrintUnformatted(root);
|
|
cJSON_Delete(root);
|
|
if (!body) {
|
|
return;
|
|
}
|
|
|
|
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", prefs->api_key ? prefs->api_key : "");
|
|
nt_log("Requesting AI response...");
|
|
resp_json = ai_http_post(prefs->endpoint ? prefs->endpoint : "", auth, body);
|
|
free(body);
|
|
|
|
if (!resp_json) {
|
|
tuin_notice("AI request failed.");
|
|
return;
|
|
}
|
|
|
|
answer = ai_extract_response(resp_json);
|
|
free(resp_json);
|
|
|
|
if (answer) {
|
|
nt_log("AI response received.");
|
|
ai_show_response_stream(answer);
|
|
} else {
|
|
tuin_notice("Failed to parse AI response.");
|
|
}
|
|
free(answer);
|
|
}
|
|
|
|
static void ai_choose_model(ai_prefs_t *prefs) {
|
|
char model[256];
|
|
|
|
if (!prefs) {
|
|
return;
|
|
}
|
|
|
|
model[0] = '\0';
|
|
if (tuin_prompt("model >", prefs->model ? prefs->model : "", model, sizeof(model)) != 0 || model[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
ai_set_string(&prefs->model, model);
|
|
}
|
|
|
|
static void ai_settings(ai_prefs_t *prefs) {
|
|
char endpoint[512];
|
|
char key[1024];
|
|
char model[256];
|
|
|
|
if (!prefs) {
|
|
return;
|
|
}
|
|
|
|
endpoint[0] = '\0';
|
|
if (tuin_prompt("new endpoint >", prefs->endpoint ? prefs->endpoint : "", endpoint, sizeof(endpoint)) == 0 &&
|
|
endpoint[0] != '\0') {
|
|
ai_set_string(&prefs->endpoint, endpoint);
|
|
}
|
|
|
|
key[0] = '\0';
|
|
if (tuin_prompt("new api key >", "", key, sizeof(key)) == 0 && key[0] != '\0') {
|
|
ai_set_string(&prefs->api_key, key);
|
|
}
|
|
|
|
model[0] = '\0';
|
|
if (tuin_prompt("new model >", prefs->model ? prefs->model : "", model, sizeof(model)) == 0 &&
|
|
model[0] != '\0') {
|
|
ai_set_string(&prefs->model, model);
|
|
}
|
|
|
|
if (ai_save_prefs(prefs) != 0) {
|
|
tuin_notice("Failed to save prefs.");
|
|
} else {
|
|
nt_log("Prefs saved.");
|
|
}
|
|
}
|
|
|
|
void menu_ai(void) {
|
|
static const TuiMenuItem AI_ITEMS[] = {
|
|
{NT_HK("C", "hat"), 'c'},
|
|
{NT_HK("M", "odel"), 'm'},
|
|
{NT_HK("S", "ettings"), 's'},
|
|
{NT_HK("X", "Exit"), 'x'},
|
|
};
|
|
ai_prefs_t prefs;
|
|
TuiMenuState menu_state = {0};
|
|
|
|
ai_prefs_init(&prefs);
|
|
(void)ai_load_prefs(&prefs);
|
|
|
|
while (1) {
|
|
TuiFrame frame = nt_frame("> Main Menu > AI");
|
|
char status_text[1024];
|
|
TuiStatus status;
|
|
TuiMenu menu = {AI_ITEMS, (int)(sizeof(AI_ITEMS) / sizeof(AI_ITEMS[0]))};
|
|
int idx;
|
|
|
|
snprintf(status_text,
|
|
sizeof(status_text),
|
|
"Model: %s | Endpoint: %s",
|
|
prefs.model ? prefs.model : "",
|
|
prefs.endpoint ? prefs.endpoint : "");
|
|
status.text = status_text;
|
|
|
|
idx = tuin_menu_run(&frame, &menu, &status, &menu_state);
|
|
|
|
if (idx < 0 || idx == 3) {
|
|
break;
|
|
}
|
|
|
|
if (idx == 0) {
|
|
ai_chat(&prefs);
|
|
} else if (idx == 1) {
|
|
ai_choose_model(&prefs);
|
|
} else if (idx == 2) {
|
|
ai_settings(&prefs);
|
|
}
|
|
}
|
|
|
|
ai_prefs_free(&prefs);
|
|
}
|