feat(03-04): replace settings UI with admin-managed placeholder; update API client
- views/SettingsView.vue: Replace full form with static placeholder card. No store imports, no API calls. Shows "AI configuration is managed by your administrator." (D-12, T-03-21) - stores/settings.js: Deleted — only consumed by SettingsView; no other imports - api/client.js: Remove getSettings, patchSettings, testProvider, getDefaultPrompt (// Settings section deleted). Add getMyQuota() for quota bar (Plan 03-05). Add getUploadUrl() and confirmUpload() for presigned upload flow (Plan 03-05).
This commit is contained in:
+15
-23
@@ -71,6 +71,18 @@ export function classifyDocument(id, topics = null) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUploadUrl(filename, contentType) {
|
||||||
|
return request('/api/documents/upload-url', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ filename, content_type: contentType }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmUpload(documentId) {
|
||||||
|
return request(`/api/documents/${documentId}/confirm`, { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
// ── Topics ───────────────────────────────────────────────────────────────────
|
// ── Topics ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function listTopics() {
|
export function listTopics() {
|
||||||
@@ -105,30 +117,10 @@ export function suggestTopics(documentId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Settings ─────────────────────────────────────────────────────────────────
|
// ── Quota ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function getSettings() {
|
export function getMyQuota() {
|
||||||
return request('/api/settings')
|
return request('/api/auth/me/quota')
|
||||||
}
|
|
||||||
|
|
||||||
export function patchSettings(patch) {
|
|
||||||
return request('/api/settings', {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(patch),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function testProvider(provider) {
|
|
||||||
return request('/api/settings/test-provider', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ provider }),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultPrompt() {
|
|
||||||
return request('/api/settings/default-prompt')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auth ─────────────────────────────────────────────────────────────────────
|
// ── Auth ─────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import * as api from '../api/client.js'
|
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
|
||||||
const settings = ref(null)
|
|
||||||
const loading = ref(false)
|
|
||||||
const error = ref(null)
|
|
||||||
|
|
||||||
async function fetchSettings() {
|
|
||||||
loading.value = true
|
|
||||||
error.value = null
|
|
||||||
try {
|
|
||||||
settings.value = await api.getSettings()
|
|
||||||
} catch (e) {
|
|
||||||
error.value = e.message
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save(patch) {
|
|
||||||
const updated = await api.patchSettings(patch)
|
|
||||||
settings.value = updated
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testConnection(provider) {
|
|
||||||
return api.testProvider(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetPrompt() {
|
|
||||||
const data = await api.getDefaultPrompt()
|
|
||||||
return data.system_prompt
|
|
||||||
}
|
|
||||||
|
|
||||||
return { settings, loading, error, fetchSettings, save, testConnection, resetPrompt }
|
|
||||||
})
|
|
||||||
@@ -1,223 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-8 max-w-3xl mx-auto">
|
<div class="p-8 max-w-3xl mx-auto">
|
||||||
<h2 class="text-2xl font-bold text-gray-900 mb-1">Settings</h2>
|
<h2 class="text-2xl font-semibold text-gray-900 mb-1">Settings</h2>
|
||||||
<p class="text-gray-500 text-sm mb-8">Configure AI provider and the system prompt.</p>
|
<p class="text-sm text-gray-500 mb-8">Account-level options for your DocuVault workspace.</p>
|
||||||
|
|
||||||
<div v-if="settingsStore.loading" class="text-gray-400 text-sm">Loading…</div>
|
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<div v-else-if="!settingsStore.settings" class="text-red-500 text-sm">Failed to load settings.</div>
|
<h3 class="text-xl font-semibold text-gray-800 mb-2">AI configuration</h3>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
<template v-else>
|
AI provider and model are managed by your administrator. Contact your admin
|
||||||
<!-- AI Provider -->
|
to request changes to which AI provider is used for your documents.
|
||||||
<section class="bg-white border border-gray-200 rounded-xl p-6 mb-5">
|
</p>
|
||||||
<h3 class="font-semibold text-gray-800 mb-4">AI Provider</h3>
|
</section>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, watch, onMounted } from 'vue'
|
// SettingsView is a static placeholder after Phase 3 D-12 settings retirement.
|
||||||
import { useSettingsStore } from '../stores/settings.js'
|
// No store usage, no API calls — AI config is admin-only via /api/admin/users/{id}/ai-config.
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user