import { useState, useEffect } from "react"; import { View, Text, TextInput, ScrollView, Switch, Pressable, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import * as SecureStore from "expo-secure-store"; import { Colors } from "@/constants/theme"; import { DEFAULT_TEMPLATE } from "@/lib/markdown"; import type { GitProvider } from "@/lib/git"; import type { AIProvider } from "@/lib/ai"; import { AI_PROVIDER_BASE_URLS, AI_PROVIDER_MODEL_PLACEHOLDERS } from "@/lib/ai"; const KEYS = { INPUT_MODE: "pref_input_mode", HOME_VIEW: "pref_home_view", AI_ENABLED: "pref_ai_enabled", AI_PROVIDER: "pref_ai_provider", AI_KEY: "pref_ai_key", AI_BASE_URL: "pref_ai_base_url", AI_MODEL: "pref_ai_model", TITLE_TEMPLATE: "pref_title_template", MD_TEMPLATE: "md_template", GIT_PROVIDER: "git_provider", GITEA_URL: "gitea_url", GITEA_TOKEN: "gitea_token", GITEA_OWNER: "gitea_owner", GITEA_REPO: "gitea_repo", GITHUB_TOKEN: "github_token", GITHUB_OWNER: "github_owner", GITHUB_REPO: "github_repo", GITLAB_URL: "gitlab_url", GITLAB_TOKEN: "gitlab_token", GITLAB_OWNER: "gitlab_owner", GITLAB_REPO: "gitlab_repo", } as const; const LABEL = { color: Colors.text2, fontSize: 11, fontWeight: "700" as const, textTransform: "uppercase" as const, letterSpacing: 0.6, marginBottom: 6, } as const; const SECTION_TITLE = { color: Colors.text1, fontSize: 16, fontWeight: "700" as const, marginTop: 28, marginBottom: 12, } as const; const INPUT = { backgroundColor: Colors.surface, borderColor: Colors.border, borderWidth: 1, borderRadius: 8, color: Colors.text1, padding: 12, fontSize: 14, marginBottom: 14, fontFamily: "monospace", } as const; const PROVIDER_LABELS: Record = { gitea: "Gitea", github: "GitHub", gitlab: "GitLab", }; function parseRepoUrl(raw: string, provider: GitProvider): { instanceUrl?: string; owner?: string; repo?: string } { try { const cleaned = raw.trim().replace(/\.git$/, ""); const u = new URL(cleaned); const parts = u.pathname.split("/").filter(Boolean); if (parts.length < 2) return {}; const repo = parts[parts.length - 1]; const owner = parts[parts.length - 2]; const instanceUrl = provider !== "github" ? `${u.protocol}//${u.host}` : undefined; return { instanceUrl, owner, repo }; } catch { return {}; } } function buildRepoUrl(instanceUrl: string | null, owner: string | null, repo: string | null, provider: GitProvider): string { if (!owner || !repo) return ""; const base = provider === "github" ? "https://github.com" : (instanceUrl ?? ""); if (!base) return ""; return `${base}/${owner}/${repo}.git`; } // Defined outside SettingsScreen — prevents remount on every rerender, which // would cause TextInput to lose focus after each keystroke. function ToggleRow({ label, value, onValueChange, }: { label: string; value: boolean; onValueChange: (v: boolean) => void; }) { return ( {label} ); } function SegmentRow({ label, options, value, onChange, wrap, }: { label: string; options: { key: string; label: string }[]; value: string; onChange: (v: string) => void; wrap?: boolean; }) { return ( {label} {options.map((opt) => ( onChange(opt.key)} style={{ ...(wrap ? { width: "31%" } : { flex: 1 }), backgroundColor: value === opt.key ? Colors.primary : Colors.surface, borderRadius: 8, padding: 12, alignItems: "center", borderWidth: 1, borderColor: value === opt.key ? Colors.primary : Colors.border, }} > {opt.label} ))} ); } function Field({ label, value, onChange, placeholder, secure, url, }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string; secure?: boolean; url?: boolean; }) { return ( <> {label} ); } export default function SettingsScreen() { const insets = useSafeAreaInsets(); const [inputMode, setInputMode] = useState<"voice" | "form">("form"); const [homeView, setHomeView] = useState<"dashboard" | "capture">("dashboard"); const [aiEnabled, setAiEnabled] = useState(false); const [aiProvider, setAiProvider] = useState("openai"); const [aiKey, setAiKey] = useState(""); const [aiBaseUrl, setAiBaseUrl] = useState(""); const [aiModel, setAiModel] = useState(""); const [titleTemplate, setTitleTemplate] = useState(""); const [mdTemplate, setMdTemplate] = useState(DEFAULT_TEMPLATE); const [saved, setSaved] = useState(false); const [gitProvider, setGitProvider] = useState("gitea"); const [giteaRepoUrl, setGiteaRepoUrl] = useState(""); const [giteaToken, setGiteaToken] = useState(""); const [githubRepoUrl, setGithubRepoUrl] = useState(""); const [githubToken, setGithubToken] = useState(""); const [gitlabRepoUrl, setGitlabRepoUrl] = useState(""); const [gitlabToken, setGitlabToken] = useState(""); useEffect(() => { (async () => { const [ im, hv, ai, aip, key, aiUrl, aiMdl, titleTpl, tpl, gp, gu, gt, go, gr, ghu, ghown, ghrepo, glu, glt, glo, glr, ] = await Promise.all([ SecureStore.getItemAsync(KEYS.INPUT_MODE), SecureStore.getItemAsync(KEYS.HOME_VIEW), SecureStore.getItemAsync(KEYS.AI_ENABLED), SecureStore.getItemAsync(KEYS.AI_PROVIDER), SecureStore.getItemAsync(KEYS.AI_KEY), SecureStore.getItemAsync(KEYS.AI_BASE_URL), SecureStore.getItemAsync(KEYS.AI_MODEL), SecureStore.getItemAsync(KEYS.TITLE_TEMPLATE), SecureStore.getItemAsync(KEYS.MD_TEMPLATE), SecureStore.getItemAsync(KEYS.GIT_PROVIDER), SecureStore.getItemAsync(KEYS.GITEA_URL), SecureStore.getItemAsync(KEYS.GITEA_TOKEN), SecureStore.getItemAsync(KEYS.GITEA_OWNER), SecureStore.getItemAsync(KEYS.GITEA_REPO), SecureStore.getItemAsync(KEYS.GITHUB_TOKEN), SecureStore.getItemAsync(KEYS.GITHUB_OWNER), SecureStore.getItemAsync(KEYS.GITHUB_REPO), SecureStore.getItemAsync(KEYS.GITLAB_URL), SecureStore.getItemAsync(KEYS.GITLAB_TOKEN), SecureStore.getItemAsync(KEYS.GITLAB_OWNER), SecureStore.getItemAsync(KEYS.GITLAB_REPO), ]); if (im) setInputMode(im as "voice" | "form"); if (hv) setHomeView(hv as "dashboard" | "capture"); if (ai) setAiEnabled(ai === "true"); if (aip) setAiProvider(aip as AIProvider); if (key) setAiKey(key); if (aiUrl) setAiBaseUrl(aiUrl); if (aiMdl) setAiModel(aiMdl); if (titleTpl) setTitleTemplate(titleTpl); if (tpl) setMdTemplate(tpl); if (gp) setGitProvider(gp as GitProvider); if (gt) setGiteaToken(gt); setGiteaRepoUrl(buildRepoUrl(gu, go, gr, "gitea")); if (ghu) setGithubToken(ghu); setGithubRepoUrl(buildRepoUrl(null, ghown, ghrepo, "github")); if (glt) setGitlabToken(glt); setGitlabRepoUrl(buildRepoUrl(glu, glo, glr, "gitlab")); })(); }, []); async function handleSave() { const gitea = parseRepoUrl(giteaRepoUrl, "gitea"); const github = parseRepoUrl(githubRepoUrl, "github"); const gitlab = parseRepoUrl(gitlabRepoUrl, "gitlab"); await Promise.all([ SecureStore.setItemAsync(KEYS.INPUT_MODE, inputMode), SecureStore.setItemAsync(KEYS.HOME_VIEW, homeView), SecureStore.setItemAsync(KEYS.AI_ENABLED, String(aiEnabled)), SecureStore.setItemAsync(KEYS.AI_PROVIDER, aiProvider), SecureStore.setItemAsync(KEYS.AI_KEY, aiKey), SecureStore.setItemAsync(KEYS.AI_BASE_URL, aiBaseUrl), SecureStore.setItemAsync(KEYS.AI_MODEL, aiModel), SecureStore.setItemAsync(KEYS.TITLE_TEMPLATE, titleTemplate), SecureStore.setItemAsync(KEYS.MD_TEMPLATE, mdTemplate), SecureStore.setItemAsync(KEYS.GIT_PROVIDER, gitProvider), SecureStore.setItemAsync(KEYS.GITEA_URL, gitea.instanceUrl ?? ""), SecureStore.setItemAsync(KEYS.GITEA_TOKEN, giteaToken), SecureStore.setItemAsync(KEYS.GITEA_OWNER, gitea.owner ?? ""), SecureStore.setItemAsync(KEYS.GITEA_REPO, gitea.repo ?? ""), SecureStore.setItemAsync(KEYS.GITHUB_TOKEN, githubToken), SecureStore.setItemAsync(KEYS.GITHUB_OWNER, github.owner ?? ""), SecureStore.setItemAsync(KEYS.GITHUB_REPO, github.repo ?? ""), SecureStore.setItemAsync(KEYS.GITLAB_URL, gitlab.instanceUrl ?? ""), SecureStore.setItemAsync(KEYS.GITLAB_TOKEN, gitlabToken), SecureStore.setItemAsync(KEYS.GITLAB_OWNER, gitlab.owner ?? ""), SecureStore.setItemAsync(KEYS.GITLAB_REPO, gitlab.repo ?? ""), ]); setSaved(true); setTimeout(() => setSaved(false), 2000); } return ( Preferences setInputMode(v as "voice" | "form")} options={[ { key: "form", label: "Form" }, { key: "voice", label: "Voice" }, ]} /> setHomeView(v as "dashboard" | "capture")} options={[ { key: "dashboard", label: "Dashboard" }, { key: "capture", label: "Quick Capture" }, ]} /> Pre-fills the Title field on new incidents. If the value ends with digits, the number is auto-incremented after each save.{"\n"} e.g. INC-001 → INC-002 → INC-003 AI — Root Cause Assistant When enabled, a Suggest button appears between Symptom and Root Cause. It sends the title and symptom to your AI provider and pre-fills the analysis fields. Supports Anthropic natively; all others use the OpenAI-compatible API. {aiEnabled && ( <> { const p = v as AIProvider; setAiProvider(p); setAiBaseUrl(AI_PROVIDER_BASE_URLS[p]); }} options={[ { key: "anthropic", label: "Anthropic" }, { key: "openai", label: "OpenAI" }, { key: "mistral", label: "Mistral" }, { key: "perplexity", label: "Perplexity" }, { key: "ollama", label: "Ollama" }, { key: "openrouter", label: "OpenRouter" }, { key: "custom", label: "Custom" }, ]} /> {aiProvider !== "anthropic" && ( )} )} Git Repository setGitProvider(v as GitProvider)} options={(["gitea", "github", "gitlab"] as GitProvider[]).map((p) => ({ key: p, label: PROVIDER_LABELS[p], }))} /> {gitProvider === "gitea" && ( <> The repo must have at least one commit — initialize it with a README if empty. )} {gitProvider === "github" && ( <> )} {gitProvider === "gitlab" && ( <> )} Markdown Template {saved ? "Saved ✓" : "Save Settings"} ); }