|
@@ -1,19 +1,54 @@
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref } from "vue";
|
|
|
|
|
|
|
+import { ref, computed } from "vue";
|
|
|
|
|
|
|
|
const emit = defineEmits(["ingested"]);
|
|
const emit = defineEmits(["ingested"]);
|
|
|
|
|
|
|
|
|
|
+const tab = ref("arquivo");
|
|
|
const manualText = ref("");
|
|
const manualText = ref("");
|
|
|
const file = ref(null);
|
|
const file = ref(null);
|
|
|
const loading = ref(false);
|
|
const loading = ref(false);
|
|
|
const status = ref("");
|
|
const status = ref("");
|
|
|
const error = ref("");
|
|
const error = ref("");
|
|
|
|
|
+const dragOver = ref(false);
|
|
|
|
|
+const fileInputRef = ref(null);
|
|
|
|
|
|
|
|
-async function onFile(e) {
|
|
|
|
|
|
|
+function formatSize(bytes) {
|
|
|
|
|
+ if (bytes < 1024) return `${bytes} B`;
|
|
|
|
|
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
|
|
|
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getFileIcon(f) {
|
|
|
|
|
+ const ext = f?.name?.split(".").pop()?.toLowerCase() ?? "";
|
|
|
|
|
+ if (ext === "pdf") return "pdf";
|
|
|
|
|
+ if (["png", "jpg", "jpeg", "webp"].includes(ext)) return "image";
|
|
|
|
|
+ return "file";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onFileInputChange(e) {
|
|
|
const f = e.target.files?.[0];
|
|
const f = e.target.files?.[0];
|
|
|
if (!f) return;
|
|
if (!f) return;
|
|
|
file.value = f;
|
|
file.value = f;
|
|
|
- status.value = `Arquivo carregado: ${f.name}`;
|
|
|
|
|
|
|
+ error.value = "";
|
|
|
|
|
+ status.value = "";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onDrop(e) {
|
|
|
|
|
+ dragOver.value = false;
|
|
|
|
|
+ const f = e.dataTransfer?.files?.[0];
|
|
|
|
|
+ if (!f) return;
|
|
|
|
|
+ file.value = f;
|
|
|
|
|
+ error.value = "";
|
|
|
|
|
+ status.value = "";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeFile() {
|
|
|
|
|
+ file.value = null;
|
|
|
|
|
+ if (fileInputRef.value) fileInputRef.value.value = "";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function triggerFileInput() {
|
|
|
|
|
+ fileInputRef.value?.click();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function onIngest() {
|
|
async function onIngest() {
|
|
@@ -21,50 +56,157 @@ async function onIngest() {
|
|
|
status.value = "";
|
|
status.value = "";
|
|
|
loading.value = true;
|
|
loading.value = true;
|
|
|
try {
|
|
try {
|
|
|
- const hasManual = Boolean(manualText.value.trim());
|
|
|
|
|
- if (hasManual) {
|
|
|
|
|
|
|
+ if (tab.value === "texto" && manualText.value.trim()) {
|
|
|
emit("ingested", { text: manualText.value.trim(), source: "manual" });
|
|
emit("ingested", { text: manualText.value.trim(), source: "manual" });
|
|
|
} else if (file.value) {
|
|
} else if (file.value) {
|
|
|
emit("ingested", { file: file.value, source: "upload" });
|
|
emit("ingested", { file: file.value, source: "upload" });
|
|
|
}
|
|
}
|
|
|
manualText.value = "";
|
|
manualText.value = "";
|
|
|
file.value = null;
|
|
file.value = null;
|
|
|
- status.value = "Enviado para ingestão.";
|
|
|
|
|
|
|
+ if (fileInputRef.value) fileInputRef.value.value = "";
|
|
|
|
|
+ status.value = "Documento enviado para ingestão com sucesso.";
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- error.value = e?.message || "erro";
|
|
|
|
|
|
|
+ error.value = e?.message || "Erro desconhecido.";
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.value = false;
|
|
loading.value = false;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-</script>
|
|
|
|
|
|
|
|
|
|
|
|
+const canSubmit = computed(() => {
|
|
|
|
|
+ if (tab.value === "arquivo") return !!file.value;
|
|
|
|
|
+ return !!manualText.value.trim();
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
|
- <section class="rounded-2xl border border-gray-300 bg-gray-100 p-4 shadow-sm backdrop-blur-md dark:border-gray-700 dark:bg-secondary/50">
|
|
|
|
|
- <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Update</h2>
|
|
|
|
|
- <div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
|
|
|
- Envie .txt, .pdf, .docx ou imagens (png/jpg) com manuais/prints para alimentar a base.
|
|
|
|
|
|
|
+ <div class="grid gap-4">
|
|
|
|
|
+ <!-- Tabs -->
|
|
|
|
|
+ <div class="flex gap-1 rounded-xl bg-gray-100 p-1 dark:bg-white/5">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="t in [{ key: 'arquivo', label: 'Arquivo' }, { key: 'texto', label: 'Texto' }]"
|
|
|
|
|
+ :key="t.key"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="flex-1 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors"
|
|
|
|
|
+ :class="tab === t.key
|
|
|
|
|
+ ? 'bg-background text-gray-900 shadow-sm dark:text-gray-100'
|
|
|
|
|
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
|
|
|
|
+ @click="tab = t.key"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ t.label }}
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
- <input
|
|
|
|
|
- class="mt-3"
|
|
|
|
|
- type="file"
|
|
|
|
|
- accept=".txt,.pdf,.docx,.png,.jpg,.jpeg,.webp,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/*,text/plain"
|
|
|
|
|
- @change="onFile"
|
|
|
|
|
- />
|
|
|
|
|
- <textarea
|
|
|
|
|
- v-model="manualText"
|
|
|
|
|
- class="mt-3"
|
|
|
|
|
- rows="6"
|
|
|
|
|
- placeholder="Ou cole um texto aqui para ingestão..."
|
|
|
|
|
- />
|
|
|
|
|
- <button
|
|
|
|
|
- class="mt-3 rounded-lg border border-primary bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground transition-colors hover:border-[var(--primary-600)] hover:bg-[var(--primary-600)] disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
- :disabled="loading || (!manualText.trim() && !file)"
|
|
|
|
|
- @click="onIngest"
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Aba: Arquivo -->
|
|
|
|
|
+ <template v-if="tab === 'arquivo'">
|
|
|
|
|
+ <!-- Preview do arquivo selecionado -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="file"
|
|
|
|
|
+ class="flex items-center gap-3 rounded-xl border border-gray-200 bg-background/60 p-3 dark:border-gray-700 dark:bg-background/10"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-white/5">
|
|
|
|
|
+ <svg v-if="getFileIcon(file) === 'pdf'" class="h-5 w-5 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ <svg v-else-if="getFileIcon(file) === 'image'" class="h-5 w-5 text-blue-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ <svg v-else class="h-5 w-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="min-w-0 flex-1">
|
|
|
|
|
+ <div class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">{{ file.name }}</div>
|
|
|
|
|
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ formatSize(file.size) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="shrink-0 rounded-lg border border-transparent p-1.5 text-gray-400 transition-colors hover:border-gray-200 hover:bg-gray-100 dark:hover:border-gray-700 dark:hover:bg-white/5"
|
|
|
|
|
+ @click="removeFile"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Zona de drag-and-drop -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-else
|
|
|
|
|
+ class="relative flex cursor-pointer flex-col items-center gap-3 rounded-xl border-2 border-dashed px-6 py-10 transition-colors"
|
|
|
|
|
+ :class="dragOver
|
|
|
|
|
+ ? 'border-primary bg-primary/5'
|
|
|
|
|
+ : 'border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600'"
|
|
|
|
|
+ @click="triggerFileInput"
|
|
|
|
|
+ @dragover.prevent="dragOver = true"
|
|
|
|
|
+ @dragleave.prevent="dragOver = false"
|
|
|
|
|
+ @drop.prevent="onDrop"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="flex h-12 w-12 items-center justify-center rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-white/5">
|
|
|
|
|
+ <svg class="h-6 w-6 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
|
|
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="text-center">
|
|
|
|
|
+ <div class="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
|
|
|
+ Arraste um arquivo ou <span class="text-primary underline underline-offset-2">clique para selecionar</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">PDF, Word, TXT, PNG, JPG, WEBP</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <input
|
|
|
|
|
+ ref="fileInputRef"
|
|
|
|
|
+ type="file"
|
|
|
|
|
+ accept=".txt,.pdf,.docx,.png,.jpg,.jpeg,.webp,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/*,text/plain"
|
|
|
|
|
+ class="sr-only"
|
|
|
|
|
+ @change="onFileInputChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Aba: Texto -->
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div class="rounded-2xl border border-gray-200 bg-white/70 p-4 shadow-sm backdrop-blur-md dark:border-white/10 dark:bg-white/[0.04]">
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ v-model="manualText"
|
|
|
|
|
+ rows="8"
|
|
|
|
|
+ placeholder="Cole o conteúdo aqui para ingestão..."
|
|
|
|
|
+ class="min-h-32 w-full resize-none border-0 bg-transparent text-sm shadow-none !outline-none focus:ring-0"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Linha de ação -->
|
|
|
|
|
+ <div class="flex items-center justify-between gap-3">
|
|
|
|
|
+ <div class="text-xs text-gray-400 dark:text-gray-500">
|
|
|
|
|
+ {{ tab === 'arquivo'
|
|
|
|
|
+ ? 'O arquivo será processado e indexado na base de conhecimento.'
|
|
|
|
|
+ : 'O texto será fragmentado e indexado na base.' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="inline-flex shrink-0 items-center gap-2 rounded-xl border border-primary bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:border-[var(--primary-600)] hover:bg-[var(--primary-600)] disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
+ :disabled="loading || !canSubmit"
|
|
|
|
|
+ @click="onIngest"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg v-if="loading" class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
|
|
|
|
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
|
|
|
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ {{ loading ? "Enviando..." : "Ingerir" }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Alertas de feedback -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="status"
|
|
|
|
|
+ class="rounded-xl border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700 dark:border-green-500/30 dark:bg-green-500/10 dark:text-green-300"
|
|
|
>
|
|
>
|
|
|
- Ingerir
|
|
|
|
|
- </button>
|
|
|
|
|
- <div v-if="status" class="mt-2.5 text-sm text-gray-500 dark:text-gray-400">{{ status }}</div>
|
|
|
|
|
- <div v-if="error" class="mt-2.5 text-sm text-gray-500 dark:text-gray-400">Erro: {{ error }}</div>
|
|
|
|
|
- </section>
|
|
|
|
|
|
|
+ {{ status }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="error"
|
|
|
|
|
+ class="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-300"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ error }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|