feat: add Quick/Structured tabs to DiagnoseView

This commit is contained in:
pyr0ball 2026-05-11 09:20:47 -07:00
parent 3b00ad8b47
commit bf276aeb50

View file

@ -1,108 +1,69 @@
<template>
<div class="p-6 max-w-4xl mx-auto">
<div class="mb-6">
<div class="mb-5">
<h1 class="text-text-primary text-xl font-semibold mb-1">Diagnose</h1>
<p class="text-text-dim text-sm">
Describe a symptom or service name. Turnstone runs layered searches broad relevance,
then CRITICAL/ERROR and returns deduplicated evidence sorted by time.
Quick: describe a symptom to surface log evidence.
Structured: tag a timestamped incident record.
</p>
</div>
<div class="flex gap-3 mb-6">
<input
v-model="symptom"
type="text"
placeholder="e.g. 'plex EAE audio' or 'ssh authentication failed'"
class="flex-1 bg-surface-raised border border-surface-border rounded px-4 py-2.5 text-sm text-text-primary placeholder-text-dim focus:outline-none focus:border-accent transition-colors"
@keydown.enter="run()"
/>
<!-- Tab toggle -->
<div class="flex gap-1 mb-6 border-b border-surface-border">
<button
class="px-6 py-2.5 rounded bg-accent text-white text-sm font-semibold hover:bg-blue-400 transition-colors disabled:opacity-50"
:disabled="loading || !symptom.trim()"
@click="run()"
v-for="t in tabs"
:key="t.key"
@click="activeTab = t.key"
:class="[
'px-4 py-2 text-sm transition-colors border-b-2 -mb-px',
activeTab === t.key
? 'border-accent text-accent'
: 'border-transparent text-text-muted hover:text-text-primary'
]"
>{{ t.label }}</button>
</div>
<!-- Quick tab -->
<QuickCapture v-if="activeTab === 'quick'" />
<!-- Structured tab -->
<template v-else>
<IncidentForm @created="onCreated" />
<div
v-if="createdLabel"
class="mt-4 p-3 rounded bg-green-900/30 border border-green-700/40 text-green-400 text-sm"
>
<span v-if="loading">Diagnosing</span>
<span v-else>Diagnose</span>
</button>
</div>
<!-- Error -->
<div v-if="error" class="mb-4 p-3 rounded bg-red-900/30 border border-red-700/40 text-sev-error text-sm">
{{ error }}
</div>
<!-- Results -->
<template v-if="entries.length">
<div class="mb-3 text-text-dim text-xs">
{{ entries.length }} relevant log{{ entries.length !== 1 ? 's' : '' }} sorted chronologically
</div>
<!-- Plain-text summary (pre-formatted for LLM context) -->
<div v-if="formatted" class="mb-4 rounded border border-surface-border bg-surface-raised p-4 overflow-x-auto">
<p class="text-text-dim text-xs uppercase tracking-widest mb-2">Formatted summary</p>
<pre class="text-text-muted text-xs whitespace-pre-wrap leading-relaxed font-mono">{{ formatted }}</pre>
</div>
<div class="rounded border border-surface-border overflow-hidden">
<LogEntryRow
v-for="entry in entries"
:key="entry.entry_id"
:entry="entry"
/>
Incident "{{ createdLabel }}" saved
<RouterLink to="/incidents" class="underline underline-offset-2">view in Incidents</RouterLink>
</div>
</template>
<!-- Zero state after run -->
<div v-else-if="ranOnce && !loading" class="text-center text-text-dim py-12">
<p class="mb-1">No log evidence found for "{{ lastQuery }}"</p>
<p class="text-sm">Check the Sources tab to confirm data is ingested, or try a broader description.</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import type { LogEntry } from '@/stores/search'
import LogEntryRow from '@/components/LogEntryRow.vue'
import { ref, onMounted, watch } from 'vue'
import { useRoute, RouterLink } from 'vue-router'
import QuickCapture from '@/components/QuickCapture.vue'
import IncidentForm from '@/components/IncidentForm.vue'
const route = useRoute()
const symptom = ref('')
const entries = ref<LogEntry[]>([])
const formatted = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
const ranOnce = ref(false)
const lastQuery = ref('')
const BASE = import.meta.env.BASE_URL.replace(/\/$/, '')
const tabs: { key: 'quick' | 'structured'; label: string }[] = [
{ key: 'quick', label: 'Quick' },
{ key: 'structured', label: 'Structured' },
]
const activeTab = ref<'quick' | 'structured'>('quick')
const createdLabel = ref('')
onMounted(() => {
const q = route.query.q
if (typeof q === 'string' && q.trim()) {
symptom.value = q
run()
}
if (route.query.tab === 'structured') activeTab.value = 'structured'
})
async function run() {
if (!symptom.value.trim()) return
loading.value = true
error.value = null
lastQuery.value = symptom.value
try {
const url = new URL(`${BASE}/api/diagnose`, window.location.origin)
url.searchParams.set('q', symptom.value)
const res = await fetch(url.toString())
if (!res.ok) throw new Error(`API returned ${res.status}`)
const data = await res.json()
entries.value = data.results
formatted.value = data.formatted ?? ''
ranOnce.value = true
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
} finally {
loading.value = false
}
watch(() => route.query.tab, (tab) => {
if (tab === 'structured' || tab === 'quick') activeTab.value = tab
})
function onCreated(label: string) {
createdLabel.value = label
setTimeout(() => { createdLabel.value = '' }, 4000)
}
</script>