feat: add Quick/Structured tabs to DiagnoseView
This commit is contained in:
parent
3b00ad8b47
commit
bf276aeb50
1 changed files with 46 additions and 85 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue