chore: initial commit — existing single-user document scanner codebase

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-22 08:53:28 +02:00
parent 6fed5ba531
commit 7a34807fa0
71 changed files with 16408 additions and 0 deletions
+223
View File
@@ -0,0 +1,223 @@
<template>
<div class="p-8 max-w-3xl mx-auto">
<h2 class="text-2xl font-bold text-gray-900 mb-1">Settings</h2>
<p class="text-gray-500 text-sm mb-8">Configure AI provider and the system prompt.</p>
<div v-if="settingsStore.loading" class="text-gray-400 text-sm">Loading</div>
<div v-else-if="!settingsStore.settings" class="text-red-500 text-sm">Failed to load settings.</div>
<template v-else>
<!-- AI Provider -->
<section class="bg-white border border-gray-200 rounded-xl p-6 mb-5">
<h3 class="font-semibold text-gray-800 mb-4">AI Provider</h3>
<div class="flex flex-wrap gap-2 mb-6">
<button
v-for="prov in providers"
:key="prov.id"
@click="activeProvider = prov.id"
class="px-4 py-2 rounded-lg text-sm font-medium border transition-colors"
:class="activeProvider === prov.id
? 'bg-indigo-600 text-white border-indigo-600'
: 'border-gray-300 text-gray-600 hover:bg-gray-50'"
>
{{ prov.label }}
</button>
</div>
<!-- Anthropic config -->
<div v-if="activeProvider === 'anthropic'" class="space-y-3">
<label class="block text-sm font-medium text-gray-700">API Key</label>
<input
v-model="providerCfg.anthropic.api_key"
type="password"
placeholder="sk-ant-…"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
<label class="block text-sm font-medium text-gray-700 mt-3">Model</label>
<input
v-model="providerCfg.anthropic.model"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
</div>
<!-- OpenAI config -->
<div v-else-if="activeProvider === 'openai'" class="space-y-3">
<label class="block text-sm font-medium text-gray-700">API Key</label>
<input
v-model="providerCfg.openai.api_key"
type="password"
placeholder="sk-…"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
<label class="block text-sm font-medium text-gray-700 mt-3">Model</label>
<input
v-model="providerCfg.openai.model"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
<label class="block text-sm font-medium text-gray-700 mt-3">Base URL (optional)</label>
<input
v-model="providerCfg.openai.base_url"
placeholder="https://api.openai.com/v1"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
</div>
<!-- Ollama config -->
<div v-else-if="activeProvider === 'ollama'" class="space-y-3">
<label class="block text-sm font-medium text-gray-700">Base URL</label>
<input
v-model="providerCfg.ollama.base_url"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
<label class="block text-sm font-medium text-gray-700 mt-3">Model</label>
<input
v-model="providerCfg.ollama.model"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
<p class="text-xs text-gray-400 mt-1">
Ollama must be started with <code class="bg-gray-100 px-1 rounded">OLLAMA_HOST=0.0.0.0 ollama serve</code>
</p>
</div>
<!-- LM Studio config -->
<div v-else-if="activeProvider === 'lmstudio'" class="space-y-3">
<label class="block text-sm font-medium text-gray-700">Base URL</label>
<input
v-model="providerCfg.lmstudio.base_url"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
<label class="block text-sm font-medium text-gray-700 mt-3">Model</label>
<input
v-model="providerCfg.lmstudio.model"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
<p class="text-xs text-gray-400 mt-1">
LM Studio server must be bound to <code class="bg-gray-100 px-1 rounded">0.0.0.0</code> in LM Studio settings.
</p>
</div>
<!-- Test connection -->
<div class="flex items-center gap-3 mt-5">
<button
@click="testConn"
:disabled="testing"
class="text-sm px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
>
{{ testing ? 'Testing…' : 'Test Connection' }}
</button>
<span v-if="testResult" :class="testResult.ok ? 'text-green-600' : 'text-red-500'" class="text-sm">
{{ testResult.ok ? '' : '' }} {{ testResult.message }}
<span v-if="testResult.ok && testResult.latency_ms" class="text-gray-400">({{ testResult.latency_ms }}ms)</span>
</span>
</div>
</section>
<!-- System Prompt -->
<section class="bg-white border border-gray-200 rounded-xl p-6 mb-5">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-gray-800">System Prompt</h3>
<button @click="resetPrompt" class="text-xs text-indigo-600 hover:underline">Reset to default</button>
</div>
<textarea
v-model="systemPrompt"
rows="8"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-indigo-400 resize-y"
></textarea>
</section>
<!-- Save -->
<div class="flex items-center gap-3">
<button
@click="save"
:disabled="saving"
class="px-6 py-2.5 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700 transition-colors disabled:opacity-50"
>
{{ saving ? 'Saving…' : 'Save Settings' }}
</button>
<span v-if="saveMsg" :class="saveError ? 'text-red-500' : 'text-green-600'" class="text-sm">
{{ saveMsg }}
</span>
</div>
</template>
</div>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from 'vue'
import { useSettingsStore } from '../stores/settings.js'
const settingsStore = useSettingsStore()
const saving = ref(false)
const testing = ref(false)
const testResult = ref(null)
const saveMsg = ref('')
const saveError = ref(false)
const providers = [
{ id: 'lmstudio', label: 'LM Studio' },
{ id: 'ollama', label: 'Ollama' },
{ id: 'openai', label: 'OpenAI' },
{ id: 'anthropic', label: 'Anthropic' },
]
const activeProvider = ref('lmstudio')
const systemPrompt = ref('')
const providerCfg = reactive({
anthropic: { api_key: '', model: 'claude-sonnet-4-6' },
openai: { api_key: '', model: 'gpt-4o', base_url: '' },
ollama: { base_url: 'http://host.docker.internal:11434', model: 'llama3.2' },
lmstudio: { base_url: 'http://host.docker.internal:1234', model: 'gemma-4-e4b-it' },
})
onMounted(async () => {
await settingsStore.fetchSettings()
populateForm()
})
function populateForm() {
const s = settingsStore.settings
if (!s) return
activeProvider.value = s.active_provider
systemPrompt.value = s.system_prompt
for (const [k, v] of Object.entries(s.providers || {})) {
if (providerCfg[k]) Object.assign(providerCfg[k], v)
}
}
async function testConn() {
testing.value = true
testResult.value = null
try {
testResult.value = await settingsStore.testConnection(activeProvider.value)
} catch (e) {
testResult.value = { ok: false, message: e.message, latency_ms: 0 }
} finally {
testing.value = false
}
}
async function resetPrompt() {
systemPrompt.value = await settingsStore.resetPrompt()
}
async function save() {
saving.value = true
saveMsg.value = ''
saveError.value = false
try {
await settingsStore.save({
system_prompt: systemPrompt.value,
active_provider: activeProvider.value,
providers: providerCfg,
})
saveMsg.value = 'Settings saved.'
} catch (e) {
saveMsg.value = e.message
saveError.value = true
} finally {
saving.value = false
setTimeout(() => saveMsg.value = '', 3000)
}
}
</script>