import { useState, useEffect, useRef } from "react"; import { View, Text, TextInput, ScrollView, Pressable, KeyboardAvoidingView, Platform, Alert, } from "react-native"; import { router, useLocalSearchParams, useNavigation } from "expo-router"; import * as SecureStore from "expo-secure-store"; import { createIncident, updateIncident, getIncidentById, getDistinctServices, getMaxTitleNumber, } from "@/lib/db"; import { Colors } from "@/constants/theme"; import { useVoice } from "@/hooks/useVoice"; import { callAI } from "@/lib/ai"; import type { IncidentDraft } from "@/types/incident"; const FIELD_STYLE = { backgroundColor: Colors.surface, borderColor: Colors.border, borderWidth: 1, borderRadius: 8, color: Colors.text1, padding: 12, fontSize: 14, marginBottom: 16, } as const; const MONO_FIELD_STYLE = { ...FIELD_STYLE, fontFamily: "monospace", minHeight: 80, textAlignVertical: "top" as const, } as const; const LABEL_STYLE = { color: Colors.text2, fontSize: 12, fontWeight: "600" as const, textTransform: "uppercase" as const, letterSpacing: 0.5, } as const; type VoiceField = "title" | "service" | "symptom" | "rootCause" | "fix"; export default function NewIncidentScreen() { const { editId } = useLocalSearchParams<{ editId?: string }>(); const navigation = useNavigation(); const [form, setForm] = useState({ title: "", service: "", symptom: "", rootCause: "", fix: "", status: "open", tags: [], }); const [tagInput, setTagInput] = useState(""); const [saving, setSaving] = useState(false); const [suggesting, setSuggesting] = useState(false); const [suggestError, setSuggestError] = useState(""); const [aiEnabled, setAiEnabled] = useState(false); const [knownServices, setKnownServices] = useState([]); const [serviceFocused, setServiceFocused] = useState(false); const { state: voiceState, transcript, start: startVoice, stop: stopVoice } = useVoice(); const voiceFieldRef = useRef(null); const [listeningField, setListeningField] = useState(null); useEffect(() => { getDistinctServices().then(setKnownServices); SecureStore.getItemAsync("pref_ai_enabled").then((v) => setAiEnabled(v === "true")); }, []); useEffect(() => { if (editId) return; SecureStore.getItemAsync("pref_title_template").then((tpl) => { if (tpl) setForm((f) => ({ ...f, title: tpl })); }); }, [editId]); useEffect(() => { if (!editId) return; navigation.setOptions({ title: "Edit Incident" }); getIncidentById(editId).then((inc) => { if (!inc) return; setForm({ title: inc.title, service: inc.service, symptom: inc.symptom, rootCause: inc.rootCause, fix: inc.fix, status: inc.status, tags: inc.tags, }); }); }, [editId]); // Live-fill the active voice field as transcript updates useEffect(() => { const field = voiceFieldRef.current; if (transcript && field) { setForm((f) => ({ ...f, [field]: transcript })); } }, [transcript]); // Clear listening state when recognition ends useEffect(() => { if ((voiceState === "idle" || voiceState === "error") && voiceFieldRef.current !== null) { voiceFieldRef.current = null; setListeningField(null); } }, [voiceState]); function handleMic(field: VoiceField) { if (listeningField !== null) { stopVoice(); return; } voiceFieldRef.current = field; setListeningField(field); startVoice("fr-FR"); } function set(key: keyof IncidentDraft) { return (value: string) => setForm((f) => ({ ...f, [key]: value })); } function handleTagInputEnd() { const trimmed = tagInput.trim(); if (trimmed && !form.tags.includes(trimmed)) { setForm((f) => ({ ...f, tags: [...f.tags, trimmed] })); } setTagInput(""); } function removeTag(tag: string) { setForm((f) => ({ ...f, tags: f.tags.filter((t) => t !== tag) })); } async function handleSuggest() { setSuggesting(true); setSuggestError(""); try { const result = await callAI(form.title, form.symptom); setForm((f) => ({ ...f, rootCause: result.rootCause, fix: result.fix })); } catch (e) { setSuggestError(e instanceof Error ? e.message : "AI call failed"); } finally { setSuggesting(false); } } async function handleSave() { if (!form.title.trim()) { Alert.alert("Required", "Title is required."); return; } setSaving(true); try { if (editId) { await updateIncident(editId, form); } else { await createIncident(form); const tpl = await SecureStore.getItemAsync("pref_title_template"); if (tpl) { const match = tpl.match(/^([\s\S]*?)(\d+)$/); if (match) { const [, prefix, numStr] = match; const dbMax = await getMaxTitleNumber(prefix); const tplNum = parseInt(numStr, 10); const next = (Math.max(dbMax, tplNum) + 1).toString().padStart(numStr.length, "0"); await SecureStore.setItemAsync("pref_title_template", prefix + next); } } } router.back(); } catch { Alert.alert("Error", "Failed to save incident."); } finally { setSaving(false); } } const filteredServices = serviceFocused && form.service.length > 0 ? knownServices.filter( (s) => s.toLowerCase().includes(form.service.toLowerCase()) && s !== form.service ) : []; function micButton(field: VoiceField) { const active = listeningField === field; return ( handleMic(field)} style={{ paddingHorizontal: 8, paddingVertical: 3, borderRadius: 4, backgroundColor: active ? "#ef444420" : Colors.surface2, borderWidth: 1, borderColor: active ? Colors.danger : Colors.border, }} > {active ? "STOP" : "MIC"} ); } function labelRow(label: string, field: VoiceField) { return ( {label} {micButton(field)} ); } return ( {labelRow("Title *", "title")} {labelRow("Service", "service")} setServiceFocused(true)} onBlur={() => setTimeout(() => setServiceFocused(false), 150)} placeholder="e.g. ghost-blog, k3s-cluster, haproxy" placeholderTextColor={Colors.textDim} autoCapitalize="none" returnKeyType="next" /> {filteredServices.length > 0 && ( {filteredServices.slice(0, 5).map((s, i) => ( { setForm((f) => ({ ...f, service: s })); setServiceFocused(false); }} style={{ padding: 12, borderTopWidth: i === 0 ? 0 : 1, borderTopColor: Colors.border, }} > {s} ))} )} {labelRow("Symptom", "symptom")} {aiEnabled && form.title.trim() && form.symptom.trim() && ( {suggesting ? "Analyzing…" : "✦ Suggest root cause & fix"} {suggestError !== "" && ( {suggestError} )} )} {labelRow("Root Cause", "rootCause")} {labelRow("Fix Applied", "fix")} Tags {form.tags.length > 0 && ( {form.tags.map((tag) => ( removeTag(tag)} style={{ backgroundColor: Colors.surface2, borderRadius: 4, paddingHorizontal: 10, paddingVertical: 4, flexDirection: "row", alignItems: "center", gap: 6, }} > {tag} x ))} )} {saving ? "Saving…" : editId ? "Save Changes" : "Save Incident"} ); }