turnstone/web/src/views/SettingsView.vue

137 lines
4.7 KiB
Vue

<template>
<div class="p-6 max-w-2xl mx-auto">
<div class="mb-6">
<h1 class="text-text-primary text-xl font-semibold mb-1">Settings</h1>
<p class="text-text-dim text-sm">
Turnstone preferences stored alongside the log database.
</p>
</div>
<div class="rounded border border-surface-border bg-surface-raised p-5 space-y-6">
<!-- Entry point -->
<div>
<h2 class="text-text-primary text-sm font-semibold mb-1">Quick Capture Entry Point</h2>
<p class="text-text-dim text-xs mb-3">
Where the "describe it and search" input appears on every page.
</p>
<div class="flex gap-3">
<button
v-for="opt in entryPointOptions"
:key="opt.value"
@click="setEntryPoint(opt.value as 'topbar' | 'fab')"
:class="[
'flex-1 px-4 py-3 rounded border text-sm transition-colors text-left',
prefs.entry_point_style === opt.value
? 'border-accent bg-accent/10 text-accent'
: 'border-surface-border text-text-muted hover:text-text-primary hover:border-accent'
]"
>
<div class="font-medium">{{ opt.label }}</div>
<div class="text-xs text-text-dim mt-0.5">{{ opt.desc }}</div>
</button>
</div>
</div>
<!-- LLM config -->
<div>
<h2 class="text-text-primary text-sm font-semibold mb-1">LLM Reasoning</h2>
<p class="text-text-dim text-xs mb-3">
Ollama endpoint used to generate plain-language diagnoses. Leave blank to disable.
</p>
<div class="space-y-3">
<div>
<label class="block text-xs text-text-dim mb-1">Ollama URL</label>
<input
v-model="prefs.llm_url"
type="text"
placeholder="http://localhost:11434"
class="w-full bg-surface border border-surface-border rounded px-3 py-2 text-sm text-text-primary placeholder-text-dim focus:outline-none focus:border-accent transition-colors"
/>
</div>
<div>
<label class="block text-xs text-text-dim mb-1">Model</label>
<input
v-model="prefs.llm_model"
type="text"
placeholder="llama3.1:8b"
class="w-full bg-surface border border-surface-border rounded px-3 py-2 text-sm text-text-primary placeholder-text-dim focus:outline-none focus:border-accent transition-colors"
/>
</div>
<button
@click="saveLlm"
class="px-4 py-2 bg-accent text-surface text-sm rounded font-medium hover:opacity-90 transition-opacity"
>
Save LLM settings
</button>
</div>
</div>
<p
v-if="saveStatus"
class="text-xs"
:class="saveStatus.ok ? 'text-green-400' : 'text-sev-error'"
>
{{ saveStatus.msg }}
</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const BASE = import.meta.env.BASE_URL.replace(/\/$/, '')
interface Prefs {
entry_point_style: 'topbar' | 'fab'
llm_url: string
llm_model: string
}
const prefs = ref<Prefs>({ entry_point_style: 'topbar', llm_url: '', llm_model: '' })
const saveStatus = ref<{ ok: boolean; msg: string } | null>(null)
const entryPointOptions = [
{ value: 'topbar', label: 'Top bar', desc: 'Persistent input bar below the nav on every page' },
{ value: 'fab', label: 'FAB', desc: 'Floating action button in the bottom-right corner' },
]
onMounted(async () => {
try {
const res = await fetch(`${BASE}/api/settings`)
if (res.ok) prefs.value = await res.json()
} catch { /* non-critical — defaults stay */ }
})
async function patch(body: Partial<Prefs>) {
const res = await fetch(`${BASE}/api/settings`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!res.ok) throw new Error(await res.text())
prefs.value = await res.json()
}
async function setEntryPoint(style: 'topbar' | 'fab') {
saveStatus.value = null
try {
await patch({ entry_point_style: style })
saveStatus.value = { ok: true, msg: 'Saved' }
setTimeout(() => { saveStatus.value = null }, 2000)
} catch {
saveStatus.value = { ok: false, msg: 'Save failed — check server connection' }
}
}
async function saveLlm() {
saveStatus.value = null
try {
await patch({ llm_url: prefs.value.llm_url, llm_model: prefs.value.llm_model })
saveStatus.value = { ok: true, msg: 'LLM settings saved' }
setTimeout(() => { saveStatus.value = null }, 2000)
} catch {
saveStatus.value = { ok: false, msg: 'Save failed — check server connection' }
}
}
</script>