- GET /api/admin/ai-config/models — fetch model list from provider's /models endpoint - POST /api/admin/ai-config/test-connection — replaced GET; accepts unsaved form values (api_key, base_url, model_name) so admins can test before saving - TestConnectionRequest Pydantic model with provider_id validation - SearchableModelSelect.vue — new reusable combobox; Teleport to body avoids overflow:hidden clipping; shows all models on open, filters only on typing; static "Enter manually" item always pinned at bottom; caches per provider - AdminAiConfigTab: model name input replaced with SearchableModelSelect; testingProvider ref + spinner added to Test Connection button - DocumentCard: processing spinner + "Classifying…", pending "Queued" badge, classification_failed badge; topics section only shown when status=ready - FileManagerView: onUnmounted imported (polling wiring pending next session) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
159 lines
5.9 KiB
Vue
159 lines
5.9 KiB
Vue
<template>
|
|
<StorageBrowser
|
|
mode="local"
|
|
:folders="foldersStore.folders"
|
|
:files="docsStore.documents"
|
|
:breadcrumb="foldersStore.breadcrumb"
|
|
:upload-queue="uploadQueue"
|
|
:loading="docsStore.loading || foldersStore.loading"
|
|
:search-query="docsStore.searchQuery"
|
|
:sort-field="docsStore.sortField"
|
|
:sort-order="docsStore.sortOrder"
|
|
:root-folders="foldersStore.rootFolders"
|
|
:topic-color-fn="topicColor"
|
|
:empty-message="currentFolderId ? 'This folder is empty' : 'No folders yet'"
|
|
:empty-hint="currentFolderId ? 'Upload files above or create a sub-folder' : 'Create a folder to get started'"
|
|
ref="browserRef"
|
|
@breadcrumb-navigate="handleBreadcrumbNavigate"
|
|
@new-folder="browserRef?.startNewFolder()"
|
|
@search-change="val => (docsStore.searchQuery = val)"
|
|
@sort-change="handleSortChange"
|
|
@upload="onFilesSelected"
|
|
@folder-navigate="folder => navigateToFolder(folder.id)"
|
|
@folder-create="handleFolderCreate"
|
|
@folder-rename="handleFolderRename"
|
|
@folder-delete="folder => (folderToDelete = folder)"
|
|
@file-open="file => $router.push(`/document/${file.id}`)"
|
|
@file-share="file => (shareDoc = file)"
|
|
@file-move="({ fileId, folderId }) => doMove(fileId, folderId)"
|
|
@file-delete="doDeleteDoc"
|
|
/>
|
|
|
|
<FolderDeleteModal
|
|
v-if="folderToDelete"
|
|
:folder="folderToDelete"
|
|
@confirm="confirmDeleteFolder"
|
|
@cancel="folderToDelete = null"
|
|
/>
|
|
<ShareModal
|
|
v-if="shareDoc"
|
|
:doc="shareDoc"
|
|
@close="shareDoc = null"
|
|
@unshared="(id) => { const d = docsStore.documents.find(x => x.id === id); if (d) d.is_shared = false }"
|
|
/>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useFoldersStore } from '../stores/folders.js'
|
|
import { useDocumentsStore } from '../stores/documents.js'
|
|
import { useTopicsStore } from '../stores/topics.js'
|
|
import StorageBrowser from '../components/storage/StorageBrowser.vue'
|
|
import FolderDeleteModal from '../components/folders/FolderDeleteModal.vue'
|
|
import ShareModal from '../components/sharing/ShareModal.vue'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const foldersStore = useFoldersStore()
|
|
const docsStore = useDocumentsStore()
|
|
const topicsStore = useTopicsStore()
|
|
const browserRef = ref(null)
|
|
|
|
// ── Navigation ────────────────────────────────────────────────────────────────
|
|
|
|
const currentFolderId = computed(() => route.params.folderId ?? null)
|
|
|
|
async function loadFolder(folderId) {
|
|
if (folderId === null) {
|
|
foldersStore.navigateTo(null)
|
|
await foldersStore.fetchFolders(null)
|
|
return
|
|
}
|
|
await foldersStore.navigateTo(folderId)
|
|
await Promise.all([
|
|
foldersStore.fetchFolders(folderId),
|
|
docsStore.fetchDocuments({ folderId, sort: docsStore.sortField, order: docsStore.sortOrder }),
|
|
])
|
|
docsStore.currentFolderId = folderId
|
|
}
|
|
|
|
onMounted(() => loadFolder(currentFolderId.value))
|
|
watch(currentFolderId, (id) => loadFolder(id))
|
|
|
|
function navigateToFolder(id) { router.push(`/folders/${id}`) }
|
|
|
|
function handleBreadcrumbNavigate(id) {
|
|
if (id == null) router.push('/')
|
|
else router.push(`/folders/${id}`)
|
|
}
|
|
|
|
function handleSortChange({ sort, order }) {
|
|
docsStore.sortField = sort
|
|
docsStore.sortOrder = order
|
|
docsStore.fetchDocuments({ folderId: currentFolderId.value, sort, order })
|
|
}
|
|
|
|
// ── Upload ────────────────────────────────────────────────────────────────────
|
|
|
|
const uploadQueue = ref([])
|
|
|
|
async function onFilesSelected({ files, autoClassify }) {
|
|
const folderId = currentFolderId.value
|
|
const promises = files.map(file => {
|
|
const item = reactive({ name: file.name, done: false, error: null, quotaError: null, topics: null })
|
|
uploadQueue.value.unshift(item)
|
|
return docsStore.upload(file, autoClassify, folderId)
|
|
.then(({ doc }) => { item.done = true; item.topics = doc.topics ?? [] })
|
|
.catch(e => {
|
|
if (e.status === 413 && e.payload) item.quotaError = e.payload
|
|
else item.error = e.message
|
|
})
|
|
})
|
|
await Promise.allSettled(promises)
|
|
await topicsStore.fetchTopics()
|
|
}
|
|
|
|
// ── Folder CRUD ───────────────────────────────────────────────────────────────
|
|
|
|
async function handleFolderCreate({ name, onError, onSuccess }) {
|
|
try {
|
|
await foldersStore.createFolder(name, currentFolderId.value)
|
|
onSuccess()
|
|
} catch (e) {
|
|
onError(e.message || 'Failed to create folder.')
|
|
}
|
|
}
|
|
|
|
async function handleFolderRename({ id, name }) {
|
|
if (!name) return
|
|
try { await foldersStore.renameFolder(id, name) } catch {}
|
|
}
|
|
|
|
const folderToDelete = ref(null)
|
|
|
|
async function confirmDeleteFolder() {
|
|
if (!folderToDelete.value) return
|
|
try { await foldersStore.deleteFolder(folderToDelete.value.id) }
|
|
finally { folderToDelete.value = null }
|
|
}
|
|
|
|
// ── Document actions ──────────────────────────────────────────────────────────
|
|
|
|
const shareDoc = ref(null)
|
|
|
|
async function doMove(docId, folderId) {
|
|
try { await docsStore.moveToFolder(docId, folderId) } catch (e) { console.error(e.message) }
|
|
}
|
|
|
|
async function doDeleteDoc(docId) {
|
|
try { await docsStore.remove(docId) } catch (e) { console.error(e.message) }
|
|
}
|
|
|
|
// ── Topic color lookup ────────────────────────────────────────────────────────
|
|
|
|
function topicColor(name) {
|
|
return topicsStore.topics.find(t => t.name === name)?.color ?? '#6366f1'
|
|
}
|
|
</script>
|